diff --git a/.github/workflows/generator-generic-ossf-slsa3-publish.yml b/.github/workflows/generator-generic-ossf-slsa3-publish.yml
new file mode 100644
index 0000000..35c829b
--- /dev/null
+++ b/.github/workflows/generator-generic-ossf-slsa3-publish.yml
@@ -0,0 +1,66 @@
+# This workflow uses actions that are not certified by GitHub.
+# They are provided by a third-party and are governed by
+# separate terms of service, privacy policy, and support
+# documentation.
+
+# This workflow lets you generate SLSA provenance file for your project.
+# The generation satisfies level 3 for the provenance requirements - see https://slsa.dev/spec/v0.1/requirements
+# The project is an initiative of the OpenSSF (openssf.org) and is developed at
+# https://github.com/slsa-framework/slsa-github-generator.
+# The provenance file can be verified using https://github.com/slsa-framework/slsa-verifier.
+# For more information about SLSA and how it improves the supply-chain, visit slsa.dev.
+
+name: SLSA generic generator
+on:
+ workflow_dispatch:
+ release:
+ types: [created]
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ outputs:
+ digests: ${{ steps.hash.outputs.digests }}
+
+ steps:
+ - uses: actions/checkout@v4
+
+ # ========================================================
+ #
+ # Step 1: Build your artifacts.
+ #
+ # ========================================================
+ - name: Build artifacts
+ run: |
+ # These are some amazing artifacts.
+ echo "artifact1" > artifact1
+ echo "artifact2" > artifact2
+
+ # ========================================================
+ #
+ # Step 2: Add a step to generate the provenance subjects
+ # as shown below. Update the sha256 sum arguments
+ # to include all binaries that you generate
+ # provenance for.
+ #
+ # ========================================================
+ - name: Generate subject for provenance
+ id: hash
+ run: |
+ set -euo pipefail
+
+ # List the artifacts the provenance will refer to.
+ files=$(ls artifact*)
+ # Generate the subjects (base64 encoded).
+ echo "hashes=$(sha256sum $files | base64 -w0)" >> "${GITHUB_OUTPUT}"
+
+ provenance:
+ needs: [build]
+ permissions:
+ actions: read # To read the workflow path.
+ id-token: write # To sign the provenance.
+ contents: write # To add assets to a release.
+ uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v1.4.0
+ with:
+ base64-subjects: "${{ needs.build.outputs.digests }}"
+ upload-assets: true # Optional: Upload to a new release
diff --git a/.github/workflows/python-publish.yml b/.github/workflows/python-publish.yml
new file mode 100644
index 0000000..82f8dbd
--- /dev/null
+++ b/.github/workflows/python-publish.yml
@@ -0,0 +1,70 @@
+# This workflow will upload a Python Package to PyPI when a release is created
+# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python#publishing-to-package-registries
+
+# This workflow uses actions that are not certified by GitHub.
+# They are provided by a third-party and are governed by
+# separate terms of service, privacy policy, and support
+# documentation.
+
+name: Upload Python Package
+
+on:
+ release:
+ types: [published]
+
+permissions:
+ contents: read
+
+jobs:
+ release-build:
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - uses: actions/setup-python@v5
+ with:
+ python-version: "3.x"
+
+ - name: Build release distributions
+ run: |
+ # NOTE: put your own distribution build steps here.
+ python -m pip install build
+ python -m build
+
+ - name: Upload distributions
+ uses: actions/upload-artifact@v4
+ with:
+ name: release-dists
+ path: dist/
+
+ pypi-publish:
+ runs-on: ubuntu-latest
+ needs:
+ - release-build
+ permissions:
+ # IMPORTANT: this permission is mandatory for trusted publishing
+ id-token: write
+
+ # Dedicated environments with protections for publishing are strongly recommended.
+ # For more information, see: https://docs.github.com/en/actions/deployment/targeting-different-environments/using-environments-for-deployment#deployment-protection-rules
+ environment:
+ name: pypi
+ # OPTIONAL: uncomment and update to include your PyPI project URL in the deployment status:
+ # url: https://pypi.org/p/YOURPROJECT
+ #
+ # ALTERNATIVE: if your GitHub Release name is the PyPI project version string
+ # ALTERNATIVE: exactly, uncomment the following line instead:
+ # url: https://pypi.org/project/YOURPROJECT/${{ github.event.release.name }}
+
+ steps:
+ - name: Retrieve release distributions
+ uses: actions/download-artifact@v4
+ with:
+ name: release-dists
+ path: dist/
+
+ - name: Publish release distributions to PyPI
+ uses: pypa/gh-action-pypi-publish@release/v1
+ with:
+ packages-dir: dist/
diff --git a/.gitignore b/.gitignore
index 82348fb..7202236 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,31 +1,17 @@
-workplace_*/
-workspace_*/
+node_modules/
*.log
-code_db/*
-results/*
-
-__pycache__/
-tmp/*
-logs/*
-*.tar.gz
-
-*.egg-info
-
-.DS_Store
-*.csv
-
-eval_data/*
-evaluation_results/*
-casestudy_results/*
-
-evaluation/*/data/
-evaluation/*/data/*
-evaluation/**/data/
-
.env
+.DS_Store
+nabdh_data.json
-terminal_tmp/*
-
-!tool_docs.csv
-
-.port*
+# Python files — not part of the Node.js app, exclude from deploy detection
+*.py
+*.cfg
+*.toml
+__pycache__/
+*.egg-info/
+dist/
+build/
+.python-version
+venv/
+env/
diff --git a/constant.py b/constant.py
deleted file mode 100644
index ba72a2b..0000000
--- a/constant.py
+++ /dev/null
@@ -1,91 +0,0 @@
-import os
-from dotenv import load_dotenv
-import platform
-# utils:
-load_dotenv() # 加载.env文件
-def str_to_bool(value):
- """convert string to bool"""
- true_values = {'true', 'yes', '1', 'on', 't', 'y'}
- false_values = {'false', 'no', '0', 'off', 'f', 'n'}
-
- if isinstance(value, bool):
- return value
-
- if value == None:
- return None
-
- value = str(value).lower().strip()
- if value in true_values:
- return True
- if value in false_values:
- return False
- return True # default return True
-
-
-DOCKER_WORKPLACE_NAME = os.getenv('DOCKER_WORKPLACE_NAME', 'workplace')
-GITHUB_AI_TOKEN = os.getenv('GITHUB_AI_TOKEN', None)
-AI_USER = os.getenv('AI_USER', "tjb-tech")
-LOCAL_ROOT = os.getenv('LOCAL_ROOT', os.getcwd())
-
-DEBUG = str_to_bool(os.getenv('DEBUG', False))
-
-DEFAULT_LOG = str_to_bool(os.getenv('DEFAULT_LOG', False))
-LOG_PATH = os.getenv('LOG_PATH', None)
-EVAL_MODE = str_to_bool(os.getenv('EVAL_MODE', False))
-BASE_IMAGES = os.getenv('BASE_IMAGES', None)
-
-def get_architecture():
- machine = platform.machine().lower()
- if 'x86' in machine or 'amd64' in machine or 'i386' in machine:
- return "tjbtech1/metachain:amd64_latest"
- elif 'arm' in machine:
- return "tjbtech1/metachain:latest"
- else:
- return "tjbtech1/metachain:latest"
-if BASE_IMAGES is None:
- BASE_IMAGES = get_architecture()
-
-COMPLETION_MODEL = os.getenv('COMPLETION_MODEL', "claude-3-5-sonnet-20241022")
-EMBEDDING_MODEL = os.getenv('EMBEDDING_MODEL', "text-embedding-3-small")
-
-MC_MODE = str_to_bool(os.getenv('MC_MODE', True))
-
-# add Env for function call and non-function call
-
-FN_CALL = str_to_bool(os.getenv('FN_CALL', None))
-API_BASE_URL = os.getenv('API_BASE_URL', None)
-ADD_USER = str_to_bool(os.getenv('ADD_USER', None))
-
-
-
-NOT_SUPPORT_SENDER = ["mistral", "groq"]
-MUST_ADD_USER = ["deepseek-reasoner", "o1-mini", "deepseek-r1"]
-
-NOT_SUPPORT_FN_CALL = ["o1-mini", "deepseek-reasoner", "deepseek-r1", "llama", "grok-2"]
-NOT_USE_FN_CALL = [ "deepseek-chat"] + NOT_SUPPORT_FN_CALL
-
-if ADD_USER is None:
- ADD_USER = False
- for model in MUST_ADD_USER:
- if model in COMPLETION_MODEL:
- ADD_USER = True
- break
-
-if FN_CALL is None:
- FN_CALL = True
- for model in NOT_USE_FN_CALL:
- if model in COMPLETION_MODEL:
- FN_CALL = False
- break
-
-NON_FN_CALL = False
-for model in NOT_SUPPORT_FN_CALL:
- if model in COMPLETION_MODEL:
- NON_FN_CALL = True
- break
-
-
-if EVAL_MODE:
- DEFAULT_LOG = False
-
-# print(FN_CALL, NON_FN_CALL, ADD_USER)
\ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
new file mode 100644
index 0000000..4408858
--- /dev/null
+++ b/package-lock.json
@@ -0,0 +1,1182 @@
+{
+ "name": "nabdh",
+ "version": "1.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "nabdh",
+ "version": "1.0.0",
+ "license": "ISC",
+ "dependencies": {
+ "compression": "^1.8.1",
+ "cors": "^2.8.6",
+ "express": "^5.2.1",
+ "socket.io": "^4.8.3",
+ "uuid": "^14.0.0"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@socket.io/component-emitter": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz",
+ "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==",
+ "license": "MIT"
+ },
+ "node_modules/@types/cors": {
+ "version": "2.8.19",
+ "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz",
+ "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/node": {
+ "version": "25.6.0",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-25.6.0.tgz",
+ "integrity": "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==",
+ "license": "MIT",
+ "dependencies": {
+ "undici-types": "~7.19.0"
+ }
+ },
+ "node_modules/@types/ws": {
+ "version": "8.18.1",
+ "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
+ "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/accepts": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz",
+ "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-types": "^3.0.0",
+ "negotiator": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/base64id": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz",
+ "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==",
+ "license": "MIT",
+ "engines": {
+ "node": "^4.5.0 || >= 5.9"
+ }
+ },
+ "node_modules/body-parser": {
+ "version": "2.2.2",
+ "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz",
+ "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==",
+ "license": "MIT",
+ "dependencies": {
+ "bytes": "^3.1.2",
+ "content-type": "^1.0.5",
+ "debug": "^4.4.3",
+ "http-errors": "^2.0.0",
+ "iconv-lite": "^0.7.0",
+ "on-finished": "^2.4.1",
+ "qs": "^6.14.1",
+ "raw-body": "^3.0.1",
+ "type-is": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/bytes": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
+ "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/call-bind-apply-helpers": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+ "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/call-bound": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
+ "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "get-intrinsic": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/compressible": {
+ "version": "2.0.18",
+ "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz",
+ "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": ">= 1.43.0 < 2"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/compression": {
+ "version": "1.8.1",
+ "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.1.tgz",
+ "integrity": "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==",
+ "license": "MIT",
+ "dependencies": {
+ "bytes": "3.1.2",
+ "compressible": "~2.0.18",
+ "debug": "2.6.9",
+ "negotiator": "~0.6.4",
+ "on-headers": "~1.1.0",
+ "safe-buffer": "5.2.1",
+ "vary": "~1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/compression/node_modules/debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/compression/node_modules/ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
+ "license": "MIT"
+ },
+ "node_modules/compression/node_modules/negotiator": {
+ "version": "0.6.4",
+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz",
+ "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/content-disposition": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz",
+ "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/content-type": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
+ "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/cookie": {
+ "version": "0.7.2",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
+ "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/cookie-signature": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz",
+ "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.6.0"
+ }
+ },
+ "node_modules/cors": {
+ "version": "2.8.6",
+ "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz",
+ "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==",
+ "license": "MIT",
+ "dependencies": {
+ "object-assign": "^4",
+ "vary": "^1"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/debug": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/depd": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
+ "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/dunder-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
+ "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "gopd": "^1.2.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/ee-first": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
+ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
+ "license": "MIT"
+ },
+ "node_modules/encodeurl": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
+ "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/engine.io": {
+ "version": "6.6.6",
+ "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.6.tgz",
+ "integrity": "sha512-U2SN0w3OpjFRVlrc17E6TMDmH58Xl9rai1MblNjAdwWp07Kk+llmzX0hjDpQdrDGzwmvOtgM5yI+meYX6iZ2xA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/cors": "^2.8.12",
+ "@types/node": ">=10.0.0",
+ "@types/ws": "^8.5.12",
+ "accepts": "~1.3.4",
+ "base64id": "2.0.0",
+ "cookie": "~0.7.2",
+ "cors": "~2.8.5",
+ "debug": "~4.4.1",
+ "engine.io-parser": "~5.2.1",
+ "ws": "~8.18.3"
+ },
+ "engines": {
+ "node": ">=10.2.0"
+ }
+ },
+ "node_modules/engine.io-parser": {
+ "version": "5.2.3",
+ "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz",
+ "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.0.0"
+ }
+ },
+ "node_modules/engine.io/node_modules/accepts": {
+ "version": "1.3.8",
+ "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
+ "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-types": "~2.1.34",
+ "negotiator": "0.6.3"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/engine.io/node_modules/mime-db": {
+ "version": "1.52.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/engine.io/node_modules/mime-types": {
+ "version": "2.1.35",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": "1.52.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/engine.io/node_modules/negotiator": {
+ "version": "0.6.3",
+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
+ "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/es-define-property": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-errors": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-object-atoms": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
+ "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/escape-html": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
+ "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
+ "license": "MIT"
+ },
+ "node_modules/etag": {
+ "version": "1.8.1",
+ "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
+ "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/express": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz",
+ "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==",
+ "license": "MIT",
+ "dependencies": {
+ "accepts": "^2.0.0",
+ "body-parser": "^2.2.1",
+ "content-disposition": "^1.0.0",
+ "content-type": "^1.0.5",
+ "cookie": "^0.7.1",
+ "cookie-signature": "^1.2.1",
+ "debug": "^4.4.0",
+ "depd": "^2.0.0",
+ "encodeurl": "^2.0.0",
+ "escape-html": "^1.0.3",
+ "etag": "^1.8.1",
+ "finalhandler": "^2.1.0",
+ "fresh": "^2.0.0",
+ "http-errors": "^2.0.0",
+ "merge-descriptors": "^2.0.0",
+ "mime-types": "^3.0.0",
+ "on-finished": "^2.4.1",
+ "once": "^1.4.0",
+ "parseurl": "^1.3.3",
+ "proxy-addr": "^2.0.7",
+ "qs": "^6.14.0",
+ "range-parser": "^1.2.1",
+ "router": "^2.2.0",
+ "send": "^1.1.0",
+ "serve-static": "^2.2.0",
+ "statuses": "^2.0.1",
+ "type-is": "^2.0.1",
+ "vary": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/finalhandler": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz",
+ "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^4.4.0",
+ "encodeurl": "^2.0.0",
+ "escape-html": "^1.0.3",
+ "on-finished": "^2.4.1",
+ "parseurl": "^1.3.3",
+ "statuses": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 18.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/forwarded": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
+ "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/fresh": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz",
+ "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/function-bind": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-intrinsic": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+ "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "es-define-property": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.1.1",
+ "function-bind": "^1.1.2",
+ "get-proto": "^1.0.1",
+ "gopd": "^1.2.0",
+ "has-symbols": "^1.1.0",
+ "hasown": "^2.0.2",
+ "math-intrinsics": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
+ "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+ "license": "MIT",
+ "dependencies": {
+ "dunder-proto": "^1.0.1",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/gopd": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-symbols": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
+ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/hasown": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz",
+ "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==",
+ "license": "MIT",
+ "dependencies": {
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/http-errors": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
+ "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
+ "license": "MIT",
+ "dependencies": {
+ "depd": "~2.0.0",
+ "inherits": "~2.0.4",
+ "setprototypeof": "~1.2.0",
+ "statuses": "~2.0.2",
+ "toidentifier": "~1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/iconv-lite": {
+ "version": "0.7.2",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz",
+ "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==",
+ "license": "MIT",
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/inherits": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+ "license": "ISC"
+ },
+ "node_modules/ipaddr.js": {
+ "version": "1.9.1",
+ "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
+ "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/is-promise": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz",
+ "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==",
+ "license": "MIT"
+ },
+ "node_modules/math-intrinsics": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/media-typer": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz",
+ "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/merge-descriptors": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz",
+ "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/mime-db": {
+ "version": "1.54.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
+ "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime-types": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz",
+ "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": "^1.54.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "license": "MIT"
+ },
+ "node_modules/negotiator": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz",
+ "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/object-assign": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
+ "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/object-inspect": {
+ "version": "1.13.4",
+ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
+ "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/on-finished": {
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
+ "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
+ "license": "MIT",
+ "dependencies": {
+ "ee-first": "1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/on-headers": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz",
+ "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/once": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+ "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
+ "license": "ISC",
+ "dependencies": {
+ "wrappy": "1"
+ }
+ },
+ "node_modules/parseurl": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
+ "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/path-to-regexp": {
+ "version": "8.4.2",
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz",
+ "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==",
+ "license": "MIT",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/proxy-addr": {
+ "version": "2.0.7",
+ "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
+ "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
+ "license": "MIT",
+ "dependencies": {
+ "forwarded": "0.2.0",
+ "ipaddr.js": "1.9.1"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/qs": {
+ "version": "6.15.1",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.1.tgz",
+ "integrity": "sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "side-channel": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=0.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/range-parser": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
+ "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/raw-body": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz",
+ "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==",
+ "license": "MIT",
+ "dependencies": {
+ "bytes": "~3.1.2",
+ "http-errors": "~2.0.1",
+ "iconv-lite": "~0.7.0",
+ "unpipe": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/router": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz",
+ "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^4.4.0",
+ "depd": "^2.0.0",
+ "is-promise": "^4.0.0",
+ "parseurl": "^1.3.3",
+ "path-to-regexp": "^8.0.0"
+ },
+ "engines": {
+ "node": ">= 18"
+ }
+ },
+ "node_modules/safe-buffer": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/safer-buffer": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
+ "license": "MIT"
+ },
+ "node_modules/send": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz",
+ "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^4.4.3",
+ "encodeurl": "^2.0.0",
+ "escape-html": "^1.0.3",
+ "etag": "^1.8.1",
+ "fresh": "^2.0.0",
+ "http-errors": "^2.0.1",
+ "mime-types": "^3.0.2",
+ "ms": "^2.1.3",
+ "on-finished": "^2.4.1",
+ "range-parser": "^1.2.1",
+ "statuses": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/serve-static": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz",
+ "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==",
+ "license": "MIT",
+ "dependencies": {
+ "encodeurl": "^2.0.0",
+ "escape-html": "^1.0.3",
+ "parseurl": "^1.3.3",
+ "send": "^1.2.0"
+ },
+ "engines": {
+ "node": ">= 18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/setprototypeof": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
+ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
+ "license": "ISC"
+ },
+ "node_modules/side-channel": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
+ "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "object-inspect": "^1.13.3",
+ "side-channel-list": "^1.0.0",
+ "side-channel-map": "^1.0.1",
+ "side-channel-weakmap": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-list": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz",
+ "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "object-inspect": "^1.13.4"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-map": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
+ "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.5",
+ "object-inspect": "^1.13.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-weakmap": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
+ "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.5",
+ "object-inspect": "^1.13.3",
+ "side-channel-map": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/socket.io": {
+ "version": "4.8.3",
+ "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.3.tgz",
+ "integrity": "sha512-2Dd78bqzzjE6KPkD5fHZmDAKRNe3J15q+YHDrIsy9WEkqttc7GY+kT9OBLSMaPbQaEd0x1BjcmtMtXkfpc+T5A==",
+ "license": "MIT",
+ "dependencies": {
+ "accepts": "~1.3.4",
+ "base64id": "~2.0.0",
+ "cors": "~2.8.5",
+ "debug": "~4.4.1",
+ "engine.io": "~6.6.0",
+ "socket.io-adapter": "~2.5.2",
+ "socket.io-parser": "~4.2.4"
+ },
+ "engines": {
+ "node": ">=10.2.0"
+ }
+ },
+ "node_modules/socket.io-adapter": {
+ "version": "2.5.6",
+ "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.6.tgz",
+ "integrity": "sha512-DkkO/dz7MGln0dHn5bmN3pPy+JmywNICWrJqVWiVOyvXjWQFIv9c2h24JrQLLFJ2aQVQf/Cvl1vblnd4r2apLQ==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "~4.4.1",
+ "ws": "~8.18.3"
+ }
+ },
+ "node_modules/socket.io-parser": {
+ "version": "4.2.6",
+ "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.6.tgz",
+ "integrity": "sha512-asJqbVBDsBCJx0pTqw3WfesSY0iRX+2xzWEWzrpcH7L6fLzrhyF8WPI8UaeM4YCuDfpwA/cgsdugMsmtz8EJeg==",
+ "license": "MIT",
+ "dependencies": {
+ "@socket.io/component-emitter": "~3.1.0",
+ "debug": "~4.4.1"
+ },
+ "engines": {
+ "node": ">=10.0.0"
+ }
+ },
+ "node_modules/socket.io/node_modules/accepts": {
+ "version": "1.3.8",
+ "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
+ "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-types": "~2.1.34",
+ "negotiator": "0.6.3"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/socket.io/node_modules/mime-db": {
+ "version": "1.52.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/socket.io/node_modules/mime-types": {
+ "version": "2.1.35",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": "1.52.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/socket.io/node_modules/negotiator": {
+ "version": "0.6.3",
+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
+ "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/statuses": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
+ "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/toidentifier": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
+ "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.6"
+ }
+ },
+ "node_modules/type-is": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz",
+ "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==",
+ "license": "MIT",
+ "dependencies": {
+ "content-type": "^1.0.5",
+ "media-typer": "^1.1.0",
+ "mime-types": "^3.0.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/undici-types": {
+ "version": "7.19.2",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.19.2.tgz",
+ "integrity": "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==",
+ "license": "MIT"
+ },
+ "node_modules/unpipe": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
+ "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/uuid": {
+ "version": "14.0.0",
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-14.0.0.tgz",
+ "integrity": "sha512-Qo+uWgilfSmAhXCMav1uYFynlQO7fMFiMVZsQqZRMIXp0O7rR7qjkj+cPvBHLgBqi960QCoo/PH2/6ZtVqKvrg==",
+ "funding": [
+ "https://github.com/sponsors/broofa",
+ "https://github.com/sponsors/ctavan"
+ ],
+ "license": "MIT",
+ "bin": {
+ "uuid": "dist-node/bin/uuid"
+ }
+ },
+ "node_modules/vary": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
+ "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/wrappy": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
+ "license": "ISC"
+ },
+ "node_modules/ws": {
+ "version": "8.18.3",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz",
+ "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.0.0"
+ },
+ "peerDependencies": {
+ "bufferutil": "^4.0.1",
+ "utf-8-validate": ">=5.0.2"
+ },
+ "peerDependenciesMeta": {
+ "bufferutil": {
+ "optional": true
+ },
+ "utf-8-validate": {
+ "optional": true
+ }
+ }
+ }
+ }
+}
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..c6f682d
--- /dev/null
+++ b/package.json
@@ -0,0 +1,28 @@
+{
+ "name": "nabdh",
+ "version": "1.0.0",
+ "main": "server.js",
+ "scripts": {
+ "start": "node server.js",
+ "dev": "node server.js"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ },
+ "keywords": [
+ "nabdh",
+ "sudan",
+ "community",
+ "app"
+ ],
+ "author": "",
+ "license": "ISC",
+ "description": "نبض - تطبيق المدينة الحي",
+ "dependencies": {
+ "compression": "^1.8.1",
+ "cors": "^2.8.6",
+ "express": "^5.2.1",
+ "socket.io": "^4.8.3",
+ "uuid": "^14.0.0"
+ }
+}
diff --git a/process_tool_docs.py b/process_tool_docs.py
deleted file mode 100644
index 1cdfa2b..0000000
--- a/process_tool_docs.py
+++ /dev/null
@@ -1,20 +0,0 @@
-from pandas import read_csv
-import json
-from rich import print
-
-df = read_csv("tool_docs.csv")
-
-rapidapi_tools = df[df['Platform'] == 'RapidAPI']['Tool_Name'].unique()
-print("[bold blue]Current RapidAPI tools:[/bold blue]")
-print(json.dumps(rapidapi_tools.tolist(), indent=4))
-print("[bold red][IMPORTANT][/bold red] [bold yellow]If you want to use these tools, you should go to RapidAPI and subscribe to them. More convenient tool platforms such as Composio are under development.[/bold yellow]")
-
-your_api_key = input("Please input your RapidAPI API key:")
-
-for column in df.columns:
- if df[column].dtype == 'object':
- df[column] = df[column].str.replace('YOUR_RAPID_API_KEY', your_api_key)
-
-df.to_csv('tool_docs.csv', index=False)
-
-print("[bold green]Done![/bold green]")
\ No newline at end of file
diff --git a/public/css/style.css b/public/css/style.css
new file mode 100644
index 0000000..307e7f8
--- /dev/null
+++ b/public/css/style.css
@@ -0,0 +1,9257 @@
+/* ===== NABDH نبض - COMPLETE STYLESHEET v3 ===== */
+@import url('https://fonts.googleapis.com/css2?family=Tajawal:wght@300;400;500;700;800;900&display=swap');
+
+*,*::before,*::after { box-sizing:border-box; margin:0; padding:0 }
+:root {
+ --red:#e74c3c; --red2:#c0392b;
+ --green:#2ecc71; --green2:#27ae60;
+ --blue:#3498db; --blue2:#2980b9;
+ --orange:#e67e22; --orange2:#d35400;
+ --purple:#9b59b6; --purple2:#8e44ad;
+ --yellow:#f1c40f; --yellow2:#f39c12;
+ --teal:#1abc9c; --teal2:#16a085;
+ --pink:#e91e63;
+ --dark:#0a0e1a;
+ --dark2:#0f1724;
+ --dark3:#161d2e;
+ --dark4:#1e2740;
+ --dark5:#27334d;
+ --border:rgba(255,255,255,0.07);
+ --border2:rgba(255,255,255,0.12);
+ --text:#e8edf5;
+ --text2:#8892a4;
+ --text3:#c4cdd8;
+ --accent:#e74c3c;
+ --accent2:#ff6b6b;
+ --glow:rgba(231,76,60,.35);
+ --glow-teal:rgba(26,188,156,.3);
+ --glow-yellow:rgba(241,196,15,.25);
+ --r:14px; --rs:10px; --rxs:7px;
+ --sh: 0 8px 32px rgba(0,0,0,.4);
+ --sh2: 0 4px 16px rgba(0,0,0,.3);
+}
+html,body { height:100%; overflow:hidden; font-family:'Tajawal',sans-serif; background:var(--dark); color:var(--text); direction:rtl }
+
+/* ===== SCROLLBAR ===== */
+::-webkit-scrollbar { width:5px }
+::-webkit-scrollbar-track { background:var(--dark2) }
+::-webkit-scrollbar-thumb { background:var(--dark5); border-radius:3px }
+
+/* ===== SPLASH ===== */
+#splash {
+ position:fixed; inset:0;
+ background:radial-gradient(ellipse at 30% 40%, #1a0515 0%, #080d1a 60%);
+ display:flex; align-items:center; justify-content:center;
+ z-index:9999; transition:opacity .7s ease, transform .7s ease;
+}
+#splash.fade-out { opacity:0; transform:scale(1.04); pointer-events:none }
+.splash-fill {
+ height:100%;
+ background:linear-gradient(90deg, var(--accent), var(--accent2), var(--yellow));
+ border-radius:10px; animation:fillBar 1.2s ease forwards;
+}
+@keyframes fillBar { from{width:0} to{width:100%} }
+.splash-inner { text-align:center; padding:2rem }
+.splash-logo {
+ position:relative; width:130px; height:130px; margin:0 auto 1.8rem;
+ display:flex; align-items:center; justify-content:center;
+}
+.pulse-ring {
+ position:absolute; inset:-10px; border-radius:50%;
+ border:2px solid var(--accent); animation:pulseRing 2.5s ease-out infinite;
+}
+.pulse-ring.d1 { animation-delay:.6s; border-color:rgba(231,76,60,.6) }
+.pulse-ring.d2 { animation-delay:1.2s; border-color:rgba(231,76,60,.3) }
+@keyframes pulseRing { 0%{transform:scale(.5);opacity:1} 100%{transform:scale(2.2);opacity:0} }
+.logo-icon { font-size:4.5rem; animation:hb 1.4s ease-in-out infinite; filter:drop-shadow(0 0 20px var(--accent)) }
+@keyframes hb { 0%,100%{transform:scale(1)} 50%{transform:scale(1.18)} }
+.splash-title {
+ font-size:4.5rem; font-weight:900; color:var(--accent);
+ text-shadow:0 0 50px var(--glow), 0 0 100px rgba(231,76,60,.2);
+ letter-spacing:-1px;
+}
+.splash-sub { color:var(--text2); margin:.6rem 0 2rem; font-size:1.05rem; letter-spacing:.5px }
+.splash-bar {
+ width:220px; height:3px; background:var(--dark4);
+ border-radius:10px; margin:0 auto 1.2rem; overflow:hidden;
+}
+.splash-fill-old-removed {
+ height:100%;
+}
+.splash-loading { color:var(--text2); font-size:.88rem }
+
+/* ===== APP ===== */
+#app { height:100vh; display:flex; flex-direction:column; opacity:1; visibility:visible !important; }
+.hidden { display:none!important }
+/* Force app visibility - override any cached hidden state */
+#app.hidden { display:flex!important; }
+#app[style*="display: none"] { display:flex!important; }
+
+/* =========================================================
+ TOPBAR — WhatsApp Dark
+ ========================================================= */
+.topbar {
+ position:sticky; top:0; z-index:100;
+ background:#111b21;
+ border-bottom:1px solid #222d34;
+ display:flex; align-items:center; justify-content:space-between;
+ padding:.55rem 1rem; gap:.5rem;
+ box-shadow:0 1px 4px rgba(0,0,0,.4);
+}
+.topbar-right { display:flex; align-items:center; gap:.65rem }
+.logo-sm { font-size:1.2rem; font-weight:900; color:#00a884; letter-spacing:-.5px }
+.logo-sm span { color:#e9edef }
+.live-badge {
+ background:rgba(231,76,60,.1); border:1px solid rgba(231,76,60,.3);
+ color:#e74c3c; padding:.18rem .6rem; border-radius:20px;
+ font-size:.7rem; display:flex; align-items:center; gap:.35rem;
+ animation:badgePulse 3s ease-in-out infinite;
+}
+@keyframes badgePulse{0%,100%{border-color:rgba(231,76,60,.3)}50%{border-color:rgba(231,76,60,.65)}}
+.live-dot{width:6px;height:6px;background:#e74c3c;border-radius:50%;animation:blink 1s infinite}
+@keyframes blink{0%,100%{opacity:1}50%{opacity:.15}}
+.topbar-stats{display:flex;gap:.3rem;flex:1;justify-content:center}
+.stat-chip{
+ background:#1f2c34;border:1px solid #222d34;
+ padding:.18rem .55rem;border-radius:20px;font-size:.7rem;color:#8696a0;
+}
+.usd-chip{color:#f1c40f!important;border-color:rgba(241,196,15,.22)!important;background:rgba(241,196,15,.06)!important}
+.menu-btn{
+ background:transparent;border:none;color:#8696a0;
+ padding:.4rem .7rem;border-radius:8px;cursor:pointer;
+ display:flex;align-items:center;justify-content:center;
+ transition:all .2s;
+}
+.menu-btn:hover{background:#1f2c34;color:#e9edef}
+
+/* =========================================================
+ SIDE MENU — WhatsApp Style
+ ========================================================= */
+.side-menu{
+ position:fixed;right:0;top:0;height:100vh;width:300px;z-index:200;
+ background:#111b21;
+ border-left:1px solid #222d34;
+ display:flex;flex-direction:column;
+ transform:translateX(100%);
+ transition:transform .28s cubic-bezier(.4,0,.2,1);
+ box-shadow:-8px 0 32px rgba(0,0,0,.55);
+ overflow:hidden;
+}
+.side-menu:not(.hidden){transform:translateX(0)}
+@keyframes slideIn{from{transform:translateX(100%)}to{transform:translateX(0)}}
+
+.menu-header{
+ display:flex;justify-content:space-between;align-items:center;
+ padding:1.1rem 1.2rem .9rem;
+ background:linear-gradient(135deg,#0d2b22 0%,#0c1e26 100%);
+ border-bottom:1px solid #222d34;
+ position:relative;overflow:hidden;flex-shrink:0;
+}
+.menu-header::before{
+ content:'';position:absolute;top:-20px;left:-20px;
+ width:120px;height:120px;border-radius:50%;
+ background:radial-gradient(circle,rgba(0,168,132,.18) 0%,transparent 70%);
+ pointer-events:none;
+}
+.menu-logo{font-size:1.5rem;font-weight:900;color:#00a884;letter-spacing:-.5px}
+.close-btn{
+ background:#1f2c34;border:none;color:#8696a0;
+ width:32px;height:32px;border-radius:50%;
+ cursor:pointer;font-size:.9rem;
+ display:flex;align-items:center;justify-content:center;
+ transition:all .2s;
+}
+.close-btn:hover{background:#2a3942;color:#e9edef}
+
+/* Profile badge */
+.menu-profile-badge{
+ display:flex!important;align-items:center;gap:.85rem;
+ padding:1rem 1.2rem;
+ background:#0d1418;
+ border-bottom:1px solid #222d34;
+ cursor:pointer;transition:background .2s;flex-shrink:0;
+}
+.menu-profile-badge:hover{background:#1f2c34}
+.mpb-avatar{
+ width:46px;height:46px;border-radius:50%;
+ background:#2a3942;display:flex;align-items:center;justify-content:center;
+ font-size:1.4rem;flex-shrink:0;
+ border:2px solid #00a884;overflow:hidden;
+}
+.mpb-avatar img{width:100%;height:100%;object-fit:cover;border-radius:50%}
+.mpb-info{flex:1;min-width:0}
+.mpb-name{
+ font-size:.95rem;font-weight:700;color:#e9edef;
+ white-space:nowrap;overflow:hidden;text-overflow:ellipsis;
+}
+.mpb-sub{font-size:.75rem;color:#00a884;margin-top:.1rem}
+
+/* Location */
+.menu-location{
+ display:flex!important;
+ padding:.6rem 1.2rem;background:#0d1418;
+ border-bottom:1px solid #1a2429;
+ align-items:center;gap:.55rem;font-size:.78rem;color:#8696a0;
+ flex-shrink:0;
+}
+.mloc-icon{font-size:.9rem;color:#00a884}
+
+/* Nav section labels */
+.menu-section-label{
+ padding:.6rem 1.2rem .3rem;
+ font-size:.68rem;font-weight:700;color:#00a884;
+ text-transform:uppercase;letter-spacing:.08em;opacity:.85;
+}
+.menu-item-divider{height:1px;background:#1a2429;margin:.3rem 0}
+
+/* Nav */
+.side-menu nav{
+ flex:1;padding:.4rem 0;display:flex;flex-direction:column;
+ gap:0;overflow-y:auto;
+ scrollbar-width:thin;scrollbar-color:#2a3942 transparent;
+}
+.side-menu nav::-webkit-scrollbar{width:3px}
+.side-menu nav::-webkit-scrollbar-thumb{background:#2a3942;border-radius:3px}
+
+.menu-item{
+ padding:.78rem 1.2rem;cursor:pointer;
+ color:#d1d7db;text-decoration:none;
+ transition:background .18s ease, transform .12s ease, border-color .18s ease;
+ font-size:.88rem;display:flex;align-items:center;gap:.85rem;
+ border-radius:8px;border:none;background:transparent;
+ margin:1px 6px;
+ border-right:3px solid transparent;
+ -webkit-tap-highlight-color:rgba(0,168,132,.15);
+ position:relative;
+}
+.menu-item:hover{background:rgba(0,168,132,.08);border-right-color:rgba(0,168,132,.4)}
+.menu-item:active{background:#2a3942;transform:scale(.97)}
+.mi-icon{
+ width:36px;height:36px;border-radius:50%;flex-shrink:0;
+ background:#1f2c34;display:flex;align-items:center;justify-content:center;
+ font-size:1rem;transition:background .2s, transform .15s;
+}
+.menu-item:hover .mi-icon{background:rgba(0,168,132,.15);transform:scale(1.1)}
+.menu-item:active .mi-icon{transform:scale(.92)}
+.mi-label{flex:1}
+.mi-badge{
+ background:#00a884;color:#111b21;font-size:.68rem;font-weight:700;
+ padding:.1rem .45rem;border-radius:10px;min-width:18px;text-align:center;
+}
+.msg-badge{
+ background:#00a884;color:#111b21;font-size:.68rem;font-weight:700;
+ padding:.1rem .4rem;border-radius:10px;min-width:16px;text-align:center;
+}
+.market-menu{color:#f1c40f!important}
+.market-menu .mi-icon{background:rgba(241,196,15,.12)!important}
+.report-menu{color:#e74c3c!important}
+.report-menu .mi-icon{background:rgba(231,76,60,.1)!important}
+.msg-menu .mi-icon{background:rgba(0,168,132,.1)!important}
+.blood-menu .mi-icon{background:rgba(231,76,60,.1)!important}
+.power-menu .mi-icon{background:rgba(241,196,15,.1)!important}
+.prayer-menu .mi-icon{background:rgba(52,152,219,.1)!important}
+.news-menu .mi-icon{background:rgba(155,89,182,.1)!important}
+
+.menu-footer{
+ padding:.9rem 1.2rem;
+ border-top:1px solid #222d34;background:#0d1418;
+ text-align:center;color:#8696a0;font-size:.75rem;flex-shrink:0;
+}
+.menu-footer p{margin:.15rem 0}
+.menu-overlay{
+ position:fixed;inset:0;background:rgba(0,0,0,.6);z-index:190;
+ backdrop-filter:blur(2px);
+}
+
+/* ── Compatibility: old HTML classes mapped to WA style ── */
+.menu-wa-banner{
+ background:linear-gradient(160deg,#0d2b22 0%,#025c4c 60%,#008069 100%);
+ padding:2.6rem 1.3rem 1rem; position:relative; overflow:hidden;
+}
+.menu-wa-banner::before{
+ content:''; position:absolute; inset:0;
+ background:radial-gradient(circle at 20% 80%,rgba(255,255,255,.05) 0%,transparent 60%);
+ pointer-events:none;
+}
+.menu-wa-close{
+ position:absolute; top:.75rem; left:.75rem;
+ background:rgba(0,0,0,.25); border:none; color:#fff;
+ width:32px; height:32px; border-radius:50%;
+ cursor:pointer; font-size:.9rem;
+ display:flex; align-items:center; justify-content:center;
+ transition:background .2s; z-index:2;
+}
+.menu-wa-close:hover{ background:rgba(0,0,0,.45) }
+.menu-wa-avatar{
+ width:64px; height:64px; border-radius:50%;
+ background:rgba(255,255,255,.15);
+ border:2.5px solid rgba(255,255,255,.55);
+ display:flex; align-items:center; justify-content:center;
+ font-size:2rem; margin-bottom:.65rem; cursor:pointer;
+ transition:transform .2s; overflow:hidden; position:relative; z-index:1;
+}
+.menu-wa-avatar:hover{ transform:scale(1.05) }
+.menu-wa-avatar img{ width:100%; height:100%; object-fit:cover; border-radius:50% }
+.menu-wa-name{
+ font-size:1.05rem; font-weight:700; color:#fff;
+ margin-bottom:.18rem; position:relative; z-index:1;
+}
+.menu-wa-sub{
+ font-size:.76rem; color:rgba(255,255,255,.75);
+ display:flex; align-items:center; gap:.4rem;
+ position:relative; z-index:1;
+}
+.menu-wa-online-dot{
+ width:8px; height:8px; background:#4caf50; border-radius:50%;
+ box-shadow:0 0 5px rgba(76,175,80,.7);
+}
+.menu-wa-stats{
+ display:flex; gap:.5rem;
+ padding:.7rem 1.3rem; background:#202c33;
+ border-bottom:1px solid rgba(255,255,255,.06);
+}
+.menu-wa-stat{
+ flex:1; background:rgba(0,168,132,.1);
+ border:1px solid rgba(0,168,132,.2); border-radius:12px;
+ padding:.48rem; text-align:center; cursor:pointer; transition:background .2s;
+}
+.menu-wa-stat:hover{ background:rgba(0,168,132,.2) }
+.menu-wa-stat-num{ font-size:.98rem; font-weight:800; color:#00a884; line-height:1.2 }
+.menu-wa-stat-lbl{ font-size:.63rem; color:#8696a0; margin-top:.1rem }
+
+/* menu-item with inner body */
+.menu-item-ico{
+ width:38px; height:38px; border-radius:50%; flex-shrink:0;
+ background:#1f2c34; display:flex; align-items:center; justify-content:center;
+ font-size:1.05rem; transition:background .2s;
+}
+.menu-item:hover .menu-item-ico{ background:rgba(0,168,132,.12); transform:scale(1.06) }
+.menu-item-body{ flex:1; min-width:0 }
+.menu-item-title{
+ font-size:.88rem; font-weight:500; color:#e9edef;
+ white-space:nowrap; overflow:hidden; text-overflow:ellipsis;
+}
+.menu-item-sub{ font-size:.7rem; color:#8696a0; margin-top:.08rem }
+.menu-item-badge{
+ background:#00a884; color:#111b21; font-size:.65rem; font-weight:700;
+ padding:.1rem .42rem; border-radius:10px; min-width:16px; text-align:center;
+}
+
+/* Special menu items */
+.menu-item-sos .menu-item-ico { background:rgba(231,76,60,.2) }
+.menu-item-sos .menu-item-title{ color:#e74c3c }
+.menu-item-chat .menu-item-ico { background:rgba(0,168,132,.15) }
+.menu-item-chat .menu-item-title{ color:#00a884 }
+.menu-item-report .menu-item-ico{ background:rgba(231,76,60,.12) }
+.menu-item-report .menu-item-title{ color:#e74c3c }
+.menu-item-market .menu-item-ico{ background:rgba(241,196,15,.12) }
+.menu-item-market .menu-item-title{ color:#f1c40f }
+.menu-item-active .menu-item-ico{ background:rgba(0,168,132,.25) !important; box-shadow:0 0 0 2px rgba(0,168,132,.2) }
+.menu-item-active .menu-item-title{ color:#00a884; font-weight:700 }
+.menu-item-active { background:rgba(0,168,132,.1) !important; border-right-color:#00a884 !important; }
+.menu-divider{ height:1px; background:rgba(255,255,255,.05); margin:.3rem 0 }
+
+/* menu footer with brand + version */
+.menu-footer-brand p{ font-size:.73rem; color:#8696a0; line-height:1.65; margin:0 }
+.menu-footer-brand p:first-child{ font-weight:600; color:#00a884 }
+.menu-footer-version{
+ font-size:.63rem; background:rgba(0,168,132,.12); color:#00a884;
+ padding:.18rem .55rem; border-radius:8px;
+ border:1px solid rgba(0,168,132,.22); white-space:nowrap;
+}
+
+/* =========================================================
+ MAIN / SECTIONS
+ ========================================================= */
+main#mainContent{flex:1;overflow-y:auto;overflow-x:hidden;padding-bottom:72px;scroll-behavior:smooth}
+.section{display:none;animation:fadeUp .3s cubic-bezier(.4,0,.2,1)}
+.section.active-sec{display:block}
+@keyframes fadeUp{from{opacity:0;transform:translateY(12px)}to{opacity:1;transform:translateY(0)}}
+.section-pad{padding:1rem}
+.sec-title{font-size:1rem;font-weight:700;margin-bottom:.85rem;color:var(--text3);display:flex;align-items:center;gap:.5rem}
+.sec-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:.85rem}
+.see-all-btn{background:none;border:none;color:var(--accent);cursor:pointer;font-family:inherit;font-size:.88rem}
+.sec-top{
+ padding:1.3rem 1.2rem;
+ background:linear-gradient(160deg,rgba(231,76,60,.1) 0%,rgba(26,188,156,.04) 100%);
+ border-bottom:1px solid var(--border);
+}
+/* ===== HERO ===== */
+.hero {
+ position:relative; overflow:hidden; padding:2.5rem 1.3rem 2rem;
+ min-height:240px; display:flex; align-items:center;
+}
+.hero-bg {
+ position:absolute; inset:0;
+ background:radial-gradient(ellipse at 60% -10%, rgba(231,76,60,.28) 0%, rgba(26,188,156,.08) 50%, transparent 70%);
+}
+.hero-particles {
+ position:absolute; inset:0; overflow:hidden; pointer-events:none;
+}
+.hero-particle {
+ position:absolute; width:2px; height:2px; border-radius:50%;
+ background:rgba(231,76,60,.5); animation:floatParticle linear infinite;
+}
+@keyframes floatParticle {
+ 0% { transform:translateY(100%) translateX(0); opacity:0 }
+ 10% { opacity:1 }
+ 90% { opacity:1 }
+ 100% { transform:translateY(-100px) translateX(30px); opacity:0 }
+}
+.hero-content { position:relative; z-index:1; width:100% }
+.hero-badge {
+ display:inline-flex; align-items:center; gap:.4rem;
+ background:rgba(231,76,60,.12); border:1px solid rgba(231,76,60,.3);
+ color:var(--accent); padding:.3rem .9rem; border-radius:20px;
+ font-size:.78rem; margin-bottom:.9rem; animation:badgePulse 4s infinite;
+}
+.hero h1 { font-size:2.2rem; font-weight:900; line-height:1.2; margin-bottom:.5rem }
+.hero-highlight { color:var(--accent); filter:drop-shadow(0 0 12px var(--glow)) }
+.hero p { color:var(--text2); font-size:.92rem; margin-bottom:1.3rem }
+.hero-stats { display:grid; grid-template-columns:repeat(4,1fr); gap:.5rem }
+.hero-stat {
+ background:linear-gradient(135deg,rgba(255,255,255,.06),rgba(255,255,255,.02));
+ border:1px solid rgba(255,255,255,.1);
+ border-radius:var(--rs); padding:.75rem .3rem; text-align:center;
+ transition:all .3s; position:relative; overflow:hidden;
+}
+.hero-stat::before {
+ content:''; position:absolute; inset:0;
+ background:linear-gradient(135deg,transparent 60%,rgba(26,188,156,.08));
+ pointer-events:none;
+}
+.hero-stat:hover { background:rgba(255,255,255,.09); border-color:var(--teal); transform:translateY(-2px) }
+.hero-stat span { display:block; font-size:1.35rem; font-weight:900; color:var(--teal); line-height:1 }
+.hero-stat small { font-size:.63rem; color:var(--text2); margin-top:.25rem; display:block }
+
+/* ===== LOCATION BAR ===== */
+.location-bar {
+ background:rgba(52,152,219,.08); border-top:1px solid rgba(52,152,219,.15);
+ border-bottom:1px solid rgba(52,152,219,.15);
+ padding:.65rem 1rem; display:flex; align-items:center; gap:.6rem;
+ font-size:.82rem; color:var(--text2);
+}
+.lb-icon { font-size:1rem }
+#locationBarText { flex:1 }
+.lb-btn {
+ background:rgba(52,152,219,.15); border:1px solid rgba(52,152,219,.3);
+ color:#74b9ff; padding:.22rem .75rem; border-radius:6px;
+ cursor:pointer; font-family:inherit; font-size:.75rem; transition:all .2s;
+}
+.lb-btn:hover { background:rgba(52,152,219,.25) }
+
+/* ===== TICKER ===== */
+.ticker-wrap {
+ background:rgba(231,76,60,.07); border-top:1px solid rgba(231,76,60,.12);
+ border-bottom:1px solid rgba(231,76,60,.12);
+ padding:.55rem 1rem; display:flex; align-items:center; gap:.8rem; overflow:hidden;
+}
+.ticker-label { color:var(--accent); font-size:1rem; flex-shrink:0 }
+.ticker-track { flex:1; overflow:hidden; position:relative }
+.ticker-content {
+ color:var(--text2); font-size:.8rem; white-space:nowrap;
+ display:inline-block; animation:ticker 35s linear infinite;
+}
+@keyframes ticker { 0%{transform:translateX(0)} 100%{transform:translateX(-50%)} }
+
+/* ===== QUICK GRID ===== */
+.quick-grid { display:grid; grid-template-columns:repeat(3,1fr); gap:.75rem }
+.quick-card {
+ display:flex; flex-direction:column; align-items:center; gap:.4rem;
+ padding:1rem .75rem; border-radius:var(--r); border:1px solid var(--border);
+ cursor:pointer; transition:all .25s cubic-bezier(.4,0,.2,1); background:var(--dark3);
+ position:relative; overflow:hidden;
+ -webkit-tap-highlight-color:rgba(255,255,255,.05);
+ user-select:none;
+}
+.quick-card:active { transform:scale(.95) !important; opacity:.85; }
+.quick-card::before {
+ content:''; position:absolute; inset:0;
+ background:inherit; filter:brightness(1.3); opacity:0;
+ transition:opacity .3s;
+}
+.quick-card:hover::before { opacity:.12 }
+.quick-card:hover { transform:translateY(-3px); box-shadow:var(--sh) }
+.q-icon { font-size:1.8rem; filter:drop-shadow(0 4px 8px rgba(0,0,0,.3)) }
+.q-title { font-size:.82rem; font-weight:700; color:var(--text) }
+.q-desc { font-size:.68rem; color:var(--text2) }
+.q-red { border-color:rgba(231,76,60,.35); background:rgba(231,76,60,.07); }
+.q-red:hover { border-color:rgba(231,76,60,.6); box-shadow:0 8px 24px rgba(231,76,60,.2); transform:translateY(-3px) scale(1.02); }
+.q-green { border-color:rgba(46,204,113,.35); background:rgba(46,204,113,.07); }
+.q-green:hover { border-color:rgba(46,204,113,.6); box-shadow:0 8px 24px rgba(46,204,113,.2); transform:translateY(-3px) scale(1.02); }
+.q-blue { border-color:rgba(52,152,219,.35); background:rgba(52,152,219,.07); }
+.q-blue:hover { border-color:rgba(52,152,219,.6); box-shadow:0 8px 24px rgba(52,152,219,.2); transform:translateY(-3px) scale(1.02); }
+.q-orange { border-color:rgba(230,126,34,.35); background:rgba(230,126,34,.07); }
+.q-orange:hover { border-color:rgba(230,126,34,.6); box-shadow:0 8px 24px rgba(230,126,34,.2); transform:translateY(-3px) scale(1.02); }
+.q-yellow { border-color:rgba(241,196,15,.35); background:rgba(241,196,15,.07); }
+.q-yellow:hover { border-color:rgba(241,196,15,.6); box-shadow:0 8px 24px rgba(241,196,15,.2); transform:translateY(-3px) scale(1.02); }
+.q-danger { border-color:rgba(231,76,60,.45); background:rgba(231,76,60,.10); }
+.q-danger:hover { border-color:rgba(231,76,60,.75); box-shadow:0 8px 24px rgba(231,76,60,.3); transform:translateY(-3px) scale(1.02); }
+.q-teal { border-color:rgba(26,188,156,.35); background:rgba(26,188,156,.07); }
+.q-teal:hover { border-color:rgba(26,188,156,.7); box-shadow:0 8px 24px rgba(26,188,156,.25); transform:translateY(-3px) scale(1.02); }
+
+/* ===== ALERT CARDS ===== */
+.alerts-list { display:flex; flex-direction:column; gap:.6rem }
+.alert-item {
+ display:flex; gap:.9rem; padding:.9rem;
+ background:var(--dark3); border:1px solid var(--border);
+ border-radius:var(--r); transition:all .25s;
+ border-right:3px solid var(--border2);
+ animation:slideInAlert .4s ease;
+}
+@keyframes slideInAlert { from{opacity:0;transform:translateX(-10px)} to{opacity:1;transform:translateX(0)} }
+.alert-item:hover { background:var(--dark4); border-color:var(--border2) }
+.type-danger { border-right-color:#e74c3c !important; background:rgba(231,76,60,.06) !important }
+.type-fire { border-right-color:#ff6b35 !important; background:rgba(255,107,53,.06) !important }
+.type-flood { border-right-color:#3498db !important; background:rgba(52,152,219,.06) !important }
+.type-medical { border-right-color:#e91e63 !important; background:rgba(233,30,99,.06) !important }
+.type-power { border-right-color:#f39c12 !important; background:rgba(243,156,18,.06) !important }
+.type-water { border-right-color:#00bcd4 !important; background:rgba(0,188,212,.06) !important }
+.type-market { border-right-color:#9b59b6 !important; background:rgba(155,89,182,.06) !important }
+.type-warning { border-right-color:#f1c40f !important; background:rgba(241,196,15,.06) !important }
+.type-info { border-right-color:#1abc9c !important; background:rgba(26,188,156,.06) !important }
+.alert-icon { font-size:1.5rem; flex-shrink:0; margin-top:.1rem }
+.alert-body { flex:1; min-width:0 }
+.alert-msg { font-size:.9rem; font-weight:600; margin-bottom:.4rem; line-height:1.4 }
+.alert-meta { display:flex; flex-wrap:wrap; gap:.4rem; align-items:center; font-size:.72rem; color:var(--text2) }
+.alert-area { color:var(--blue) }
+.alert-dist { color:var(--teal); font-weight:600 }
+.vote-btn {
+ background:rgba(26,188,156,.1); border:1px solid rgba(26,188,156,.2);
+ color:var(--teal); padding:.18rem .55rem; border-radius:20px;
+ cursor:pointer; font-family:inherit; font-size:.72rem; transition:all .2s;
+}
+.vote-btn:hover { background:rgba(26,188,156,.2); transform:scale(1.05) }
+
+/* ===== EXCHANGE ===== */
+.rate-card-home {
+ background:linear-gradient(135deg, var(--dark3), var(--dark4));
+ border:1px solid rgba(241,196,15,.2); border-radius:var(--r);
+ padding:1.2rem; text-align:center;
+ box-shadow:0 4px 20px rgba(241,196,15,.1);
+}
+.rate-big { font-size:2.5rem; font-weight:900; color:var(--yellow) }
+.rate-big small { font-size:.8rem; color:var(--text2); font-weight:400 }
+.rate-change { font-size:.82rem; color:var(--text2); margin-top:.3rem }
+
+.rate-hero {
+ background:linear-gradient(135deg, rgba(241,196,15,.12), rgba(230,126,34,.06));
+ border-top:1px solid rgba(241,196,15,.15); border-bottom:1px solid rgba(241,196,15,.15);
+ padding:1.5rem 1.2rem;
+}
+.rate-hero-inner { text-align:center; margin-bottom:1rem }
+.rate-label { font-size:.82rem; color:var(--text2); margin-bottom:.4rem }
+.rate-hero-num { font-size:3.5rem; font-weight:900; color:var(--yellow); line-height:1; filter:drop-shadow(0 0 20px var(--glow-yellow)) }
+.rate-unit { font-size:.85rem; color:var(--text2); margin-top:.3rem }
+.rate-updated { font-size:.78rem; color:var(--text2); margin-top:.5rem }
+.rate-mini-stats { display:grid; grid-template-columns:repeat(3,1fr); gap:.5rem }
+.rms { background:var(--dark3); border:1px solid var(--border); border-radius:var(--rs); padding:.6rem; text-align:center }
+.rms span { display:block; font-size:1.15rem; font-weight:800; color:var(--yellow) }
+.rms small { font-size:.68rem; color:var(--text2) }
+.rates-list { display:flex; flex-direction:column; gap:.5rem }
+.rate-item {
+ display:flex; justify-content:space-between; align-items:center;
+ padding:.85rem 1rem; background:var(--dark3); border:1px solid var(--border);
+ border-radius:var(--rs); transition:all .2s;
+}
+.rate-item:hover { background:var(--dark4) }
+.rate-item-num { font-size:1.1rem; font-weight:800; color:var(--yellow) }
+.rate-item-info { font-size:.73rem; color:var(--text2); margin-top:.2rem }
+.verified-badge {
+ background:rgba(46,204,113,.12); border:1px solid rgba(46,204,113,.25);
+ color:var(--green); padding:.18rem .55rem; border-radius:20px; font-size:.72rem;
+}
+
+/* ===== MAP ===== */
+.map-topbar {
+ display:flex; gap:.6rem; padding:.6rem .8rem;
+ background:var(--dark2); border-bottom:1px solid var(--border);
+ position:sticky; top:0; z-index:50;
+}
+.map-search-wrap { flex:1; position:relative }
+.map-search-inp {
+ width:100%; background:var(--dark3); border:1px solid var(--border);
+ color:var(--text); padding:.6rem .9rem; border-radius:var(--rs);
+ font-family:inherit; font-size:.88rem; outline:none;
+ transition:all .2s;
+}
+.map-search-inp:focus { border-color:var(--teal); box-shadow:0 0 0 3px rgba(26,188,156,.1) }
+.map-search-inp::placeholder { color:var(--text2) }
+.map-search-results {
+ position:absolute; top:calc(100% + 4px); right:0; left:0;
+ background:var(--dark2); border:1px solid var(--border);
+ border-radius:var(--rs); z-index:200; max-height:260px; overflow-y:auto;
+ box-shadow:var(--sh);
+}
+.map-locate-btn {
+ background:var(--dark3); border:1px solid var(--border);
+ color:var(--text); padding:.5rem .85rem; border-radius:var(--rs);
+ cursor:pointer; font-size:1.15rem; transition:all .2s; flex-shrink:0;
+}
+.map-locate-btn:hover { background:rgba(26,188,156,.15); border-color:var(--teal) }
+.map-filters {
+ display:flex; gap:.4rem; padding:.55rem .8rem; overflow-x:auto;
+ background:var(--dark2); border-bottom:1px solid var(--border);
+ scrollbar-width:none;
+}
+.map-filters::-webkit-scrollbar { display:none }
+#map { height:52vh; min-height:280px; background:var(--dark3) }
+.map-stats-bar {
+ display:flex; gap:.4rem; padding:.6rem .8rem; align-items:center;
+ background:var(--dark2); border-top:1px solid var(--border); overflow-x:auto;
+}
+.msb-item { display:flex; align-items:center; gap:.3rem; font-size:.78rem; white-space:nowrap }
+.msb-item span { font-size:1rem; font-weight:800 }
+.msb-item small { color:var(--text2) }
+.msb-danger span { color:var(--red) }
+.msb-warning span { color:var(--yellow) }
+.msb-info span { color:var(--green) }
+.msb-total span { color:var(--teal) }
+.msb-report-btn {
+ margin-right:auto; background:rgba(231,76,60,.15); border:1px solid rgba(231,76,60,.35);
+ color:var(--red); padding:.28rem .75rem; border-radius:20px; cursor:pointer;
+ font-family:inherit; font-size:.78rem; white-space:nowrap; transition:all .2s;
+}
+.msb-report-btn:hover { background:rgba(231,76,60,.25) }
+
+/* ===== FILTER BUTTONS ===== */
+.filt, .mfilt, .mcat {
+ background:var(--dark3); border:1px solid var(--border);
+ color:var(--text2); padding:.3rem .8rem; border-radius:20px;
+ cursor:pointer; font-family:inherit; font-size:.78rem; white-space:nowrap; transition:all .2s;
+}
+.filt:hover, .mfilt:hover, .mcat:hover { color:var(--text); border-color:var(--border2) }
+.active-filt { background:rgba(26,188,156,.15)!important; border-color:rgba(26,188,156,.4)!important; color:var(--teal)!important }
+.active-mfilt { background:rgba(241,196,15,.12)!important; border-color:rgba(241,196,15,.35)!important; color:var(--yellow)!important }
+.active-mcat { background:rgba(52,152,219,.12)!important; border-color:rgba(52,152,219,.35)!important; color:var(--blue)!important }
+.filt-nearby { color:var(--teal)!important; border-color:rgba(26,188,156,.25)!important }
+.sort-select {
+ background:var(--dark3); border:1px solid var(--border);
+ color:var(--text); padding:.28rem .6rem; border-radius:var(--rxs);
+ font-family:inherit; font-size:.78rem; outline:none; cursor:pointer;
+}
+
+/* ===== MARKERS ===== */
+.custom-marker { background:none; border:none }
+.marker-inner {
+ font-size:1.5rem; line-height:1;
+ filter:drop-shadow(0 2px 6px rgba(0,0,0,.6));
+ cursor:pointer; transition:transform .15s;
+}
+.marker-inner:hover { transform:scale(1.2) }
+.marker-user {
+ font-size:1.6rem;
+ filter:drop-shadow(0 0 12px var(--glow-teal)) drop-shadow(0 2px 6px rgba(0,0,0,.5));
+ animation:userPulse 2s ease-in-out infinite;
+}
+@keyframes userPulse { 0%,100%{filter:drop-shadow(0 0 8px var(--glow-teal))} 50%{filter:drop-shadow(0 0 18px rgba(26,188,156,.8))} }
+.leaflet-popup-content-wrapper { background:var(--dark2)!important; border:1px solid var(--border2)!important; border-radius:var(--rs)!important; box-shadow:var(--sh)!important; color:var(--text)!important }
+.leaflet-popup-tip { background:var(--dark2)!important }
+.popup-title { font-size:.9rem; font-weight:700; margin-bottom:.3rem; color:var(--text) }
+.popup-area { font-size:.78rem; color:var(--teal); margin-bottom:.2rem }
+.popup-votes { font-size:.75rem; color:var(--text2) }
+.popup-dist { font-size:.75rem; color:var(--teal); font-weight:600; margin-top:.2rem }
+
+/* ===== GEO PICKER ===== */
+.geo-picker-wrap { position:relative }
+.geo-dropdown {
+ position:absolute; top:calc(100% + 3px); right:0; left:0;
+ background:var(--dark2); border:1px solid var(--border);
+ border-radius:var(--rs); z-index:300; max-height:220px; overflow-y:auto;
+ box-shadow:var(--sh);
+}
+.geo-result-item {
+ padding:.65rem 1rem; cursor:pointer; font-size:.85rem;
+ color:var(--text2); border-bottom:1px solid var(--border);
+ transition:all .15s;
+}
+.geo-result-item:hover { background:var(--dark4); color:var(--text) }
+.geo-result-item:last-child { border-bottom:none }
+
+/* ===== FORMS ===== */
+.form-card {
+ background:var(--dark3); border:1px solid var(--border);
+ border-radius:var(--r); padding:1.1rem; display:flex; flex-direction:column; gap:.65rem;
+}
+.inp {
+ background:var(--dark4); border:1px solid var(--border);
+ color:var(--text); padding:.65rem .9rem; border-radius:var(--rs);
+ font-family:inherit; font-size:.9rem; outline:none; width:100%; transition:all .2s;
+}
+.inp:focus { border-color:var(--teal); box-shadow:0 0 0 3px rgba(26,188,156,.1) }
+.inp::placeholder { color:var(--text2) }
+.ta { resize:none; min-height:90px; line-height:1.5 }
+.radio-row { display:flex; gap:1.5rem }
+.radio-row label { display:flex; align-items:center; gap:.5rem; cursor:pointer; font-size:.88rem; color:var(--text2) }
+.radio-row input[type=radio] { accent-color:var(--teal) }
+.loc-attach-row {
+ display:flex; align-items:center; gap:.6rem; flex-wrap:wrap;
+ padding:.5rem .7rem; background:rgba(52,152,219,.06);
+ border:1px solid rgba(52,152,219,.15); border-radius:var(--rs);
+}
+.loc-status-text { font-size:.78rem; color:var(--text2); flex:1 }
+.loc-attach-btn {
+ background:rgba(26,188,156,.12); border:1px solid rgba(26,188,156,.25);
+ color:var(--teal); padding:.28rem .75rem; border-radius:20px;
+ cursor:pointer; font-family:inherit; font-size:.78rem; transition:all .2s; white-space:nowrap;
+}
+.loc-attach-btn:hover { background:rgba(26,188,156,.22); transform:scale(1.02) }
+.loc-dot { width:8px; height:8px; border-radius:50%; flex-shrink:0 }
+.loc-idle { background:var(--text2) }
+.loc-loading { background:var(--yellow); animation:blink .6s infinite }
+.loc-ok { background:var(--green) }
+.loc-err { background:var(--red) }
+.loc-status-display { display:flex; align-items:center; gap:.5rem; flex:1; font-size:.78rem; color:var(--text2) }
+.btn-submit {
+ background:linear-gradient(135deg, var(--teal), var(--teal2));
+ border:none; color:#fff; padding:.8rem; border-radius:var(--rs);
+ cursor:pointer; font-family:inherit; font-size:.95rem; font-weight:700;
+ transition:all .25s; box-shadow:0 4px 15px rgba(26,188,156,.3);
+}
+.btn-submit:hover { transform:translateY(-2px); box-shadow:0 8px 24px rgba(26,188,156,.4) }
+.btn-submit:active { transform:translateY(0) }
+.btn-report {
+ background:linear-gradient(135deg, var(--red), var(--red2))!important;
+ box-shadow:0 4px 15px rgba(231,76,60,.3)!important;
+}
+.btn-report:hover { box-shadow:0 8px 24px rgba(231,76,60,.4)!important }
+.btn-market {
+ background:linear-gradient(135deg, var(--yellow), var(--orange))!important;
+ box-shadow:0 4px 15px rgba(241,196,15,.25)!important; color:#0a0e1a!important;
+}
+.btn-market:hover { box-shadow:0 8px 24px rgba(241,196,15,.35)!important }
+
+/* ===== MEDICINE ===== */
+.search-box { margin-bottom:.75rem }
+.search-inp {
+ width:100%; background:var(--dark3); border:1px solid var(--border);
+ color:var(--text); padding:.7rem 1rem; border-radius:var(--rs);
+ font-family:inherit; font-size:.9rem; outline:none; transition:all .2s;
+}
+.search-inp:focus { border-color:var(--blue); box-shadow:0 0 0 3px rgba(52,152,219,.1) }
+.search-inp::placeholder { color:var(--text2) }
+.med-filter-row { display:flex; gap:.4rem; overflow-x:auto; padding-bottom:.3rem; scrollbar-width:none }
+.med-filter-row::-webkit-scrollbar { display:none }
+.med-list { display:flex; flex-direction:column; gap:.6rem }
+.med-item {
+ display:flex; justify-content:space-between; align-items:flex-start; gap:.8rem;
+ padding:.9rem 1rem; border-radius:var(--r); border:1px solid var(--border);
+ background:var(--dark3); transition:all .2s;
+ border-right:3px solid var(--border);
+}
+.med-available { border-right-color:var(--green) }
+.med-unavailable { border-right-color:var(--red); opacity:.8 }
+.med-item:hover { background:var(--dark4); transform:translateX(-2px) }
+.med-name { font-size:.9rem; font-weight:700; margin-bottom:.2rem }
+.med-name-en { font-size:.75rem; color:var(--text2); margin-bottom:.2rem }
+.med-info { font-size:.75rem; color:var(--text2); margin-bottom:.15rem }
+.med-dist { font-size:.73rem; color:var(--teal); font-weight:600 }
+.med-price { font-size:1rem; font-weight:800; color:var(--teal); text-align:center; margin-bottom:.3rem }
+.med-price.unavail { color:var(--text2) }
+.avail-badge { padding:.2rem .55rem; border-radius:20px; font-size:.72rem; white-space:nowrap }
+.avail-yes { background:rgba(46,204,113,.12); border:1px solid rgba(46,204,113,.25); color:var(--green) }
+.avail-no { background:rgba(231,76,60,.1); border:1px solid rgba(231,76,60,.2); color:var(--red) }
+
+/* ===== VOICE ===== */
+.voice-list { display:flex; flex-direction:column; gap:.65rem; margin-top:1rem }
+.voice-item {
+ background:var(--dark3); border:1px solid var(--border);
+ border-radius:var(--r); padding:.9rem 1rem; transition:all .2s;
+}
+.voice-item:hover { background:var(--dark4) }
+.voice-item-header { display:flex; justify-content:space-between; align-items:flex-start; gap:.5rem; margin-bottom:.4rem }
+.voice-title { font-size:.88rem; font-weight:700; flex:1 }
+.voice-cat {
+ background:var(--dark4); border:1px solid var(--border);
+ color:var(--text2); padding:.18rem .55rem; border-radius:20px; font-size:.7rem; white-space:nowrap;
+}
+.voice-desc { font-size:.82rem; color:var(--text2); margin-bottom:.5rem; line-height:1.4 }
+.voice-footer { display:flex; justify-content:space-between; align-items:center }
+.voice-meta { font-size:.72rem; color:var(--text2) }
+
+/* ===== SKILLS ===== */
+.skills-intro {
+ background:rgba(26,188,156,.07); border:1px solid rgba(26,188,156,.15);
+ border-radius:var(--rs); padding:.75rem 1rem; font-size:.85rem;
+ color:var(--text2); margin-bottom:1rem;
+}
+.skills-list { display:flex; flex-direction:column; gap:.65rem }
+.skill-item {
+ display:flex; gap:.9rem; padding:.9rem 1rem;
+ background:var(--dark3); border:1px solid var(--border);
+ border-radius:var(--r); transition:all .2s;
+}
+.skill-item:hover { background:var(--dark4); border-color:var(--border2) }
+.skill-avatar {
+ width:46px; height:46px; border-radius:50%; flex-shrink:0;
+ background:linear-gradient(135deg, var(--teal), var(--blue));
+ display:flex; align-items:center; justify-content:center;
+ font-size:1.1rem; font-weight:800; color:#fff;
+ box-shadow:0 4px 12px rgba(26,188,156,.3);
+}
+.skill-body { flex:1; min-width:0 }
+.skill-name { font-size:.9rem; font-weight:700; margin-bottom:.2rem }
+.skill-skill { font-size:.8rem; color:var(--teal); margin-bottom:.35rem }
+.skill-exchange { display:flex; gap:.5rem; flex-wrap:wrap; margin-bottom:.35rem }
+.skill-offer { background:rgba(46,204,113,.1); border:1px solid rgba(46,204,113,.2); color:var(--green); padding:.18rem .6rem; border-radius:20px; font-size:.72rem }
+.skill-want { background:rgba(52,152,219,.1); border:1px solid rgba(52,152,219,.2); color:var(--blue); padding:.18rem .6rem; border-radius:20px; font-size:.72rem }
+.skill-footer { display:flex; justify-content:space-between; font-size:.73rem; color:var(--text2) }
+.skill-rating { color:var(--yellow) }
+.skill-contact { font-size:.75rem; color:var(--text2); margin-top:.3rem }
+
+/* ===== MARKET P2P ===== */
+.market-top { background:linear-gradient(160deg, rgba(241,196,15,.1), transparent)!important }
+.market-filters { display:flex; gap:.4rem; padding:.55rem .8rem; overflow-x:auto; background:var(--dark2); border-bottom:1px solid var(--border); scrollbar-width:none }
+.market-filters::-webkit-scrollbar { display:none }
+.market-cats { display:flex; gap:.35rem; padding:.5rem .8rem; overflow-x:auto; background:var(--dark2); border-bottom:1px solid var(--border); scrollbar-width:none }
+.market-cats::-webkit-scrollbar { display:none }
+.market-list { display:grid; grid-template-columns:repeat(2,1fr); gap:.75rem }
+.market-card {
+ background:var(--dark3); border:1px solid var(--border);
+ border-radius:var(--r); padding:.9rem; cursor:pointer;
+ transition:all .25s; position:relative; overflow:hidden;
+}
+.market-card:hover { background:var(--dark4); border-color:var(--border2); transform:translateY(-2px); box-shadow:var(--sh2) }
+.mc-type { display:inline-block; padding:.2rem .6rem; border-radius:20px; font-size:.7rem; font-weight:700; margin-bottom:.5rem }
+.mc-sell { background:rgba(46,204,113,.12); border:1px solid rgba(46,204,113,.25); color:var(--green) }
+.mc-buy { background:rgba(52,152,219,.12); border:1px solid rgba(52,152,219,.25); color:var(--blue) }
+.mc-trade { background:rgba(230,126,34,.12); border:1px solid rgba(230,126,34,.25); color:var(--orange) }
+.mc-title { font-size:.85rem; font-weight:700; margin-bottom:.4rem; display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical; overflow:hidden }
+.mc-price { font-size:1rem; font-weight:900; color:var(--yellow); margin-bottom:.35rem }
+.mc-meta { font-size:.7rem; color:var(--text2); margin-bottom:.35rem }
+.mc-dist { font-size:.72rem; color:var(--teal); font-weight:600; margin-bottom:.35rem }
+.mc-footer { display:flex; justify-content:space-between; align-items:center }
+.mc-cat { background:var(--dark4); border:1px solid var(--border); color:var(--text2); padding:.15rem .5rem; border-radius:20px; font-size:.68rem }
+.mc-likes { cursor:pointer; font-size:.78rem; color:var(--text2); transition:all .2s }
+.mc-likes:hover { color:var(--pink); transform:scale(1.1) }
+.market-type-row { display:flex; gap:.5rem }
+.mtype {
+ flex:1; background:var(--dark4); border:1px solid var(--border);
+ color:var(--text2); padding:.55rem; border-radius:var(--rs);
+ cursor:pointer; font-family:inherit; font-size:.82rem; transition:all .2s;
+}
+.active-mtype { border-color:var(--yellow)!important; color:var(--yellow)!important; background:rgba(241,196,15,.1)!important }
+.price-row { display:flex; gap:.6rem }
+.market-mini-list { display:flex; flex-direction:column; gap:.5rem }
+.market-mini-card {
+ display:flex; justify-content:space-between; align-items:center; gap:.6rem;
+ padding:.75rem .9rem; background:var(--dark3); border:1px solid var(--border);
+ border-radius:var(--rs); cursor:pointer; transition:all .2s;
+}
+.market-mini-card:hover { background:var(--dark4) }
+.mmc-title { font-size:.85rem; font-weight:600; margin-bottom:.15rem }
+.mmc-meta { font-size:.72rem; color:var(--text2) }
+.mmc-price { font-size:.9rem; font-weight:800; color:var(--yellow); white-space:nowrap; flex-shrink:0 }
+
+/* ===== REPORT ===== */
+.report-top { background:linear-gradient(160deg, rgba(231,76,60,.12), transparent)!important }
+.report-types { display:flex; gap:.6rem; padding:0 1rem 1rem; flex-wrap:wrap }
+.rtype {
+ flex:1; background:var(--dark3); border:1px solid var(--border);
+ color:var(--text2); padding:.6rem .8rem; border-radius:var(--rs);
+ cursor:pointer; font-family:inherit; font-size:.88rem; font-weight:600; transition:all .2s;
+}
+.active-rtype[data-type=danger] { background:rgba(231,76,60,.15)!important; border-color:rgba(231,76,60,.5)!important; color:var(--red)!important }
+.active-rtype[data-type=warning] { background:rgba(241,196,15,.12)!important; border-color:rgba(241,196,15,.4)!important; color:var(--yellow)!important }
+.active-rtype[data-type=info] { background:rgba(46,204,113,.12)!important; border-color:rgba(46,204,113,.4)!important; color:var(--green)!important }
+.report-tips {
+ background:rgba(52,152,219,.06); border:1px solid rgba(52,152,219,.15);
+ border-radius:var(--r); padding:1rem; margin-top:1rem;
+}
+.report-tips h4 { font-size:.9rem; margin-bottom:.7rem; color:var(--blue) }
+.report-tips ul { padding-right:1rem }
+.report-tips li { font-size:.82rem; color:var(--text2); margin-bottom:.35rem; line-height:1.4 }
+
+/* ===== NEARBY USERS PANEL ===== */
+.nearby-users-panel {
+ background:rgba(26,188,156,.06); border:1px solid rgba(26,188,156,.15);
+ border-radius:var(--r); padding:.9rem; margin-bottom:1rem;
+}
+.nup-title { font-size:.88rem; font-weight:700; color:var(--teal); margin-bottom:.6rem; display:flex; align-items:center; gap:.5rem }
+.nup-list { display:flex; gap:.5rem; overflow-x:auto; scrollbar-width:none; padding-bottom:.3rem }
+.nup-list::-webkit-scrollbar { display:none }
+.nup-user {
+ display:flex; flex-direction:column; align-items:center; gap:.3rem;
+ padding:.5rem .75rem; background:var(--dark3); border:1px solid rgba(26,188,156,.15);
+ border-radius:var(--rs); cursor:pointer; transition:all .2s; flex-shrink:0;
+ min-width:80px; text-align:center;
+}
+.nup-user:hover { background:rgba(26,188,156,.1); border-color:var(--teal) }
+.nup-avatar { width:38px; height:38px; border-radius:50%; background:linear-gradient(135deg,var(--teal),var(--blue)); display:flex; align-items:center; justify-content:center; font-size:.9rem; font-weight:800; color:#fff }
+.nup-name { font-size:.73rem; color:var(--text); font-weight:600 }
+.nup-dist { font-size:.68rem; color:var(--teal) }
+
+/* ===== CHAT MODAL ===== */
+.chat-modal-overlay {
+ position:fixed; inset:0; background:rgba(0,0,0,.8); z-index:500;
+ display:flex; align-items:flex-end; justify-content:center;
+ backdrop-filter:blur(5px);
+}
+.chat-modal {
+ width:100%; max-width:480px; max-height:85vh;
+ background:var(--dark2); border-top:1px solid var(--border);
+ border-radius:20px 20px 0 0;
+ display:flex; flex-direction:column;
+ animation:slideUpModal .3s cubic-bezier(.4,0,.2,1);
+ box-shadow:0 -10px 40px rgba(0,0,0,.5);
+}
+@keyframes slideUpModal { from{transform:translateY(100%)} to{transform:translateY(0)} }
+.chat-header {
+ display:flex; justify-content:space-between; align-items:center;
+ padding:1rem 1.2rem; border-bottom:1px solid var(--border);
+ background:rgba(26,188,156,.06);
+}
+.chat-header-info { flex:1 }
+.chat-header-title { font-size:1rem; font-weight:700 }
+.chat-header-sub { font-size:.75rem; color:var(--teal); margin-top:.15rem }
+.chat-close-btn {
+ background:var(--dark4); border:none; color:var(--text2);
+ padding:.4rem .75rem; border-radius:8px; cursor:pointer; font-size:1rem;
+}
+.chat-messages {
+ flex:1; overflow-y:auto; padding:.9rem; display:flex; flex-direction:column; gap:.6rem;
+ min-height:200px;
+}
+.chat-msg {
+ max-width:80%; padding:.65rem .9rem;
+ border-radius:var(--r); font-size:.85rem; line-height:1.4;
+ animation:msgIn .2s ease;
+}
+@keyframes msgIn { from{opacity:0;transform:scale(.95)} to{opacity:1;transform:scale(1)} }
+.chat-msg-in { background:var(--dark3); border:1px solid var(--border); align-self:flex-end }
+.chat-msg-out { background:rgba(26,188,156,.12); border:1px solid rgba(26,188,156,.2); align-self:flex-start }
+.chat-msg-sender { font-size:.7rem; color:var(--teal); margin-bottom:.2rem }
+.chat-msg-time { font-size:.67rem; color:var(--text2); margin-top:.2rem; text-align:left }
+.chat-empty { display:flex; flex-direction:column; align-items:center; justify-content:center; padding:2rem; color:var(--text2); font-size:.85rem; gap:.5rem }
+.chat-input-row {
+ display:flex; gap:.6rem; padding:.8rem 1rem;
+ border-top:1px solid var(--border);
+ background:var(--dark2);
+}
+.chat-inp {
+ flex:1; background:var(--dark3); border:1px solid var(--border);
+ color:var(--text); padding:.6rem .9rem; border-radius:var(--rs);
+ font-family:inherit; font-size:.88rem; outline:none; transition:all .2s;
+}
+.chat-inp:focus { border-color:var(--teal) }
+.chat-inp::placeholder { color:var(--text2) }
+.chat-send-btn {
+ background:linear-gradient(135deg, var(--teal), var(--teal2));
+ border:none; color:#fff; padding:.6rem .9rem; border-radius:var(--rs);
+ cursor:pointer; font-size:1.1rem; transition:all .2s; flex-shrink:0;
+}
+.chat-send-btn:hover { transform:scale(1.05) }
+
+/* ===== MARKET MODAL ===== */
+.modal-overlay {
+ position:fixed; inset:0; background:rgba(0,0,0,.82); z-index:5000;
+ display:flex; align-items:center; justify-content:center; padding:1rem;
+ backdrop-filter:blur(4px);
+}
+.modal-overlay.hidden { display:none !important; }
+.modal-box {
+ background:var(--dark2); border:1px solid var(--border);
+ border-radius:20px; padding:1.5rem; width:100%; max-width:420px;
+ max-height:90vh; overflow-y:auto; position:relative;
+ box-shadow:0 20px 60px rgba(0,0,0,.6);
+ animation:popIn .3s cubic-bezier(.4,0,.2,1);
+}
+@keyframes popIn { from{opacity:0;transform:scale(.92)} to{opacity:1;transform:scale(1)} }
+.modal-close {
+ position:absolute; top:1rem; left:1rem;
+ background:var(--dark4); border:none; color:var(--text2);
+ width:32px; height:32px; border-radius:50%; cursor:pointer;
+ font-size:.95rem; display:flex; align-items:center; justify-content:center;
+ transition:all .2s;
+}
+.modal-close:hover { background:var(--dark5); color:var(--text) }
+.modal-type-badge { display:inline-block; padding:.25rem .75rem; border-radius:20px; font-size:.78rem; font-weight:700; margin-bottom:.8rem }
+.modal-title { font-size:1.3rem; font-weight:800; margin-bottom:.6rem }
+.modal-price { font-size:1.6rem; font-weight:900; color:var(--yellow); margin-bottom:.8rem }
+.modal-desc { font-size:.88rem; color:var(--text2); line-height:1.6; margin-bottom:1rem }
+.modal-info-grid { display:grid; grid-template-columns:1fr 1fr; gap:.6rem; margin-bottom:1.2rem }
+.modal-info-item { background:var(--dark3); border:1px solid var(--border); border-radius:var(--rs); padding:.6rem .8rem }
+.modal-info-item small { display:block; font-size:.7rem; color:var(--text2); margin-bottom:.2rem }
+.modal-info-item span { font-size:.85rem; font-weight:600 }
+.modal-contact-btn {
+ width:100%; background:linear-gradient(135deg, var(--teal), var(--teal2));
+ border:none; color:#fff; padding:.9rem; border-radius:var(--rs);
+ cursor:pointer; font-family:inherit; font-size:.95rem; font-weight:700;
+ transition:all .25s; box-shadow:0 4px 15px rgba(26,188,156,.3);
+}
+.modal-contact-btn:hover { transform:translateY(-2px); box-shadow:0 8px 24px rgba(26,188,156,.4) }
+
+/* ===== BOTTOM NAV — WhatsApp Style ===== */
+.bottom-nav {
+ position:fixed; bottom:0; left:0; right:0;
+ background:#111b21;
+ border-top:1px solid #222d34; z-index:100;
+ display:flex; align-items:center;
+ padding:.25rem 0 .45rem;
+ box-shadow:0 -2px 12px rgba(0,0,0,.4);
+}
+.bnav {
+ flex:1; display:flex; flex-direction:column; align-items:center; gap:.18rem;
+ background:none; border:none; color:#8696a0;
+ padding:.35rem 0; border-radius:0; cursor:pointer;
+ transition:color .18s; font-family:inherit; position:relative;
+}
+.bnav span:first-child { font-size:1.35rem; line-height:1 }
+.bnav small { font-size:.6rem; color:inherit }
+.bnav:hover, .active-bnav { color:#00a884!important }
+.bnav-center {
+ background:linear-gradient(135deg,#00a884,#25d366);
+ border-radius:50%; width:52px; height:52px;
+ margin-top:-14px; flex:none; margin-left:.5rem; margin-right:.5rem;
+ box-shadow:0 4px 18px rgba(0,168,132,.45);
+ color:#fff!important; padding:0;
+}
+.bnav-center span:first-child { font-size:1.45rem }
+.bnav-center small { display:none }
+.bnav-badge {
+ position:absolute; top:.1rem; right:calc(50% - 20px);
+ background:#00a884; color:#111b21; font-size:.58rem; font-weight:700;
+ padding:.08rem .35rem; border-radius:8px; min-width:16px; text-align:center;
+}
+
+/* ===== TOAST ===== */
+.toast {
+ position:fixed; top:72px; left:50%; transform:translateX(-50%);
+ background:var(--dark2); border:1px solid var(--border);
+ color:var(--text); padding:.7rem 1.3rem; border-radius:30px;
+ font-size:.88rem; z-index:600; max-width:85vw;
+ box-shadow:var(--sh); transition:all .3s;
+ animation:toastIn .3s cubic-bezier(.4,0,.2,1);
+}
+@keyframes toastIn { from{opacity:0;transform:translateX(-50%) translateY(-10px)} to{opacity:1;transform:translateX(-50%) translateY(0)} }
+.toast.success { background:rgba(46,204,113,.15); border-color:rgba(46,204,113,.35); color:var(--green) }
+.toast.error { background:rgba(231,76,60,.15); border-color:rgba(231,76,60,.35); color:var(--red) }
+.toast.hidden { display:none!important }
+
+/* ===== NOTIFICATION BADGE ===== */
+.notif-badge {
+ position:fixed; top:75px; right:.8rem;
+ background:var(--dark2); border:1px solid var(--border2);
+ color:var(--text); padding:.65rem 1rem; border-radius:var(--r);
+ font-size:.82rem; z-index:590; max-width:200px;
+ box-shadow:var(--sh); animation:slideInRight .3s ease;
+}
+@keyframes slideInRight { from{opacity:0;transform:translateX(20px)} to{opacity:1;transform:translateX(0)} }
+.notif-badge.hidden { display:none!important }
+
+/* ===== EMPTY STATE ===== */
+.empty-state {
+ text-align:center; padding:2.5rem 1.5rem;
+ background:var(--dark3); border:1px dashed var(--dark5);
+ border-radius:var(--r);
+}
+.empty-icon { font-size:2.8rem; margin-bottom:.7rem; opacity:.6 }
+.empty-title { font-size:1rem; font-weight:700; margin-bottom:.4rem; color:var(--text3) }
+.empty-sub { font-size:.83rem; color:var(--text2); margin-bottom:1.1rem; line-height:1.5 }
+.empty-btn {
+ background:rgba(26,188,156,.1); border:1px solid rgba(26,188,156,.25);
+ color:var(--teal); padding:.5rem 1.2rem; border-radius:20px;
+ cursor:pointer; font-family:inherit; font-size:.85rem; transition:all .2s;
+}
+.empty-btn:hover { background:rgba(26,188,156,.2) }
+
+/* ===== STATS ===== */
+.map-stats-row { display:flex; gap:.5rem; overflow-x:auto; padding-bottom:.2rem; scrollbar-width:none }
+.map-stats-row::-webkit-scrollbar { display:none }
+
+/* ===== HEATMAP LEGEND ===== */
+.heatmap-legend {
+ display:flex; gap:.5rem; padding:.5rem .8rem; align-items:center;
+ font-size:.75rem; color:var(--text2); flex-wrap:wrap;
+}
+.hleg-item { display:flex; align-items:center; gap:.3rem }
+.hleg-dot { width:10px; height:10px; border-radius:50% }
+
+/* ===== RESPONSIVE ===== */
+@media (max-width: 360px) {
+ .hero h1 { font-size:1.8rem }
+ .rate-hero-num { font-size:2.8rem }
+ .quick-grid { grid-template-columns:repeat(2,1fr) }
+ .market-list { grid-template-columns:1fr }
+ .hero-stats { grid-template-columns:repeat(2,1fr) }
+ .rate-mini-stats{ grid-template-columns:repeat(2,1fr) }
+}
+@media (min-width: 480px) {
+ .market-list { grid-template-columns:repeat(3,1fr) }
+ .quick-grid { grid-template-columns:repeat(3,1fr) }
+}
+
+/* ===== EXTRA ===== */
+button:disabled { opacity:.5; cursor:not-allowed }
+.spin { animation:spin 1s linear infinite }
+@keyframes spin { from{transform:rotate(0deg)} to{transform:rotate(360deg)} }
+.pulse { animation:pulse 2s ease-in-out infinite }
+@keyframes pulse { 0%,100%{opacity:1} 50%{opacity:.5} }
+
+/* ===== LEAFLET OVERRIDES ===== */
+.leaflet-control-zoom {
+ border:1px solid var(--border)!important;
+ border-radius:var(--rs)!important;
+ box-shadow:var(--sh2)!important;
+ overflow:hidden;
+}
+.leaflet-control-zoom a {
+ background:var(--dark3)!important; color:var(--text)!important;
+ border:none!important; transition:all .2s!important;
+}
+.leaflet-control-zoom a:hover { background:var(--dark4)!important }
+.leaflet-control-attribution { display:none!important }
+
+/* ===== SOS BUTTON ===== */
+.sos-btn {
+ width: 100%;
+ background: linear-gradient(135deg, #c0392b, #e74c3c);
+ color: #fff;
+ border: none;
+ border-radius: var(--r);
+ padding: 1rem 1.5rem;
+ font-family: 'Tajawal', sans-serif;
+ font-size: 1rem;
+ font-weight: 800;
+ cursor: pointer;
+ letter-spacing: .3px;
+ box-shadow: 0 4px 20px rgba(231,76,60,.4);
+ transition: all .2s;
+ position: relative;
+ overflow: hidden;
+ user-select: none;
+ -webkit-user-select: none;
+}
+.sos-btn::before {
+ content: '';
+ position: absolute;
+ inset: 0;
+ background: rgba(255,255,255,.08);
+ transform: scaleX(0);
+ transform-origin: right;
+ transition: transform 3s linear;
+}
+.sos-btn.sos-pressing::before {
+ transform: scaleX(1);
+ transform-origin: right;
+}
+.sos-btn:active, .sos-btn.sos-pressing {
+ transform: scale(.98);
+ box-shadow: 0 2px 10px rgba(231,76,60,.6);
+}
+.sos-btn:hover { background: linear-gradient(135deg, #e74c3c, #ff6b6b); }
+
+/* share-btn-sm / show-on-map-btn / chat-start-btn — defined below */
+
+/* spin — already defined above */
+
+/* ===== NAME MODAL ===== */
+#nameModal .modal-box {
+ max-width: 340px;
+ width: 90%;
+ margin: auto;
+}
+
+/* ===== HEATMAP point ===== */
+.heat-point { pointer-events: none; }
+
+/* ===== VERIFIED BADGE ===== */
+.verified-badge {
+ background: rgba(46,204,113,.12);
+ border: 1px solid rgba(46,204,113,.25);
+ color: var(--green);
+ border-radius: 6px;
+ padding: .15rem .45rem;
+ font-size: .72rem;
+ font-weight: 600;
+}
+
+/* ===== AUTO LOCATION BAR enhancement ===== */
+.location-bar {
+ background: var(--dark3);
+ border-bottom: 1px solid var(--border);
+ padding: .55rem 1rem;
+ display: flex;
+ align-items: center;
+ gap: .6rem;
+ font-size: .88rem;
+ color: var(--text2);
+ position: sticky;
+ top: 56px;
+ z-index: 50;
+}
+.lb-icon { font-size: 1rem; flex-shrink: 0; }
+#locationBarText {
+ flex: 1;
+ font-weight: 500;
+ color: var(--text);
+ font-size: .85rem;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+.lb-btn {
+ background: rgba(26,188,156,.1);
+ border: 1px solid rgba(26,188,156,.2);
+ color: var(--teal);
+ border-radius: 8px;
+ padding: .25rem .6rem;
+ cursor: pointer;
+ font-size: .78rem;
+ font-family: inherit;
+ font-weight: 600;
+ white-space: nowrap;
+ transition: all .2s;
+ flex-shrink: 0;
+}
+.lb-btn:hover { background: rgba(26,188,156,.2); }
+
+/* Auto-location indicator pulse */
+.loc-dot.loc-ok { animation: locPulse 2s ease-in-out infinite; }
+@keyframes locPulse {
+ 0%, 100% { color: var(--green); }
+ 50% { color: rgba(46,204,113,.4); }
+}
+
+/* ===== MODAL CONTACT BTN ===== */
+.modal-contact-btn {
+ flex: 1;
+ background: linear-gradient(135deg, var(--teal), var(--teal2));
+ color: #fff;
+ border: none;
+ border-radius: var(--r);
+ padding: .75rem 1rem;
+ font-family: inherit;
+ font-size: .95rem;
+ font-weight: 700;
+ cursor: pointer;
+ transition: all .2s;
+}
+.modal-contact-btn:hover { opacity: .9; transform: translateY(-1px); }
+
+/* ===== SOS FLOAT BUTTON ===== */
+.sos-float-btn {
+ position: fixed;
+ bottom: 5.5rem;
+ left: 1rem;
+ width: 52px;
+ height: 52px;
+ border-radius: 50%;
+ background: rgba(231,76,60,.15);
+ border: 2px solid rgba(231,76,60,.5);
+ font-size: 1.5rem;
+ cursor: pointer;
+ z-index: 500;
+ transition: all .2s;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ -webkit-tap-highlight-color: transparent;
+ user-select: none;
+}
+.sos-float-btn:active,
+.sos-float-btn.sos-pressing {
+ background: rgba(231,76,60,.55);
+ border-color: #e74c3c;
+ transform: scale(1.15);
+ animation: hb .3s ease-in-out infinite;
+}
+
+/* ===== SHARE BUTTON SMALL ===== */
+.share-btn-sm {
+ background: rgba(26,188,156,.08);
+ border: 1px solid rgba(26,188,156,.2);
+ color: var(--teal);
+ border-radius: 6px;
+ padding: .2rem .45rem;
+ cursor: pointer;
+ font-size: .78rem;
+ transition: background .15s;
+}
+.share-btn-sm:active { background: rgba(26,188,156,.2); }
+
+/* ===== SHOW ON MAP BUTTON ===== */
+.show-on-map-btn {
+ background: rgba(26,188,156,.08);
+ border: 1px solid rgba(26,188,156,.2);
+ color: var(--teal);
+ border-radius: 6px;
+ padding: .25rem .5rem;
+ cursor: pointer;
+ font-size: .8rem;
+ margin-top: .3rem;
+ display: block;
+ width: 100%;
+ transition: background .15s;
+}
+
+/* ===== CHAT START BUTTON (in skills) ===== */
+.chat-start-btn {
+ background: rgba(26,188,156,.08);
+ border: 1px solid rgba(26,188,156,.25);
+ color: var(--teal);
+ border-radius: 8px;
+ padding: .35rem .8rem;
+ cursor: pointer;
+ font-family: inherit;
+ font-size: .8rem;
+ margin-top: .5rem;
+ transition: background .15s;
+}
+.chat-start-btn:active { background: rgba(26,188,156,.2); }
+
+/* ===== HEATMAP TOGGLE ===== */
+.heatmap-btn {
+ background: rgba(241,196,15,.08);
+ border: 1px solid rgba(241,196,15,.25);
+ color: #f1c40f;
+ border-radius: 8px;
+ padding: .35rem .75rem;
+ cursor: pointer;
+ font-family: inherit;
+ font-size: .8rem;
+ transition: background .15s;
+}
+.heatmap-btn:active { background: rgba(241,196,15,.2); }
+
+
+/* ============================================================
+ NEW FEATURES CSS - Profile, People Search, Messaging v2
+ ============================================================ */
+
+/* --- Profile Hero Enhanced --- */
+.profile-avatar-wrap {
+ position: relative;
+ display: inline-block;
+}
+.avatar-change-hint {
+ position: absolute;
+ bottom: -4px;
+ left: -4px;
+ width: 22px;
+ height: 22px;
+ border-radius: 50%;
+ background: var(--teal);
+ border: 2px solid var(--bg);
+ font-size: .7rem;
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ opacity: 0;
+ transition: opacity .2s;
+}
+.profile-avatar-wrap:hover .avatar-change-hint { opacity: 1; }
+
+.profile-hero-job {
+ font-size: .82rem;
+ color: var(--teal);
+ font-weight: 600;
+ margin: .2rem 0;
+}
+
+.profile-badge.pbd {
+ background: rgba(39,174,96,.15);
+ color: #27ae60;
+ border-color: rgba(39,174,96,.3);
+}
+
+/* --- Public Phone Bar --- */
+.profile-public-phone-bar {
+ display: flex;
+ align-items: center;
+ gap: .8rem;
+ background: linear-gradient(135deg, rgba(39,174,96,.12), rgba(39,174,96,.06));
+ border: 1.5px solid rgba(39,174,96,.35);
+ border-radius: var(--r);
+ padding: .8rem 1rem;
+ margin: 0 1rem .8rem;
+}
+.ppb-icon { font-size: 1.5rem; }
+.ppb-content { flex: 1; min-width: 0; }
+.ppb-label { font-size: .72rem; color: var(--text2); }
+.ppb-number { font-size: 1.1rem; font-weight: 700; color: var(--text1); letter-spacing: .03em; }
+.ppb-call-btn {
+ background: var(--green);
+ color: #fff;
+ border: none;
+ padding: .45rem .9rem;
+ border-radius: 10px;
+ cursor: pointer;
+ font-family: inherit;
+ font-size: .85rem;
+ font-weight: 700;
+ white-space: nowrap;
+ transition: filter .2s;
+}
+.ppb-call-btn:hover { filter: brightness(1.1); }
+
+/* --- Profile Info Card Enhanced --- */
+.pic-section-title {
+ font-size: .75rem;
+ font-weight: 700;
+ color: var(--teal);
+ text-transform: uppercase;
+ letter-spacing: .05em;
+ padding: .5rem 0 .3rem;
+ border-bottom: 1px solid var(--border);
+ margin-bottom: .4rem;
+}
+.pic-row-phone {
+ background: rgba(39,174,96,.05);
+ border-radius: 8px;
+ padding: .4rem .5rem;
+ border: 1px solid rgba(39,174,96,.15);
+}
+.pic-label-public {
+ color: var(--green) !important;
+ font-weight: 700 !important;
+}
+.pic-val-highlighted {
+ color: var(--text1) !important;
+ font-weight: 700 !important;
+ font-size: .95rem !important;
+}
+.pic-call-btn {
+ background: none;
+ border: none;
+ cursor: pointer;
+ font-size: 1rem;
+ padding: .1rem;
+ margin-right: .3rem;
+}
+
+/* --- Profile Edit Form Enhanced --- */
+.pe-section-title {
+ font-size: .78rem;
+ font-weight: 700;
+ color: var(--text2);
+ padding: .7rem 0 .3rem;
+ border-bottom: 1px solid var(--border);
+ margin-bottom: .4rem;
+ text-transform: uppercase;
+ letter-spacing: .04em;
+}
+.pe-public-phone-wrap { position: relative; }
+.pe-inp-highlighted {
+ border-color: rgba(39,174,96,.5) !important;
+ background: rgba(39,174,96,.04) !important;
+ color: var(--text1) !important;
+}
+.pe-inp-highlighted:focus {
+ border-color: var(--green) !important;
+ box-shadow: 0 0 0 3px rgba(39,174,96,.15) !important;
+}
+.pe-public-note {
+ font-size: .72rem;
+ color: var(--green);
+ padding: .3rem .2rem 0;
+ line-height: 1.4;
+}
+
+/* --- Profile Actions Enhanced --- */
+.pa-btn.pa-map {
+ background: rgba(52,152,219,.1);
+ color: var(--blue);
+ border-color: rgba(52,152,219,.3);
+}
+.pa-btn.pa-map:hover { background: rgba(52,152,219,.2); }
+
+/* --- People Search Box Enhanced --- */
+.psb-icon {
+ font-size: 1.1rem;
+ padding: 0 .4rem;
+ flex-shrink: 0;
+}
+.psb-input-wrap {
+ display: flex;
+ align-items: center;
+ background: var(--card);
+ border: 1.5px solid var(--border);
+ border-radius: var(--r);
+ padding: .1rem .4rem .1rem .6rem;
+ gap: .4rem;
+ transition: border-color .2s;
+}
+.psb-input-wrap:focus-within {
+ border-color: var(--teal);
+ box-shadow: 0 0 0 3px rgba(26,188,156,.1);
+}
+.psb-inp {
+ flex: 1;
+ background: none;
+ border: none;
+ outline: none;
+ color: var(--text1);
+ font-family: Tajawal, sans-serif;
+ font-size: .95rem;
+ padding: .6rem .2rem;
+ min-width: 0;
+}
+.psb-stats {
+ display: flex;
+ align-items: center;
+ gap: .5rem;
+ padding: .3rem .2rem 0;
+ font-size: .75rem;
+ color: var(--text2);
+}
+.psb-sep { color: var(--border); }
+
+/* --- Search Loading Spinner --- */
+.search-loading {
+ display: flex;
+ align-items: center;
+ gap: .6rem;
+ padding: 1.2rem;
+ color: var(--text2);
+ font-size: .88rem;
+ justify-content: center;
+}
+.sl-spinner {
+ width: 18px; height: 18px;
+ border: 2px solid var(--border);
+ border-top-color: var(--teal);
+ border-radius: 50%;
+ animation: spin .7s linear infinite;
+ flex-shrink: 0;
+}
+
+/* --- Person Search Card Enhanced --- */
+.person-search-card {
+ background: var(--card);
+ border: 1px solid var(--border);
+ border-radius: var(--r);
+ padding: .9rem;
+ margin-bottom: .6rem;
+ cursor: pointer;
+ transition: all .2s;
+}
+.person-search-card:hover {
+ border-color: var(--teal);
+ background: rgba(26,188,156,.04);
+ transform: translateY(-1px);
+}
+.psc-job {
+ font-size: .78rem;
+ color: var(--teal);
+ margin: .15rem 0;
+ font-weight: 600;
+}
+
+/* الرقم المعلن في بطاقة البحث */
+.psc-public-phone {
+ display: flex;
+ align-items: center;
+ gap: .5rem;
+ background: rgba(39,174,96,.08);
+ border: 1px solid rgba(39,174,96,.2);
+ border-radius: 8px;
+ padding: .35rem .6rem;
+ margin: .4rem 0 .2rem;
+}
+.psc-pp-tag {
+ font-size: .68rem;
+ font-weight: 700;
+ color: var(--green);
+ background: rgba(39,174,96,.15);
+ padding: .1rem .3rem;
+ border-radius: 4px;
+ flex-shrink: 0;
+}
+.psc-pp-num {
+ flex: 1;
+ font-size: .88rem;
+ font-weight: 700;
+ color: var(--text1);
+ direction: ltr;
+ text-align: right;
+}
+.psc-call-btn {
+ background: var(--green);
+ color: #fff;
+ border: none;
+ width: 28px;
+ height: 28px;
+ border-radius: 50%;
+ cursor: pointer;
+ font-size: .8rem;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ text-decoration: none;
+ flex-shrink: 0;
+}
+
+/* type tags */
+.psc-type-tag {
+ position: absolute;
+ bottom: -4px;
+ left: -4px;
+ font-size: .55rem;
+ padding: .1rem .25rem;
+ border-radius: 4px;
+ font-weight: 700;
+}
+.psc-tag-listing { background: rgba(241,196,15,.2); color: #f1c40f; }
+.psc-tag-skill { background: rgba(26,188,156,.2); color: var(--teal); }
+.psc-avatar { position: relative; }
+
+/* action buttons in search card */
+.psc-btn.psc-call {
+ background: rgba(39,174,96,.12);
+ color: var(--green);
+ border-color: rgba(39,174,96,.25);
+}
+.psc-btn.psc-call:hover { background: rgba(39,174,96,.25); }
+.psc-btn.psc-map {
+ background: rgba(52,152,219,.12);
+ color: var(--blue);
+ border-color: rgba(52,152,219,.25);
+}
+.psc-btn.psc-map:hover { background: rgba(52,152,219,.25); }
+
+/* --- Person Modal Enhanced --- */
+.pmh-online::after {
+ content: '';
+ position: absolute;
+ bottom: 2px;
+ left: 2px;
+ width: 10px; height: 10px;
+ background: var(--green);
+ border-radius: 50%;
+ border: 2px solid var(--card);
+}
+.pmh-job {
+ font-size: .8rem;
+ color: var(--teal);
+ font-weight: 600;
+ margin: .2rem 0;
+}
+.pm-public-phone {
+ display: flex;
+ align-items: center;
+ gap: .6rem;
+ background: rgba(39,174,96,.1);
+ border: 1.5px solid rgba(39,174,96,.3);
+ border-radius: var(--r);
+ padding: .7rem 1rem;
+ margin: .8rem 0 .5rem;
+}
+.pm-pp-icon { font-size: 1.2rem; }
+.pm-pp-number { flex: 1; font-size: 1rem; font-weight: 700; color: var(--text1); direction: ltr; }
+.pm-pp-call {
+ background: var(--green);
+ color: #fff;
+ border: none;
+ padding: .4rem .8rem;
+ border-radius: 8px;
+ cursor: pointer;
+ font-family: Tajawal, sans-serif;
+ font-size: .82rem;
+ font-weight: 700;
+ text-decoration: none;
+ white-space: nowrap;
+}
+.pm-bio {
+ font-size: .85rem;
+ color: var(--text2);
+ line-height: 1.5;
+ padding: .4rem 0;
+ border-bottom: 1px solid var(--border);
+ margin-bottom: .5rem;
+}
+.pm-contact-grid {
+ display: flex;
+ flex-wrap: wrap;
+ gap: .4rem;
+ margin: .4rem 0;
+}
+.pm-contact-item {
+ display: flex;
+ align-items: center;
+ gap: .3rem;
+ padding: .35rem .7rem;
+ border-radius: 8px;
+ font-size: .8rem;
+ font-weight: 600;
+ cursor: pointer;
+ text-decoration: none;
+ transition: filter .2s;
+}
+.pm-contact-item:hover { filter: brightness(1.2); }
+.pm-wa { background: rgba(37,211,102,.15); color: #25d366; border: 1px solid rgba(37,211,102,.3); }
+.pm-tg { background: rgba(0,136,204,.15); color: #0088cc; border: 1px solid rgba(0,136,204,.3); }
+.pm-web { background: rgba(52,152,219,.15); color: var(--blue); border: 1px solid rgba(52,152,219,.3); }
+.pm-actions {
+ display: flex;
+ gap: .5rem;
+ margin-top: .8rem;
+}
+.pm-dm-btn { flex: 2; }
+.pm-map-btn { flex: 1; background: rgba(52,152,219,.12) !important; color: var(--blue) !important; border-color: rgba(52,152,219,.3) !important; }
+
+/* --- Person pin on map --- */
+.person-pin-marker {
+ width: 44px;
+ height: 44px;
+ border-radius: 50%;
+ background: linear-gradient(135deg, var(--teal), #0fa887);
+ color: #fff;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: .85rem;
+ font-weight: 700;
+ box-shadow: 0 0 0 3px rgba(26,188,156,.4), 0 4px 12px rgba(0,0,0,.4);
+ border: 2px solid rgba(255,255,255,.3);
+}
+
+/* --- Messages Section Enhanced --- */
+.msg-quick-search {
+ position: relative;
+ margin-bottom: .5rem;
+}
+.msg-quick-results {
+ position: absolute;
+ top: 100%;
+ right: 0;
+ left: 0;
+ background: var(--card);
+ border: 1px solid var(--border);
+ border-radius: var(--r);
+ z-index: 100;
+ max-height: 250px;
+ overflow-y: auto;
+ box-shadow: 0 8px 24px rgba(0,0,0,.3);
+}
+.mqr-item {
+ display: flex;
+ align-items: center;
+ gap: .7rem;
+ padding: .7rem 1rem;
+ cursor: pointer;
+ border-bottom: 1px solid var(--border);
+ transition: background .15s;
+}
+.mqr-item:hover { background: rgba(26,188,156,.06); }
+.mqr-item:last-child { border-bottom: none; }
+.mqr-avatar {
+ width: 34px; height: 34px;
+ border-radius: 50%;
+ background: linear-gradient(135deg, var(--teal), #0fa887);
+ color: #fff;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: .78rem;
+ font-weight: 700;
+ flex-shrink: 0;
+}
+.mqr-name { font-size: .88rem; font-weight: 600; color: var(--text1); }
+.mqr-area { font-size: .72rem; color: var(--text2); }
+
+.public-chat-btn {
+ background: rgba(26,188,156,.1);
+ border: 1.5px solid rgba(26,188,156,.3);
+ color: var(--teal);
+ padding: .7rem 1.5rem;
+ border-radius: var(--r);
+ cursor: pointer;
+ font-family: Tajawal, sans-serif;
+ font-size: .9rem;
+ font-weight: 700;
+ transition: all .2s;
+}
+.public-chat-btn:hover { background: rgba(26,188,156,.18); }
+
+
+/* ============================================================
+ SHARE APP MODAL - مشاركة رابط التطبيق
+============================================================ */
+
+/* Menu item for share app */
+.share-app-menu-item {
+ color: var(--teal) !important;
+ background: rgba(26,188,156,.07) !important;
+ border: 1px solid rgba(26,188,156,.25) !important;
+ font-weight: 600;
+}
+.share-app-menu-item:hover {
+ background: rgba(26,188,156,.15) !important;
+}
+
+/* Modal box override for share */
+.share-app-modal-box {
+ max-width: 380px;
+ padding: 0;
+ overflow: hidden;
+ border: 1px solid rgba(26,188,156,.3);
+}
+
+/* Header */
+.share-app-header {
+ background: linear-gradient(135deg, #0f3460 0%, #1a1a2e 100%);
+ padding: 1.5rem 1.2rem 1rem;
+ text-align: center;
+ border-bottom: 1px solid rgba(26,188,156,.2);
+}
+.share-app-logo {
+ font-size: 2.5rem;
+ animation: share-pulse 1.5s ease-in-out infinite;
+}
+@keyframes share-pulse {
+ 0%, 100% { transform: scale(1); }
+ 50% { transform: scale(1.1); }
+}
+.share-app-title {
+ font-size: 1.15rem;
+ font-weight: 700;
+ color: var(--teal);
+ margin: .3rem 0 .2rem;
+}
+.share-app-sub {
+ font-size: .8rem;
+ color: var(--text2);
+ margin: 0;
+ line-height: 1.4;
+}
+
+/* QR code area */
+.share-qr-area {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ padding: 1rem 1rem .5rem;
+ background: #12122a;
+}
+.share-qr-area canvas {
+ border-radius: 10px;
+ border: 2px solid rgba(26,188,156,.3);
+ padding: 6px;
+ background: #1a1a2e;
+ display: block;
+}
+.share-qr-label {
+ font-size: .75rem;
+ color: var(--text2);
+ margin: .5rem 0 0;
+ text-align: center;
+}
+
+/* URL copy row */
+.share-url-row {
+ display: flex;
+ align-items: center;
+ gap: .5rem;
+ padding: .6rem 1rem;
+ background: #0f0f24;
+}
+.share-url-input {
+ flex: 1;
+ background: rgba(255,255,255,.05);
+ border: 1px solid rgba(26,188,156,.25);
+ border-radius: 8px;
+ padding: .45rem .6rem;
+ color: var(--teal);
+ font-size: .78rem;
+ font-family: monospace;
+ direction: ltr;
+ outline: none;
+ min-width: 0;
+}
+.share-url-copy-btn {
+ background: rgba(26,188,156,.15);
+ border: 1px solid rgba(26,188,156,.4);
+ color: var(--teal);
+ border-radius: 8px;
+ padding: .45rem .8rem;
+ cursor: pointer;
+ font-size: .8rem;
+ font-family: inherit;
+ white-space: nowrap;
+ transition: background .2s;
+ flex-shrink: 0;
+}
+.share-url-copy-btn:hover {
+ background: rgba(26,188,156,.3);
+}
+
+/* Share buttons grid */
+.share-buttons-grid {
+ display: grid;
+ grid-template-columns: repeat(4, 1fr);
+ gap: .6rem;
+ padding: .7rem 1rem;
+ background: #0f0f24;
+}
+.share-btn-card {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: .3rem;
+ padding: .65rem .3rem;
+ border-radius: 12px;
+ border: 1px solid rgba(255,255,255,.08);
+ background: rgba(255,255,255,.04);
+ cursor: pointer;
+ transition: all .2s;
+ font-family: inherit;
+}
+.share-btn-card:hover {
+ transform: translateY(-2px);
+ background: rgba(255,255,255,.1);
+}
+.sbc-icon {
+ font-size: 1.4rem;
+ line-height: 1;
+}
+.sbc-label {
+ font-size: .68rem;
+ color: var(--text2);
+ font-weight: 500;
+}
+
+.share-whatsapp { border-color: rgba(37,211,102,.3) !important; }
+.share-whatsapp:hover { background: rgba(37,211,102,.12) !important; }
+.share-whatsapp .sbc-label { color: #25d366; }
+
+.share-telegram { border-color: rgba(36,161,222,.3) !important; }
+.share-telegram:hover { background: rgba(36,161,222,.12) !important; }
+.share-telegram .sbc-label { color: #24a1de; }
+
+.share-twitter { border-color: rgba(255,255,255,.15) !important; }
+.share-twitter:hover { background: rgba(255,255,255,.1) !important; }
+.share-twitter .sbc-label { color: var(--text1); }
+.share-twitter .sbc-icon { font-style: normal; font-weight: 900; font-size: 1.2rem; color: var(--text1); }
+
+.share-native { border-color: rgba(26,188,156,.3) !important; }
+.share-native:hover { background: rgba(26,188,156,.12) !important; }
+.share-native .sbc-label { color: var(--teal); }
+
+/* Stats bar */
+.share-stats-bar {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: .8rem;
+ padding: .8rem 1rem 1.2rem;
+ background: linear-gradient(135deg, #0f3460 0%, #1a1a2e 100%);
+ border-top: 1px solid rgba(26,188,156,.15);
+}
+.ssb-item {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: .15rem;
+}
+.ssb-val {
+ font-size: 1.1rem;
+ font-weight: 700;
+ color: var(--teal);
+}
+.ssb-key {
+ font-size: .7rem;
+ color: var(--text2);
+}
+.ssb-divider {
+ width: 1px;
+ height: 28px;
+ background: rgba(26,188,156,.2);
+}
+
+/* Responsive */
+@media (max-width: 400px) {
+ .share-app-modal-box { max-width: 95vw; }
+ .share-buttons-grid { gap: .4rem; padding: .6rem .7rem; }
+ .sbc-icon { font-size: 1.2rem; }
+ .sbc-label { font-size: .63rem; }
+}
+
+/* Share links as buttons */
+a.share-btn-card {
+ text-decoration: none;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+}
+
+/* ============================================================
+ PWA — تثبيت التطبيق وعدم الاتصال
+============================================================ */
+
+/* ── Offline Indicator ── */
+.offline-indicator {
+ position: fixed;
+ top: 0; left: 0; right: 0;
+ background: linear-gradient(90deg, #e67e22, #d35400);
+ color: #fff;
+ text-align: center;
+ padding: .4rem 1rem;
+ font-size: .8rem;
+ font-weight: 600;
+ z-index: 500;
+ pointer-events: none;
+ animation: slideDown .3s ease;
+ letter-spacing: .02em;
+}
+.offline-indicator.hidden { display: none !important; }
+@keyframes slideDown { from{transform:translateY(-100%)} to{transform:translateY(0)} }
+
+/* ── PWA Install Banner (bottom sheet) ── */
+.pwa-install-banner {
+ position: fixed;
+ bottom: 70px;
+ left: 1rem; right: 1rem;
+ background: linear-gradient(135deg, #0f3460, #1a1a2e);
+ border: 1px solid rgba(26,188,156,.4);
+ border-radius: 16px;
+ padding: .9rem 1rem;
+ z-index: 500;
+ box-shadow: 0 -4px 30px rgba(0,0,0,.5);
+ transform: translateY(120%);
+ transition: transform .4s cubic-bezier(.4,0,.2,1);
+ display: flex;
+ align-items: center;
+ gap: .8rem;
+}
+.pwa-install-banner.pwa-banner-show {
+ transform: translateY(0);
+}
+.pwa-banner-content {
+ display: flex;
+ align-items: center;
+ gap: .7rem;
+ flex: 1;
+ min-width: 0;
+}
+.pwa-banner-icon {
+ font-size: 2rem;
+ flex-shrink: 0;
+ animation: pwa-pulse 2s ease-in-out infinite;
+}
+@keyframes pwa-pulse { 0%,100%{transform:scale(1)} 50%{transform:scale(1.15)} }
+.pwa-banner-text {
+ display: flex;
+ flex-direction: column;
+ gap: .15rem;
+ min-width: 0;
+}
+.pwa-banner-text strong {
+ font-size: .9rem;
+ color: var(--text1);
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+.pwa-banner-text span {
+ font-size: .73rem;
+ color: var(--text2);
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+.pwa-banner-actions {
+ display: flex;
+ align-items: center;
+ gap: .4rem;
+ flex-shrink: 0;
+}
+.pwa-install-btn {
+ background: #1abc9c;
+ color: #0a0e1a;
+ border: none;
+ border-radius: 20px;
+ padding: .45rem 1rem;
+ font-size: .82rem;
+ font-weight: 700;
+ cursor: pointer;
+ font-family: inherit;
+ white-space: nowrap;
+ transition: opacity .2s;
+}
+.pwa-install-btn:hover { opacity: .85; }
+.pwa-dismiss-btn {
+ background: rgba(255,255,255,.08);
+ color: var(--text2);
+ border: 1px solid rgba(255,255,255,.1);
+ border-radius: 50%;
+ width: 28px; height: 28px;
+ font-size: .8rem;
+ cursor: pointer;
+ display: flex; align-items: center; justify-content: center;
+ flex-shrink: 0;
+ transition: background .2s;
+}
+.pwa-dismiss-btn:hover { background: rgba(255,255,255,.15); }
+
+/* ── Menu Install Button ── */
+.pwa-install-menu-item {
+ color: #f39c12 !important;
+ background: rgba(243,156,18,.07) !important;
+ border: 1px solid rgba(243,156,18,.25) !important;
+ font-weight: 600;
+ position: relative;
+}
+.pwa-install-menu-item::after {
+ content: 'جديد';
+ position: absolute;
+ left: .8rem;
+ top: 50%;
+ transform: translateY(-50%);
+ background: #f39c12;
+ color: #0a0e1a;
+ font-size: .6rem;
+ font-weight: 700;
+ padding: .1rem .35rem;
+ border-radius: 8px;
+}
+
+/* ── PWA standalone tweaks ── */
+@media (display-mode: standalone) {
+ /* Hide browser UI hints when installed */
+ .pwa-install-menu-item { display: none !important; }
+ #pwaInstallBanner { display: none !important; }
+ /* Safe area for notch phones */
+ body { padding-top: env(safe-area-inset-top); }
+ .top-bar {
+ padding-top: calc(.6rem + env(safe-area-inset-top));
+ }
+ .bottom-nav {
+ padding-bottom: env(safe-area-inset-bottom);
+ }
+}
+
+/* ============================================================
+ 🩸 BLOOD BANK
+ ============================================================ */
+.blood-top { background: linear-gradient(135deg,rgba(192,57,43,.15),rgba(231,76,60,.08)); border-bottom:2px solid rgba(231,76,60,.3); }
+.blood-tabs { display:flex; gap:.5rem; margin-bottom:1rem; }
+.blood-tab { flex:1; padding:.6rem; background:var(--card); border:1px solid var(--border); color:var(--text2); border-radius:.6rem; cursor:pointer; font-size:.9rem; transition:.2s; }
+.blood-tab.active-blood-tab { background:rgba(192,57,43,.2); border-color:rgba(231,76,60,.5); color:#e74c3c; font-weight:600; }
+.blood-type-grid { display:grid; grid-template-columns:repeat(4,1fr); gap:.5rem; margin:.4rem 0; }
+.btype-btn { padding:.5rem .3rem; background:var(--card); border:1.5px solid var(--border); color:var(--text); border-radius:.5rem; cursor:pointer; font-weight:700; font-size:.95rem; transition:.2s; }
+.btype-btn.active-btype { background:rgba(192,57,43,.2); border-color:#e74c3c; color:#e74c3c; box-shadow:0 0 8px rgba(231,76,60,.3); }
+.btn-blood { background:linear-gradient(135deg,#c0392b,#e74c3c)!important; }
+.blood-list { display:flex; flex-direction:column; gap:.7rem; }
+.blood-donor-card { background:var(--card); border:1px solid var(--border); border-radius:.8rem; padding:.9rem; display:flex; align-items:center; gap:.8rem; }
+.blood-type-badge { width:44px; height:44px; border-radius:50%; background:rgba(231,76,60,.15); border:2px solid #e74c3c; display:flex; align-items:center; justify-content:center; font-weight:800; font-size:1rem; color:#e74c3c; flex-shrink:0; }
+.blood-donor-info { flex:1; min-width:0; }
+.blood-donor-name { font-weight:600; font-size:.95rem; color:var(--text); }
+.blood-donor-area { font-size:.8rem; color:var(--text2); margin-top:.2rem; }
+.blood-donor-phone { display:inline-block; margin-top:.4rem; padding:.25rem .6rem; background:rgba(26,188,156,.1); border:1px solid rgba(26,188,156,.3); border-radius:.4rem; font-size:.8rem; color:var(--teal); text-decoration:none; }
+
+/* ============================================================
+ ⚡ ELECTRICITY SCHEDULE
+ ============================================================ */
+.power-top { background: linear-gradient(135deg,rgba(241,196,15,.12),rgba(243,156,18,.06)); border-bottom:2px solid rgba(241,196,15,.3); }
+.power-status-card { background:var(--card); border:1px solid var(--border); border-radius:1rem; padding:1.2rem; display:flex; align-items:center; gap:1rem; flex-wrap:wrap; }
+.power-icon { font-size:2.5rem; flex-shrink:0; }
+.power-status-text { flex:1; min-width:0; }
+.power-status-text span:first-child { display:block; font-size:.85rem; color:var(--text2); }
+.power-area-name { display:block; font-size:1rem; font-weight:700; color:var(--text); margin-top:.2rem; }
+.power-vote-btns { display:flex; gap:.5rem; margin-right:auto; }
+.power-vote { padding:.4rem .8rem; border-radius:.5rem; border:1.5px solid; cursor:pointer; font-size:.85rem; font-weight:600; transition:.2s; }
+.on-vote { border-color:rgba(39,174,96,.4); background:rgba(39,174,96,.08); color:#27ae60; }
+.on-vote:hover { background:rgba(39,174,96,.2); }
+.off-vote { border-color:rgba(231,76,60,.4); background:rgba(231,76,60,.08); color:#e74c3c; }
+.off-vote:hover { background:rgba(231,76,60,.2); }
+.power-time-row { display:grid; grid-template-columns:1fr 1fr; gap:.6rem; margin-top:.6rem; }
+.power-time-field label { display:block; font-size:.78rem; color:var(--text2); margin-bottom:.3rem; }
+.btn-power { background:linear-gradient(135deg,#f39c12,#f1c40f)!important; color:#1a1a2e!important; }
+.power-list { display:flex; flex-direction:column; gap:.7rem; }
+.power-card { background:var(--card); border:1px solid var(--border); border-radius:.8rem; padding:.9rem; }
+.power-card-top { display:flex; align-items:center; justify-content:space-between; margin-bottom:.4rem; }
+.power-area-badge { font-weight:700; color:var(--yellow); }
+.power-cut-time { font-size:.85rem; color:var(--text2); }
+.power-card-info { font-size:.85rem; color:var(--text); display:flex; gap:1rem; flex-wrap:wrap; }
+.power-card-votes { font-size:.8rem; color:var(--text2); margin-top:.3rem; }
+.power-on { border-color:rgba(39,174,96,.35)!important; }
+.power-off { border-color:rgba(231,76,60,.35)!important; }
+
+/* ============================================================
+ 🕌 PRAYER TIMES
+ ============================================================ */
+.prayer-top { background: linear-gradient(135deg,rgba(26,188,156,.12),rgba(22,160,133,.06)); border-bottom:2px solid rgba(26,188,156,.3); }
+.prayer-countdown-card { background:linear-gradient(135deg,rgba(26,188,156,.15),rgba(22,160,133,.08)); border:1px solid rgba(26,188,156,.3); border-radius:1.2rem; padding:1.5rem; text-align:center; margin-bottom:1.2rem; }
+.prayer-next-label { font-size:.8rem; color:var(--teal); text-transform:uppercase; letter-spacing:.08em; margin-bottom:.3rem; }
+.prayer-next-name { font-size:1.8rem; font-weight:800; color:var(--text); margin-bottom:.4rem; }
+.prayer-countdown { font-size:2.5rem; font-weight:900; color:var(--teal); font-variant-numeric:tabular-nums; letter-spacing:.05em; }
+.prayer-hijri { font-size:.85rem; color:var(--text2); margin-top:.4rem; }
+.prayer-grid { display:grid; grid-template-columns:repeat(3,1fr); gap:.6rem; margin-bottom:1rem; }
+.prayer-card { background:var(--card); border:1px solid var(--border); border-radius:.8rem; padding:.8rem .5rem; text-align:center; transition:.2s; }
+.prayer-card.prayer-active { border-color:var(--teal); background:rgba(26,188,156,.08); box-shadow:0 0 12px rgba(26,188,156,.2); }
+.prayer-icon { font-size:1.4rem; margin-bottom:.2rem; }
+.prayer-name { font-size:.8rem; color:var(--text2); margin-bottom:.2rem; }
+.prayer-time { font-size:1rem; font-weight:700; color:var(--text); }
+.prayer-location-row { display:flex; align-items:center; justify-content:space-between; margin:.6rem 0; font-size:.85rem; color:var(--text2); }
+
+/* ============================================================
+ 📷 PHOTO UPLOAD
+ ============================================================ */
+.photo-upload-row { display:flex; align-items:center; gap:.6rem; margin-top:.6rem; }
+.photo-upload-btn { display:inline-flex; align-items:center; gap:.4rem; padding:.45rem .9rem; background:rgba(26,188,156,.1); border:1.5px dashed rgba(26,188,156,.4); border-radius:.6rem; color:var(--teal); cursor:pointer; font-size:.85rem; font-weight:600; transition:.2s; }
+.photo-upload-btn:hover { background:rgba(26,188,156,.18); border-color:rgba(26,188,156,.6); }
+.photo-name-label { font-size:.78rem; color:var(--text2); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; max-width:160px; }
+.photo-preview { margin-top:.6rem; border-radius:.6rem; overflow:hidden; border:1px solid var(--border); max-height:180px; }
+.photo-preview img { width:100%; max-height:180px; object-fit:cover; display:block; }
+
+/* ============================================================
+ QUICK CARD COLORS for new sections
+ ============================================================ */
+.q-blood { border-color:rgba(192,57,43,.4); background:rgba(192,57,43,.08); }
+.q-blood:hover { border-color:rgba(231,76,60,.65); box-shadow:0 8px 24px rgba(231,76,60,.2); transform:translateY(-3px) scale(1.02); }
+.q-blood::before { background:rgba(231,76,60,.06); }
+.q-power { border-color:rgba(241,196,15,.4); background:rgba(241,196,15,.08); }
+.q-power:hover { border-color:rgba(243,156,18,.65); box-shadow:0 8px 24px rgba(243,156,18,.2); transform:translateY(-3px) scale(1.02); }
+.q-power::before { background:rgba(241,196,15,.06); }
+.q-prayer { border-color:rgba(26,188,156,.4); background:rgba(26,188,156,.08); }
+.q-prayer:hover { border-color:rgba(26,188,156,.65); box-shadow:0 8px 24px rgba(26,188,156,.2); transform:translateY(-3px) scale(1.02); }
+.q-prayer::before { background:rgba(26,188,156,.06); }
+
+/* ============================================================
+ MENU ITEMS for new sections
+ ============================================================ */
+.blood-menu { color:#e87070!important; }
+.power-menu { color:#f1c40f!important; }
+.prayer-menu { color:#1abc9c!important; }
+
+/* ============================================================
+ FIX: Blood type grid RTL + badge colors
+ ============================================================ */
+.blood-type-grid { direction: ltr; }
+.blood-type-grid .btype-btn { font-size:.9rem; }
+.blood-type-badge.Apos, .blood-type-badge.Aneg { background:rgba(192,57,43,.15); border-color:rgba(192,57,43,.6); color:#e74c3c; }
+.blood-type-badge.Bpos, .blood-type-badge.Bneg { background:rgba(142,68,173,.15); border-color:rgba(142,68,173,.6); color:#9b59b6; }
+.blood-type-badge.ABpos, .blood-type-badge.ABneg { background:rgba(41,128,185,.15); border-color:rgba(41,128,185,.6); color:#2980b9; }
+.blood-type-badge.Opos, .blood-type-badge.Oneg { background:rgba(39,174,96,.15); border-color:rgba(39,174,96,.6); color:#27ae60; }
+
+/* Blood section tabs */
+.blood-tabs { display:flex; gap:.4rem; margin-bottom:1rem; background:var(--card2); border-radius:.7rem; padding:.3rem; }
+.blood-tab { flex:1; padding:.5rem .3rem; border:none; background:transparent; color:var(--text2); border-radius:.5rem; cursor:pointer; font-size:.82rem; font-weight:600; transition:.2s; font-family:inherit; }
+.blood-tab.active-blood-tab { background:var(--card); color:var(--text); box-shadow:0 2px 8px rgba(0,0,0,.2); }
+
+/* Power section time row */
+.power-time-row { display:grid; grid-template-columns:1fr 1fr; gap:.6rem; margin-top:.6rem; }
+.power-time-field { display:flex; flex-direction:column; gap:.2rem; }
+
+/* Prayer section method select */
+#prayerMethod { color:var(--text); background:var(--card); border:1px solid var(--border); }
+
+/* Fix: main content scrolling */
+#mainContent { overflow-y:auto; }
+
+
+/* ============================================================
+ NEW SECTIONS - CSS
+ ============================================================ */
+
+/* EMPTY STATE */
+.empty-state { text-align:center; padding:2rem 1rem; color:var(--text2); font-size:.9rem; line-height:1.8; }
+.empty-state br+small { font-size:.8rem; opacity:.7; }
+
+/* ============================================================
+ 🏥 HOSPITALS
+ ============================================================ */
+.hosp-top { background:linear-gradient(135deg,rgba(52,152,219,.18),rgba(41,128,185,.08)); border-bottom:1px solid rgba(52,152,219,.2); }
+.hosp-top h2 { color:#3498db; }
+.hosp-menu { color:#3498db; }
+.hosp-list { display:flex; flex-direction:column; gap:.7rem; }
+.hosp-card { background:var(--card); border:1px solid var(--border); border-radius:var(--r); overflow:hidden; }
+.hosp-card-top { display:flex; align-items:flex-start; gap:.7rem; padding:.9rem .9rem .5rem; }
+.hosp-type-icon { font-size:1.8rem; flex-shrink:0; }
+.hosp-info { flex:1; min-width:0; }
+.hosp-name { font-weight:700; font-size:.95rem; color:var(--text1); margin-bottom:.2rem; }
+.hosp-meta { color:var(--text2); font-size:.8rem; margin-bottom:.2rem; }
+.hosp-addr { color:var(--text2); font-size:.8rem; margin-bottom:.2rem; }
+.hosp-phone { color:#3498db; font-size:.85rem; margin-bottom:.2rem; }
+.hosp-stars { font-size:.85rem; margin-top:.3rem; }
+.card-dist { color:var(--teal); font-size:.8rem; margin-right:.5rem; }
+.hosp-badge-emerg { background:rgba(231,76,60,.15); color:#e74c3c; border-radius:.3rem; padding:.1rem .4rem; font-size:.75rem; font-weight:600; }
+.hosp-type-badge { font-size:.72rem; background:rgba(52,152,219,.15); color:#3498db; padding:.15rem .4rem; border-radius:.3rem; }
+.hosp-emergency-badge { font-size:.72rem; background:rgba(231,76,60,.15); color:#e74c3c; padding:.15rem .4rem; border-radius:.3rem; }
+.hosp-actions { display:flex; gap:.4rem; flex-wrap:wrap; padding:.5rem .9rem .7rem; border-top:1px solid var(--border); background:rgba(255,255,255,.02); }
+.hosp-btn { background:rgba(255,255,255,.06); border:1px solid var(--border); border-radius:.5rem; padding:.35rem .7rem; cursor:pointer; font-size:.82rem; font-family:inherit; color:var(--text); white-space:nowrap; transition:opacity .15s; }
+.hosp-btn:hover { opacity:.8; }
+.hosp-call { background:rgba(39,174,96,.1)!important; color:#27ae60!important; border-color:rgba(39,174,96,.3)!important; }
+.hosp-map { background:rgba(52,152,219,.1)!important; color:#3498db!important; border-color:rgba(52,152,219,.3)!important; }
+.hosp-rate { background:rgba(241,196,15,.1)!important; color:#f1c40f!important; border-color:rgba(241,196,15,.3)!important; }
+
+/* ============================================================
+ 📰 NEWS
+ ============================================================ */
+.news-top { background:linear-gradient(135deg,rgba(241,196,15,.18),rgba(230,126,34,.08)); border-bottom:1px solid rgba(241,196,15,.2); }
+.news-top h2 { color:#f39c12; }
+.news-menu { color:#f39c12; }
+.news-card { background:var(--card); border:1px solid var(--border); border-radius:var(--r); padding:.9rem; margin-bottom:.7rem; }
+.news-card-top { display:flex; justify-content:space-between; align-items:center; margin-bottom:.5rem; flex-wrap:wrap; gap:.3rem; }
+.news-cat-badge { background:rgba(241,196,15,.15); color:#f39c12; border-radius:.4rem; padding:.15rem .5rem; font-size:.78rem; font-weight:600; }
+.news-time { color:var(--text2); font-size:.78rem; }
+.news-title { font-weight:700; font-size:.95rem; color:var(--text1); margin-bottom:.4rem; line-height:1.4; }
+.news-body { color:var(--text2); font-size:.87rem; line-height:1.5; margin-bottom:.5rem; }
+.news-area { color:var(--teal); font-size:.8rem; margin-bottom:.5rem; }
+.news-footer { display:flex; justify-content:space-between; align-items:center; flex-wrap:wrap; gap:.4rem; }
+.news-trust { display:flex; align-items:center; gap:.4rem; flex:1; }
+.news-cred-bar { height:6px; background:rgba(255,255,255,.08); border-radius:3px; flex:1; max-width:80px; }
+.news-cred-fill { height:100%; background:var(--teal); border-radius:3px; }
+.news-vote-btn { background:rgba(255,255,255,.06); border:1px solid var(--border); border-radius:.4rem; padding:.25rem .5rem; cursor:pointer; font-size:.8rem; font-family:inherit; color:var(--text); }
+.news-vote-down { background:rgba(231,76,60,.07)!important; border-color:rgba(231,76,60,.2)!important; }
+.news-vote-btn:hover { opacity:.8; }
+
+/* ============================================================
+ 🚗 RIDES
+ ============================================================ */
+.rides-top { background:linear-gradient(135deg,rgba(39,174,96,.18),rgba(46,204,113,.08)); border-bottom:1px solid rgba(39,174,96,.2); }
+.rides-top h2 { color:#27ae60; }
+.rides-menu { color:#3498db; }
+.ride-card { background:var(--card); border:1px solid var(--border); border-radius:var(--r); padding:.9rem; margin-bottom:.7rem; }
+.ride-card.ride-full { opacity:.65; }
+.ride-route { display:flex; align-items:center; gap:.5rem; flex-wrap:wrap; font-weight:700; font-size:.95rem; margin-bottom:.5rem; }
+.ride-from { color:#27ae60; flex:1; min-width:60px; }
+.ride-arrow { color:var(--teal); font-size:1.1rem; }
+.ride-to { color:#3498db; flex:1; min-width:60px; }
+.ride-meta { display:flex; flex-wrap:wrap; gap:.5rem; font-size:.83rem; color:var(--text2); margin-bottom:.4rem; }
+.ride-notes { color:var(--text2); font-size:.82rem; margin-bottom:.4rem; background:rgba(255,255,255,.04); padding:.3rem .5rem; border-radius:.4rem; }
+.ride-actions { display:flex; gap:.4rem; flex-wrap:wrap; margin-top:.5rem; }
+.btn-rides { background:linear-gradient(135deg,#27ae60,#2ecc71)!important; }
+
+/* ============================================================
+ 🌦️ WEATHER
+ ============================================================ */
+.weather-top { background:linear-gradient(135deg,rgba(52,152,219,.18),rgba(26,188,156,.08)); border-bottom:1px solid rgba(52,152,219,.2); }
+.weather-top h2 { color:#2e86c1; }
+.weather-menu { color:#3498db; }
+.weather-main-card { background:linear-gradient(135deg,var(--card),rgba(52,152,219,.07)); border:1px solid rgba(52,152,219,.2); border-radius:var(--r); padding:1.5rem; text-align:center; margin-bottom:1rem; }
+.weather-loading { text-align:center; color:var(--text2); padding:2rem; font-size:.9rem; }
+.weather-icon-big { font-size:3.5rem; line-height:1; margin-bottom:.5rem; }
+.weather-temp-big { font-size:2.5rem; font-weight:800; color:var(--text1); }
+.weather-desc { font-size:.95rem; color:var(--text2); margin:.3rem 0; }
+.weather-feels { font-size:.82rem; color:var(--teal); margin-bottom:.8rem; }
+.weather-details-row { display:grid; grid-template-columns:repeat(3,1fr); gap:.5rem; margin-top:.8rem; }
+.weather-detail,.wd-item { display:flex; flex-direction:column; align-items:center; gap:.15rem; background:rgba(255,255,255,.04); padding:.5rem; border-radius:.5rem; border:1px solid var(--border); }
+.weather-detail span:first-child,.wd-item span:first-child { font-size:1.1rem; }
+.weather-detail span:last-of-type,.wd-item span:nth-child(2) { font-size:.85rem; font-weight:600; color:var(--text1); }
+.weather-detail small,.wd-item small { font-size:.7rem; color:var(--text2); }
+.weather-forecast-row,.weather-forecast { display:flex; gap:.5rem; overflow-x:auto; padding:.3rem 0; -webkit-overflow-scrolling:touch; scrollbar-width:thin; }
+.weather-day-card,.wdc-card { background:rgba(255,255,255,.04); border:1px solid var(--border); border-radius:.6rem; padding:.5rem .6rem; text-align:center; min-width:65px; flex-shrink:0; }
+.wd-name,.wdc-day { font-size:.75rem; color:var(--text2); margin-bottom:.3rem; white-space:nowrap; }
+.wd-emoji,.wdc-icon { font-size:1.4rem; margin-bottom:.2rem; }
+.wd-max,.wdc-max { font-size:.88rem; font-weight:700; color:var(--text1); }
+.wd-min,.wdc-min { font-size:.78rem; color:var(--text2); }
+.wdc-temps { display:flex; gap:.3rem; justify-content:center; font-size:.82rem; }
+.weather-header { text-align:center; padding:.5rem 0 1rem; }
+.weather-city { font-size:.9rem; color:var(--text2); margin-bottom:.5rem; }
+
+/* ============================================================
+ 💧 WATER
+ ============================================================ */
+.water-top { background:linear-gradient(135deg,rgba(41,128,185,.18),rgba(52,152,219,.08)); border-bottom:1px solid rgba(41,128,185,.2); }
+.water-top h2 { color:#1a5276; }
+.water-menu { color:#3498db; }
+.water-status-card { background:var(--card); border:1px solid rgba(41,128,185,.3); border-radius:var(--r); padding:1rem; display:flex; align-items:center; gap:.8rem; flex-wrap:wrap; margin-bottom:.8rem; }
+.water-icon { font-size:2rem; flex-shrink:0; }
+.water-status-text { flex:1; min-width:100px; }
+.water-area-name { display:block; font-size:.85rem; color:var(--teal); }
+.water-vote-btns { display:flex; gap:.5rem; flex-shrink:0; }
+.water-vote { border:none; border-radius:.5rem; padding:.4rem .8rem; cursor:pointer; font-size:.85rem; font-weight:700; font-family:inherit; }
+.water-on { background:rgba(39,174,96,.15); color:#27ae60; border:1px solid rgba(39,174,96,.3)!important; }
+.water-off { background:rgba(231,76,60,.15); color:#e74c3c; border:1px solid rgba(231,76,60,.3)!important; }
+.water-list { display:flex; flex-direction:column; gap:.6rem; }
+.water-card { background:var(--card); border:1px solid var(--border); border-radius:var(--r); padding:.8rem; }
+.water-card-top { display:flex; justify-content:space-between; align-items:center; margin-bottom:.4rem; flex-wrap:wrap; gap:.3rem; }
+.water-type-badge { border-radius:.3rem; padding:.1rem .4rem; font-size:.78rem; font-weight:600; }
+.wtype-cut { background:rgba(231,76,60,.15); color:#e74c3c; }
+.wtype-low { background:rgba(241,196,15,.15); color:#f39c12; }
+.wtype-dirty { background:rgba(52,73,94,.3); color:#95a5a6; }
+.wtype-dist { background:rgba(41,128,185,.15); color:#3498db; }
+.water-card-time { color:var(--text2); font-size:.78rem; }
+.water-card-area { font-size:.88rem; margin-bottom:.3rem; }
+.water-card-notes,.water-card-duration { color:var(--text2); font-size:.82rem; margin-bottom:.3rem; }
+.water-card-votes { display:flex; gap:.4rem; margin-top:.4rem; }
+.water-vote-sm { background:rgba(255,255,255,.06); border:1px solid var(--border); border-radius:.4rem; padding:.2rem .5rem; cursor:pointer; font-size:.8rem; font-family:inherit; color:var(--text); }
+.wv-yes { background:rgba(39,174,96,.08)!important; color:#27ae60!important; }
+.wv-no { background:rgba(231,76,60,.08)!important; color:#e74c3c!important; }
+.btn-water { background:linear-gradient(135deg,#2980b9,#16a085)!important; }
+
+/* ============================================================
+ 🎓 STUDY GROUPS
+ ============================================================ */
+.study-top { background:linear-gradient(135deg,rgba(142,68,173,.18),rgba(155,89,182,.08)); border-bottom:1px solid rgba(142,68,173,.2); }
+.study-top h2 { color:#9b59b6; }
+.study-menu { color:#9b59b6; }
+.study-card { background:var(--card); border:1px solid var(--border); border-radius:var(--r); padding:.9rem; margin-bottom:.8rem; }
+.study-card-top { display:flex; justify-content:space-between; align-items:center; margin-bottom:.4rem; }
+.study-level-badge { background:rgba(142,68,173,.15); color:#9b59b6; border-radius:1rem; padding:.2rem .6rem; font-size:.78rem; font-weight:700; }
+.study-members { font-size:.78rem; color:var(--text2); }
+.study-name { font-weight:700; font-size:.98rem; color:var(--text1); margin-bottom:.2rem; }
+.study-subject { font-size:.85rem; color:var(--teal); margin-bottom:.2rem; }
+.study-schedule,.study-area { font-size:.8rem; color:var(--text2); margin-bottom:.2rem; }
+.study-footer { display:flex; justify-content:space-between; align-items:center; flex-wrap:wrap; gap:.4rem; margin-top:.5rem; }
+.study-time { font-size:.75rem; color:var(--text2); }
+.study-contact-btn { background:rgba(39,174,96,.12); color:#27ae60; border:none; border-radius:.5rem; padding:.3rem .6rem; text-decoration:none; font-size:.82rem; font-family:inherit; }
+.study-join-btn { background:rgba(142,68,173,.15); color:#9b59b6; border:1px solid rgba(142,68,173,.3); border-radius:.5rem; padding:.3rem .7rem; cursor:pointer; font-size:.82rem; font-family:inherit; }
+.study-join-btn.study-joined { background:rgba(39,174,96,.1); color:#27ae60; border-color:rgba(39,174,96,.3); }
+.study-chat-btn { background:var(--teal); color:#fff; border:none; border-radius:.5rem; padding:.3rem .7rem; cursor:pointer; font-size:.82rem; font-family:inherit; }
+.btn-study { background:linear-gradient(135deg,#8e44ad,#9b59b6)!important; }
+.study-progress-bar { height:4px; background:rgba(255,255,255,.1); border-radius:2px; margin-top:.3rem; }
+.study-progress-fill { height:100%; background:linear-gradient(90deg,#9b59b6,#8e44ad); border-radius:2px; transition:width .4s; }
+.study-chat-header { display:flex; justify-content:space-between; align-items:center; background:var(--card); border:1px solid var(--border); border-radius:.7rem .7rem 0 0; padding:.7rem 1rem; font-weight:700; font-size:.9rem; }
+.study-chat-msgs { background:var(--bg); border:1px solid var(--border); border-top:none; padding:.8rem; min-height:200px; max-height:300px; overflow-y:auto; display:flex; flex-direction:column; gap:.5rem; }
+.study-chat-input { display:flex; gap:.5rem; background:var(--card); border:1px solid var(--border); border-top:none; border-radius:0 0 .7rem .7rem; padding:.5rem; }
+.study-msg { max-width:80%; background:var(--card); border:1px solid var(--border); border-radius:.6rem; padding:.4rem .7rem; }
+.study-msg-mine { align-self:flex-end; background:rgba(26,188,156,.1); border-color:rgba(26,188,156,.2); }
+.study-msg-author { font-size:.72rem; color:var(--text2); display:block; margin-bottom:.2rem; }
+.study-msg-text { font-size:.85rem; color:var(--text1); }
+.study-msg-time { font-size:.7rem; color:var(--text2); display:block; text-align:right; margin-top:.2rem; }
+
+/* ============================================================
+ 📦 HELP REQUESTS
+ ============================================================ */
+.help-top { background:linear-gradient(135deg,rgba(230,126,34,.18),rgba(231,76,60,.08)); border-bottom:1px solid rgba(230,126,34,.2); }
+.help-top h2 { color:#d35400; }
+.help-menu { color:#e67e22; }
+.help-card { background:var(--card); border:1px solid var(--border); border-radius:var(--r); padding:.9rem; margin-bottom:.8rem; }
+.help-card.help-urgent { border-color:rgba(231,76,60,.4); box-shadow:0 0 0 2px rgba(231,76,60,.1); }
+.help-card.help-closed { opacity:.65; }
+.help-card-top { display:flex; gap:.4rem; flex-wrap:wrap; margin-bottom:.4rem; align-items:center; }
+.help-type-badge { background:rgba(230,126,34,.15); color:#e67e22; border-radius:1rem; padding:.2rem .6rem; font-size:.78rem; font-weight:700; }
+.ht-food { background:rgba(230,126,34,.15); color:#e67e22; }
+.ht-medicine { background:rgba(155,89,182,.15); color:#9b59b6; }
+.ht-transport { background:rgba(39,174,96,.15); color:#27ae60; }
+.ht-shelter { background:rgba(52,152,219,.15); color:#3498db; }
+.ht-money { background:rgba(241,196,15,.15); color:#f1c40f; }
+.ht-other { background:rgba(127,140,141,.15); color:#95a5a6; }
+.help-urgent-badge { background:rgba(231,76,60,.15); color:#e74c3c; border-radius:1rem; padding:.2rem .6rem; font-size:.78rem; font-weight:700; animation:blink 1.2s ease-in-out infinite; }
+.help-title { font-weight:700; font-size:.95rem; color:var(--text1); margin-bottom:.3rem; }
+.help-desc { font-size:.83rem; color:var(--text2); line-height:1.5; margin-bottom:.4rem; }
+.help-meta { color:var(--text2); font-size:.8rem; margin-bottom:.4rem; }
+.help-time { font-size:.75rem; color:var(--text2); }
+.help-actions { display:flex; gap:.4rem; flex-wrap:wrap; margin-top:.4rem; }
+.btn-help { background:linear-gradient(135deg,#e67e22,#e74c3c)!important; }
+
+/* ============================================================
+ 🗳️ POLLS
+ ============================================================ */
+.polls-top { background:linear-gradient(135deg,rgba(22,160,133,.18),rgba(26,188,156,.08)); border-bottom:1px solid rgba(22,160,133,.2); }
+.polls-top h2 { color:#148f77; }
+.polls-menu { color:var(--teal); }
+.poll-card { background:var(--card); border:1px solid var(--border); border-radius:var(--r); padding:1rem; margin-bottom:.8rem; }
+.poll-card.poll-expired { opacity:.7; }
+.poll-card-top { display:flex; justify-content:space-between; align-items:flex-start; gap:.5rem; margin-bottom:.5rem; }
+.poll-q { font-weight:700; font-size:.95rem; flex:1; line-height:1.4; }
+.poll-badge-expired { background:rgba(127,140,141,.2); color:#95a5a6; border-radius:.3rem; padding:.1rem .4rem; font-size:.75rem; flex-shrink:0; }
+.poll-options { display:flex; flex-direction:column; gap:.4rem; margin-bottom:.6rem; }
+.poll-option { position:relative; border:1px solid var(--border); border-radius:.5rem; padding:.5rem .7rem; overflow:hidden; transition:border-color .2s,background .2s; }
+.poll-clickable { cursor:pointer; }
+.poll-clickable:hover { border-color:var(--teal); background:rgba(26,188,156,.07); }
+.poll-voted { border-color:var(--teal); }
+.poll-winner { border-color:var(--teal)!important; background:rgba(26,188,156,.1)!important; }
+.poll-opt-row { display:flex; justify-content:space-between; align-items:center; position:relative; margin-bottom:.2rem; }
+.poll-opt-text { font-size:.9rem; color:var(--text1); }
+.poll-opt-pct { font-weight:700; color:var(--teal); font-size:.88rem; }
+.poll-bar { height:5px; background:rgba(255,255,255,.06); border-radius:3px; position:relative; }
+.poll-bar-fill { height:100%; background:var(--teal); border-radius:3px; transition:width .5s; }
+.poll-footer { display:flex; justify-content:space-between; align-items:center; flex-wrap:wrap; gap:.4rem; font-size:.78rem; color:var(--text2); }
+.poll-option-inp { margin-bottom:.4rem; display:block; width:100%; box-sizing:border-box; }
+
+/* ============================================================
+ 📊 DASHBOARD
+ ============================================================ */
+.dash-top { background:linear-gradient(135deg,rgba(44,62,80,.5),rgba(52,152,219,.15)); border-bottom:1px solid rgba(52,152,219,.2); }
+.dash-top h2 { color:var(--teal); }
+.dash-menu { color:#3498db; }
+.dash-live-row { display:grid; grid-template-columns:repeat(2,1fr); gap:.6rem; margin-bottom:1rem; }
+.dash-stat-card { background:var(--card); border:1px solid var(--border); border-radius:var(--r); padding:1rem; text-align:center; }
+.dsc-num { font-size:1.6rem; font-weight:900; color:var(--teal); }
+.dsc-lbl { font-size:.75rem; color:var(--text2); margin-top:.2rem; }
+.dash-grid { display:grid; grid-template-columns:repeat(3,1fr); gap:.6rem; margin-top:.4rem; }
+.dash-feat-card { background:var(--card); border:1px solid var(--border); border-radius:var(--r); padding:.8rem; text-align:center; }
+.dfc-icon { font-size:1.4rem; margin-bottom:.3rem; }
+.dfc-num { font-size:1.2rem; font-weight:800; color:var(--teal); }
+.dfc-lbl { font-size:.7rem; color:var(--text2); margin-top:.2rem; }
+.dash-area-row { display:flex; align-items:center; gap:.5rem; padding:.5rem 0; border-bottom:1px solid var(--border); }
+.dash-area-rank { font-size:.9rem; font-weight:800; color:var(--teal); min-width:1.5rem; text-align:center; }
+.dash-area-name { flex:1; font-size:.85rem; color:var(--text1); }
+.dash-area-count { font-size:.75rem; color:var(--text2); white-space:nowrap; }
+.dash-area-bar { width:80px; height:6px; background:var(--border); border-radius:3px; overflow:hidden; }
+.dash-area-fill { height:100%; background:linear-gradient(90deg,var(--teal),#3498db); border-radius:3px; }
+.dash-24h-grid { display:grid; grid-template-columns:repeat(2,1fr); gap:.5rem; margin-top:.5rem; }
+.dash-24h-item { background:var(--card); border:1px solid var(--border); border-radius:.5rem; padding:.6rem; text-align:center; }
+.dash-24h-num { font-size:1.1rem; font-weight:800; color:var(--text1); }
+.dash-24h-lbl { font-size:.72rem; color:var(--text2); }
+
+/* ============================================================
+ 🎨 QUICK CARD COLORS
+ ============================================================ */
+/* — hospitals — */
+.q-hosp { border-color:rgba(26,102,133,.4); background:rgba(26,102,133,.08); }
+.q-hosp:hover { border-color:rgba(39,174,96,.6); box-shadow:0 8px 24px rgba(26,102,133,.25); }
+.q-hosp::before { background:linear-gradient(135deg,#1a6685,#27ae60); }
+/* — news — */
+.q-news { border-color:rgba(243,156,18,.35); background:rgba(243,156,18,.07); }
+.q-news:hover { border-color:rgba(211,84,0,.6); box-shadow:0 8px 24px rgba(243,156,18,.25); }
+.q-news::before { background:linear-gradient(135deg,#f39c12,#d35400); }
+/* — rides — */
+.q-rides { border-color:rgba(41,128,185,.35); background:rgba(41,128,185,.07); }
+.q-rides:hover { border-color:rgba(26,188,156,.6); box-shadow:0 8px 24px rgba(41,128,185,.25); }
+.q-rides::before{ background:linear-gradient(135deg,#2980b9,#1abc9c); }
+/* — weather — */
+.q-weather{ border-color:rgba(41,128,185,.35); background:rgba(142,68,173,.07); }
+.q-weather:hover{ border-color:rgba(142,68,173,.6); box-shadow:0 8px 24px rgba(142,68,173,.25); }
+.q-weather::before{ background:linear-gradient(135deg,#2980b9,#8e44ad); }
+/* — water — */
+.q-water { border-color:rgba(22,160,133,.35); background:rgba(22,160,133,.07); }
+.q-water:hover { border-color:rgba(22,160,133,.6); box-shadow:0 8px 24px rgba(22,160,133,.25); }
+.q-water::before{ background:linear-gradient(135deg,#2980b9,#16a085); }
+/* — study — */
+.q-study { border-color:rgba(142,68,173,.35); background:rgba(142,68,173,.07); }
+.q-study:hover { border-color:rgba(155,89,182,.6); box-shadow:0 8px 24px rgba(142,68,173,.25); }
+.q-study::before{ background:linear-gradient(135deg,#8e44ad,#9b59b6); }
+/* — hood — */
+.q-hood { border-color:rgba(76,175,80,.35); background:rgba(76,175,80,.07); }
+.q-hood:hover { border-color:rgba(76,175,80,.6); box-shadow:0 8px 24px rgba(76,175,80,.25); }
+.q-hood::before { background:linear-gradient(135deg,#388e3c,#4caf50); }
+/* — help — */
+.q-help { border-color:rgba(230,126,34,.35); background:rgba(230,126,34,.07); }
+.q-help:hover { border-color:rgba(231,76,60,.6); box-shadow:0 8px 24px rgba(230,126,34,.25); }
+.q-help::before { background:linear-gradient(135deg,#e67e22,#e74c3c); }
+/* — polls — */
+.q-polls { border-color:rgba(22,160,133,.35); background:rgba(22,160,133,.07); }
+.q-polls:hover { border-color:rgba(26,188,156,.6); box-shadow:0 8px 24px rgba(22,160,133,.25); }
+.q-polls::before{ background:linear-gradient(135deg,#16a085,#1abc9c); }
+/* — dashboard — */
+.q-dash { border-color:rgba(52,152,219,.35); background:rgba(52,152,219,.07); }
+.q-dash:hover { border-color:rgba(41,128,185,.6); box-shadow:0 8px 24px rgba(52,152,219,.25); }
+.q-dash::before { background:linear-gradient(135deg,#3498db,#2980b9); }
+/* — shared hover boost — */
+.q-hosp:hover,.q-news:hover,.q-rides:hover,.q-weather:hover,.q-water:hover,.q-hood:hover,
+.q-study:hover,.q-help:hover,.q-polls:hover,.q-dash:hover { transform:translateY(-3px) scale(1.02); }
+
+/* ============================================================
+ 🖼️ ADVANCED PROFILE
+ ============================================================ */
+.profile-cover { height:140px; background:linear-gradient(135deg,var(--teal),#3498db); position:relative; overflow:hidden; transition:background .4s; }
+.profile-cover::after { content:''; position:absolute; inset:0; background:repeating-linear-gradient(45deg,rgba(255,255,255,.03) 0 10px,transparent 10px 20px); pointer-events:none; }
+.profile-cover-overlay { position:absolute; inset:0; background:rgba(0,0,0,.15); }
+.profile-cover-actions { position:absolute; top:.6rem; left:.6rem; z-index:2; }
+.pcov-btn { background:rgba(0,0,0,.35); border:1px solid rgba(255,255,255,.2); color:#fff; border-radius:.5rem; padding:.3rem .6rem; font-size:.8rem; cursor:pointer; font-family:inherit; backdrop-filter:blur(4px); }
+.profile-identity-card { background:var(--card); border-radius:0 0 var(--r) var(--r); padding:0 1rem 1rem; position:relative; }
+.profile-avatar-zone { position:relative; display:inline-block; margin-top:-40px; }
+.profile-avatar-big { width:80px; height:80px; border-radius:50%; background:linear-gradient(135deg,var(--teal),#3498db); display:flex; align-items:center; justify-content:center; font-size:1.8rem; font-weight:900; color:#fff; border:3px solid var(--bg); box-shadow:0 4px 12px rgba(0,0,0,.25); cursor:pointer; user-select:none; transition:transform .2s; }
+.profile-avatar-big:hover { transform:scale(1.07); }
+.profile-avatar-ring { position:absolute; inset:-3px; border-radius:50%; border:2px solid var(--teal); animation:pulse-ring 2s ease-out infinite; }
+.avatar-emoji-hint { position:absolute; bottom:0; right:0; background:var(--teal); color:#fff; border-radius:50%; width:22px; height:22px; display:flex; align-items:center; justify-content:center; font-size:.7rem; pointer-events:none; }
+.profile-identity-info { display:flex; flex-direction:column; gap:.15rem; margin-top:.5rem; }
+.profile-hero-name { font-size:1.15rem; font-weight:800; color:var(--text1); }
+.profile-hero-job { font-size:.85rem; color:var(--teal); }
+.profile-hero-area { font-size:.82rem; color:var(--text2); }
+.pbl { background:rgba(26,188,156,.15); color:var(--teal); border-radius:1rem; padding:.1rem .5rem; font-size:.72rem; display:inline-block; }
+.pbd { background:rgba(46,204,113,.15); color:#27ae60; border-radius:1rem; padding:.1rem .5rem; font-size:.72rem; display:inline-block; }
+.profile-edit-fab { position:absolute; top:.6rem; right:.6rem; width:36px; height:36px; background:var(--teal); border-radius:50%; display:flex; align-items:center; justify-content:center; cursor:pointer; font-size:1rem; box-shadow:0 2px 8px rgba(26,188,156,.4); border:none; color:#fff; }
+.profile-stats-strip { display:flex; justify-content:space-around; padding:.8rem 0; border-top:1px solid var(--border); border-bottom:1px solid var(--border); margin:.6rem 0; }
+.pss-item { display:flex; flex-direction:column; align-items:center; gap:.2rem; }
+.pss-num { font-size:1.1rem; font-weight:700; color:var(--teal); }
+.pss-lbl { font-size:.72rem; color:var(--text2); }
+.pss-divider { width:1px; background:var(--border); }
+.profile-main-actions { display:flex; justify-content:space-around; padding:.5rem 0; margin-bottom:.5rem; }
+.pma-btn { display:flex; flex-direction:column; align-items:center; gap:.3rem; background:none; border:1px solid var(--border); border-radius:var(--r); padding:.5rem .4rem; cursor:pointer; font-family:inherit; transition:background .15s; min-width:52px; }
+.pma-btn:hover { background:rgba(255,255,255,.06); }
+.pma-icon { font-size:1.3rem; }
+.pma-lbl { font-size:.68rem; color:var(--text2); }
+.pma-msg { border-color:rgba(52,152,219,.3); }
+.pma-map { border-color:rgba(26,188,156,.3); }
+.pma-share { border-color:rgba(155,89,182,.3); }
+.pma-qr { border-color:rgba(241,196,15,.3); }
+.pma-sos { border-color:rgba(231,76,60,.4); }
+.pma-sos .pma-lbl { color:#e74c3c; font-weight:700; }
+.profile-info-block { background:var(--card); border:1px solid var(--border); border-radius:var(--r); margin-bottom:.7rem; overflow:hidden; }
+.pib-title { font-size:.8rem; font-weight:700; color:var(--teal); padding:.6rem .9rem; border-bottom:1px solid var(--border); background:rgba(26,188,156,.05); }
+.pib-row { display:flex; align-items:center; gap:.6rem; padding:.55rem .9rem; border-bottom:1px solid var(--border); }
+.pib-row:last-child { border-bottom:none; }
+.pib-icon { font-size:1rem; flex-shrink:0; width:1.4rem; text-align:center; }
+.pib-content { flex:1; min-width:0; }
+.pib-label { font-size:.72rem; color:var(--text2); display:block; }
+.pib-val { font-size:.88rem; color:var(--text1); }
+.pib-val-highlight { color:#27ae60; font-weight:700; }
+.pib-val-link { color:var(--teal); text-decoration:none; }
+.pib-val-link:hover { text-decoration:underline; }
+.pib-bio { font-size:.87rem; color:var(--text2); line-height:1.6; font-style:italic; }
+.pib-action-btn { background:none; border:1px solid var(--border); border-radius:.4rem; padding:.2rem .5rem; cursor:pointer; font-size:.78rem; font-family:inherit; color:var(--text2); margin-top:.2rem; }
+.profile-badges-block { background:var(--card); border:1px solid var(--border); border-radius:var(--r); padding:.8rem; margin-bottom:.7rem; }
+.profile-badges-grid { display:flex; flex-wrap:wrap; gap:.4rem; }
+.pbadge { background:rgba(26,188,156,.12); color:var(--teal); border-radius:1rem; padding:.25rem .65rem; font-size:.78rem; display:inline-flex; align-items:center; gap:.25rem; }
+.pbadge-gold { background:rgba(241,196,15,.15); color:#f39c12; }
+.pbadge-blue { background:rgba(52,152,219,.12); color:#3498db; }
+.pbadge-red { background:rgba(231,76,60,.12); color:#e74c3c; }
+.pbadge-purple { background:rgba(155,89,182,.12); color:#9b59b6; }
+.pbadge-green { background:rgba(39,174,96,.12); color:#27ae60; }
+.profile-edit-form { background:var(--card); border:1px solid var(--border); border-radius:var(--r); padding:1rem; margin-top:.8rem; }
+
+/* QR Modal */
+.qr-modal-overlay { position:fixed; inset:0; background:rgba(0,0,0,.75); z-index:9999; display:flex; align-items:center; justify-content:center; }
+.qr-modal-box { background:var(--card); border:1px solid var(--border); border-radius:var(--r); padding:1.5rem; text-align:center; max-width:280px; width:90%; }
+.qr-modal-name { font-size:1rem; font-weight:700; color:var(--text1); margin-bottom:.5rem; }
+.qr-modal-canvas { border:4px solid #fff; border-radius:.5rem; }
+.qr-modal-info { font-size:.82rem; color:var(--text2); margin:.6rem 0; }
+.qr-modal-close { background:var(--teal); color:#fff; border:none; border-radius:.5rem; padding:.5rem 1.2rem; cursor:pointer; font-family:inherit; font-size:.9rem; margin-top:.5rem; }
+
+/* SOS Modal */
+.sos-modal-overlay { position:fixed; inset:0; background:rgba(0,0,0,.8); z-index:9999; display:flex; align-items:center; justify-content:center; }
+.sos-modal-box { background:var(--card); border:2px solid rgba(231,76,60,.5); border-radius:var(--r); padding:1.5rem; max-width:320px; width:90%; text-align:center; }
+.sos-modal-title { font-size:1.2rem; font-weight:800; color:#e74c3c; margin-bottom:1rem; }
+.sos-btn-big { display:block; width:100%; padding:.8rem; border-radius:.6rem; border:none; cursor:pointer; font-size:.95rem; font-weight:700; font-family:inherit; margin-bottom:.5rem; }
+.sos-call-btn { background:rgba(231,76,60,.2); color:#e74c3c; border:1px solid rgba(231,76,60,.4); }
+.sos-report-btn { background:rgba(241,196,15,.2); color:#f39c12; border:1px solid rgba(241,196,15,.4); }
+.sos-share-btn { background:rgba(52,152,219,.2); color:#3498db; border:1px solid rgba(52,152,219,.4); }
+.sos-cancel-btn { background:rgba(255,255,255,.06); color:var(--text2); border:1px solid var(--border); }
+
+/* Prayer Times */
+.prayer-top { background:linear-gradient(135deg,rgba(39,174,96,.18),rgba(26,188,156,.08)); border-bottom:1px solid rgba(39,174,96,.2); }
+.prayer-top h2 { color:#27ae60; }
+.prayer-menu { color:#27ae60; }
+.prayer-countdown-card { background:var(--card); border:1px solid rgba(39,174,96,.3); border-radius:var(--r); padding:1.2rem; text-align:center; margin-bottom:1rem; }
+.prayer-next-label { font-size:.8rem; color:var(--text2); margin-bottom:.3rem; }
+.prayer-next-name { font-size:1.3rem; font-weight:800; color:#27ae60; margin-bottom:.3rem; }
+.prayer-countdown { font-size:2rem; font-weight:900; color:var(--text1); font-variant-numeric:tabular-nums; }
+.prayer-grid { display:grid; grid-template-columns:repeat(3,1fr); gap:.5rem; margin-bottom:1rem; }
+.prayer-card { background:var(--card); border:1px solid var(--border); border-radius:var(--r); padding:.7rem; text-align:center; }
+.prayer-card.active-prayer { border-color:#27ae60; background:rgba(39,174,96,.08); }
+.prayer-name { font-size:.82rem; color:var(--text2); margin-bottom:.2rem; }
+.prayer-time { font-size:1rem; font-weight:700; color:var(--text1); }
+.prayer-card.active-prayer .prayer-name { color:#27ae60; }
+.prayer-location-row { display:flex; align-items:center; justify-content:space-between; margin-top:.5rem; font-size:.82rem; color:var(--text2); flex-wrap:wrap; gap:.4rem; }
+.btn-prayer { background:rgba(39,174,96,.15); color:#27ae60; border:1px solid rgba(39,174,96,.3); border-radius:.5rem; padding:.3rem .6rem; cursor:pointer; font-size:.8rem; font-family:inherit; }
+
+/* ============================================================
+ QR MODAL & Profile QR
+ ============================================================ */
+.profile-qr-modal { position:fixed; inset:0; background:rgba(0,0,0,.8); z-index:9999; display:flex; align-items:center; justify-content:center; }
+.profile-qr-modal.hidden { display:none; }
+.pqm-inner { background:var(--card); border:1px solid var(--border); border-radius:var(--r); padding:1.5rem; text-align:center; max-width:300px; width:92%; position:relative; }
+.pqm-close { position:absolute; top:.5rem; left:.5rem; background:rgba(255,255,255,.08); border:none; color:var(--text2); border-radius:50%; width:28px; height:28px; cursor:pointer; font-size:.9rem; display:flex; align-items:center; justify-content:center; }
+.pqm-qr-wrap { display:inline-block; padding:.5rem; background:#0a0e1a; border-radius:.5rem; margin-bottom:.8rem; border:2px solid rgba(26,188,156,.3); }
+.pqm-name { font-size:1rem; font-weight:700; color:var(--text1); margin-bottom:.3rem; }
+.pqm-sub { font-size:.82rem; color:var(--text2); margin-bottom:.5rem; }
+
+/* ============================================================
+ SOS Enhanced
+ ============================================================ */
+.sos-pressing { animation:sos-pulse .3s ease-in-out infinite !important; }
+@keyframes sos-pulse { 0%,100%{transform:scale(1)} 50%{transform:scale(1.05)} }
+
+/* ============================================================
+ Study group chat messages fix
+ ============================================================ */
+.study-msg-me { align-self:flex-end; }
+.study-msg-bubble { background:rgba(26,188,156,.12); border:1px solid rgba(26,188,156,.2); border-radius:.6rem; padding:.4rem .7rem; font-size:.85rem; color:var(--text1); }
+.study-msg:not(.study-msg-me) .study-msg-bubble { background:var(--card); border-color:var(--border); }
+.study-msg-name { font-size:.72rem; color:var(--teal); margin-bottom:.2rem; }
+.study-msg-time { font-size:.7rem; color:var(--text2); margin-top:.2rem; text-align:right; }
+
+/* ===== MAP ENHANCEMENTS v2 ===== */
+.map-enhanced { height:55vh; min-height:300px; background:var(--dark3); position:relative; }
+.map-layers-btn {
+ background:var(--dark3); border:1px solid var(--border);
+ color:var(--text); padding:.5rem .7rem; border-radius:var(--rs);
+ cursor:pointer; font-size:1rem; transition:all .2s; flex-shrink:0;
+}
+.map-layers-btn:hover { background:rgba(26,188,156,.15); border-color:var(--teal) }
+.map-layers-panel {
+ position:relative; background:var(--dark2); border-bottom:1px solid var(--border);
+ padding:.8rem 1rem; z-index:60;
+}
+.mlp-title { font-weight:700; font-size:.88rem; color:var(--teal); margin-bottom:.6rem }
+.mlp-options { display:flex; gap:.8rem; flex-wrap:wrap; margin-bottom:.7rem }
+.mlp-opt { display:flex; align-items:center; gap:.4rem; font-size:.82rem; cursor:pointer; color:var(--text2) }
+.mlp-opt input[type=radio] { accent-color:var(--teal) }
+.mlp-toggles { display:flex; gap:.4rem; flex-wrap:wrap }
+.mlp-toggle {
+ background:var(--dark3); border:1px solid var(--border); color:var(--text2);
+ padding:.25rem .7rem; border-radius:20px; cursor:pointer; font-size:.78rem;
+ font-family:inherit; transition:all .2s;
+}
+.mlp-toggle.active { background:rgba(26,188,156,.15); border-color:var(--teal); color:var(--teal) }
+.map-refresh-btn {
+ background:var(--dark3); border:1px solid var(--border); color:var(--text2);
+ padding:.25rem .6rem; border-radius:var(--rs); cursor:pointer; font-size:.85rem;
+ font-family:inherit; transition:all .2s;
+}
+.map-refresh-btn:hover { background:rgba(26,188,156,.15); border-color:var(--teal) }
+
+/* FAB tools on map */
+.map-fab-tools {
+ display:none; /* hidden by default, shown when section active */
+ position:absolute; bottom:80px; left:.8rem; flex-direction:column; gap:.5rem; z-index:400;
+}
+#sec-map.active-sec .map-fab-tools { display:flex !important; }
+.map-fab-btn {
+ width:44px; height:44px; border-radius:50%; border:none; cursor:pointer;
+ font-size:1.1rem; display:flex; align-items:center; justify-content:center;
+ box-shadow:0 2px 8px rgba(0,0,0,.4); transition:all .2s; font-family:inherit;
+}
+.fab-report { background:#e74c3c; color:#fff }
+.fab-sos { background:#c0392b; color:#fff }
+.fab-locate { background:rgba(26,188,156,.9); color:#fff }
+.fab-layers { background:rgba(26,30,40,.9); color:var(--teal); border:1px solid var(--teal); }
+.map-fab-btn:hover { transform:scale(1.1) }
+
+/* Stat bar weather mini */
+#msb-weather-mini { display:flex; align-items:center; gap:.2rem; color:var(--yellow) }
+
+/* ===== NEWS CARD ENHANCED ===== */
+.news-card {
+ background:var(--card); border:1px solid var(--border); border-radius:var(--r);
+ padding:1rem; margin-bottom:.8rem; transition:all .2s;
+}
+.news-card:hover { border-color:rgba(243,156,18,.3); box-shadow:0 2px 12px rgba(0,0,0,.2) }
+.news-card-top { display:flex; justify-content:space-between; align-items:center; margin-bottom:.5rem }
+.news-cat { font-size:.75rem; background:rgba(243,156,18,.12); color:#f39c12; padding:.2rem .55rem; border-radius:20px; }
+.news-time { font-size:.72rem; color:var(--text2) }
+.news-title { font-weight:700; font-size:.96rem; margin-bottom:.4rem; line-height:1.4 }
+.news-body { font-size:.84rem; color:var(--text2); line-height:1.6; margin-bottom:.5rem }
+.news-footer { display:flex; gap:.8rem; font-size:.75rem; color:var(--text2); margin-bottom:.5rem }
+.news-source { color:var(--teal) }
+.news-cred-bar-wrap { display:flex; align-items:center; gap:.5rem; margin-bottom:.6rem }
+.news-cred-label { font-size:.72rem; color:var(--text2); white-space:nowrap }
+.news-cred-bar { flex:1; height:5px; background:rgba(255,255,255,.08); border-radius:3px; overflow:hidden }
+.news-cred-fill { height:100%; border-radius:3px; transition:width .6s }
+.news-cred-pct { font-size:.75rem; font-weight:700; width:2.5rem; text-align:left }
+.news-actions { display:flex; gap:.4rem; flex-wrap:wrap }
+.news-vote-btn { background:none; border:1px solid var(--border); border-radius:.5rem; padding:.25rem .55rem; cursor:pointer; font-size:.78rem; color:var(--text2); font-family:inherit; transition:all .2s }
+.news-vote-btn:hover { border-color:var(--teal); color:var(--teal) }
+.nv-up:hover { border-color:#1abc9c; color:#1abc9c }
+.nv-down:hover { border-color:#e74c3c; color:#e74c3c }
+
+/* ===== RIDE CARD ENHANCED ===== */
+.ride-card {
+ background:var(--card); border:1px solid var(--border); border-radius:var(--r);
+ padding:1rem; margin-bottom:.8rem;
+}
+.ride-route { display:flex; flex-direction:column; gap:.2rem; margin-bottom:.6rem }
+.ride-from,.ride-to { font-size:.92rem; font-weight:600 }
+.ride-arrow { text-align:center; color:var(--text2); font-size:.9rem }
+.ride-meta { display:flex; gap:.8rem; flex-wrap:wrap; font-size:.78rem; color:var(--text2); margin-bottom:.5rem }
+.ride-driver { display:flex; align-items:center; gap:.5rem; font-size:.84rem; margin-bottom:.6rem }
+.ride-call-btn { background:rgba(26,188,156,.15); border:1px solid rgba(26,188,156,.3); color:var(--teal); border-radius:20px; padding:.2rem .5rem; text-decoration:none; font-size:.78rem }
+.ride-actions { display:flex; gap:.4rem; flex-wrap:wrap }
+.ride-btn { background:var(--dark3); border:1px solid var(--border); color:var(--text2); padding:.35rem .75rem; border-radius:20px; cursor:pointer; font-size:.8rem; font-family:inherit; text-decoration:none; transition:all .2s }
+.ride-contact { background:rgba(26,188,156,.12); border-color:rgba(26,188,156,.3); color:var(--teal) }
+.ride-join { background:rgba(52,152,219,.12); border-color:rgba(52,152,219,.3); color:#3498db }
+.ride-full-badge { font-size:.78rem; color:#e74c3c; background:rgba(231,76,60,.1); border:1px solid rgba(231,76,60,.2); padding:.25rem .6rem; border-radius:20px }
+
+/* ===== WATER CARD ENHANCED ===== */
+.water-card {
+ background:var(--card); border:1px solid var(--border); border-radius:var(--r);
+ padding:1rem; margin-bottom:.8rem;
+}
+.water-card-top { display:flex; justify-content:space-between; align-items:center; margin-bottom:.5rem }
+.water-type { font-size:.78rem; padding:.25rem .6rem; border-radius:20px; font-weight:600 }
+.wtype-cut { background:rgba(231,76,60,.15); color:#e74c3c; border:1px solid rgba(231,76,60,.3) }
+.wtype-low { background:rgba(52,152,219,.15); color:#3498db; border:1px solid rgba(52,152,219,.3) }
+.wtype-dirty { background:rgba(155,89,182,.15); color:#9b59b6; border:1px solid rgba(155,89,182,.3) }
+.wtype-dist { background:rgba(26,188,156,.15); color:var(--teal); border:1px solid rgba(26,188,156,.3) }
+.water-time { font-size:.72rem; color:var(--text2) }
+.water-area { font-size:.88rem; font-weight:600; margin-bottom:.3rem }
+.water-notes { font-size:.82rem; color:var(--text2); margin-bottom:.4rem }
+.water-meta { display:flex; align-items:center; gap:.8rem; font-size:.76rem; color:var(--text2); margin-bottom:.5rem }
+.water-map-btn { background:none; border:1px solid var(--border); color:var(--teal); border-radius:20px; padding:.15rem .5rem; cursor:pointer; font-size:.75rem; font-family:inherit }
+.water-votes { display:flex; gap:.4rem }
+.water-vote-sm { background:none; border:1px solid var(--border); padding:.25rem .6rem; border-radius:20px; cursor:pointer; font-size:.78rem; font-family:inherit; color:var(--text2); transition:all .2s }
+.wv-yes:hover { border-color:#1abc9c; color:#1abc9c }
+.wv-no:hover { border-color:#e74c3c; color:#e74c3c }
+
+/* ===== HELP CARD ENHANCED ===== */
+.help-card {
+ background:var(--card); border:1px solid var(--border); border-radius:var(--r);
+ padding:1rem; margin-bottom:.8rem; transition:all .2s;
+}
+.help-urgent { border-color:rgba(231,76,60,.4); background:rgba(231,76,60,.04) }
+.help-card-top { display:flex; align-items:center; gap:.4rem; flex-wrap:wrap; margin-bottom:.5rem }
+.help-type-badge { font-size:.75rem; padding:.2rem .5rem; border-radius:20px; }
+.ht-food { background:rgba(243,156,18,.12); color:#f39c12; border:1px solid rgba(243,156,18,.3) }
+.ht-medicine { background:rgba(231,76,60,.12); color:#e74c3c; border:1px solid rgba(231,76,60,.3) }
+.ht-transport { background:rgba(52,152,219,.12); color:#3498db; border:1px solid rgba(52,152,219,.3) }
+.ht-shelter { background:rgba(46,204,113,.12); color:#2ecc71; border:1px solid rgba(46,204,113,.3) }
+.ht-money { background:rgba(241,196,15,.12); color:#f1c40f; border:1px solid rgba(241,196,15,.3) }
+.ht-other { background:rgba(26,188,156,.12); color:var(--teal); border:1px solid rgba(26,188,156,.3) }
+.help-badge-urgent { font-size:.72rem; background:rgba(231,76,60,.15); color:#e74c3c; border:1px solid rgba(231,76,60,.3); padding:.15rem .45rem; border-radius:20px }
+.help-badge-closed { font-size:.72rem; background:rgba(46,204,113,.15); color:#2ecc71; border:1px solid rgba(46,204,113,.3); padding:.15rem .45rem; border-radius:20px }
+.help-time { font-size:.72rem; color:var(--text2); margin-right:auto }
+.help-title { font-weight:700; font-size:.94rem; margin-bottom:.3rem }
+.help-desc { font-size:.82rem; color:var(--text2); line-height:1.5; margin-bottom:.4rem }
+.help-meta { font-size:.76rem; color:var(--text2); margin-bottom:.5rem }
+.help-actions { display:flex; gap:.4rem; flex-wrap:wrap }
+.help-btn { background:var(--dark3); border:1px solid var(--border); color:var(--text2); padding:.3rem .7rem; border-radius:20px; cursor:pointer; font-size:.78rem; font-family:inherit; text-decoration:none; transition:all .2s }
+.help-call { background:rgba(26,188,156,.12); border-color:rgba(26,188,156,.3); color:var(--teal) }
+.help-offer { background:rgba(52,152,219,.12); border-color:rgba(52,152,219,.3); color:#3498db }
+
+/* ===== POLL CARD ENHANCED ===== */
+.poll-card {
+ background:var(--card); border:1px solid var(--border); border-radius:var(--r);
+ padding:1rem; margin-bottom:.8rem;
+}
+.poll-card-top { display:flex; justify-content:space-between; align-items:flex-start; gap:.5rem; margin-bottom:.7rem }
+.poll-q { font-weight:700; font-size:.94rem; line-height:1.5; flex:1 }
+.poll-badge-expired { font-size:.7rem; background:rgba(231,76,60,.12); color:#e74c3c; border:1px solid rgba(231,76,60,.3); padding:.2rem .5rem; border-radius:20px; white-space:nowrap }
+.poll-opt-row {
+ display:grid; grid-template-columns:1fr auto auto; gap:.4rem; align-items:center;
+ padding:.45rem .5rem; border-radius:.5rem; margin-bottom:.35rem;
+ border:1px solid var(--border); transition:all .2s; position:relative; overflow:hidden;
+}
+.poll-opt-row.poll-clickable { cursor:pointer }
+.poll-opt-row.poll-clickable:hover { border-color:var(--teal); background:rgba(26,188,156,.06) }
+.poll-opt-row.poll-winner { border-color:rgba(241,196,15,.4) }
+.poll-opt-text { font-size:.84rem; z-index:1 }
+.poll-bar { position:absolute; left:0; top:0; height:100%; z-index:0; border-radius:.5rem }
+.poll-bar-fill { height:100%; border-radius:.5rem; transition:width .6s ease; opacity:.18 }
+.poll-opt-pct { font-size:.78rem; font-weight:700; color:var(--text2); z-index:1; min-width:2.8rem; text-align:left }
+.poll-footer { display:flex; gap:.8rem; font-size:.74rem; color:var(--text2); margin-top:.5rem; padding-top:.5rem; border-top:1px solid var(--border) }
+.poll-total { color:var(--teal); font-weight:600 }
+
+/* ===== STUDY CARD ENHANCED ===== */
+.study-card {
+ background:var(--card); border:1px solid var(--border); border-radius:var(--r);
+ padding:1rem; margin-bottom:.8rem;
+}
+.study-card-top { display:flex; justify-content:space-between; align-items:center; margin-bottom:.5rem }
+.study-level-badge { font-size:.75rem; background:rgba(155,89,182,.12); color:#9b59b6; border:1px solid rgba(155,89,182,.3); padding:.2rem .5rem; border-radius:20px }
+.study-members { font-size:.75rem; color:var(--text2) }
+.study-name { font-weight:700; font-size:.96rem; margin-bottom:.3rem }
+.study-subject { font-size:.84rem; color:var(--teal); margin-bottom:.25rem }
+.study-schedule { font-size:.78rem; color:var(--text2); margin-bottom:.2rem }
+.study-area { font-size:.76rem; color:var(--text2); margin-bottom:.4rem }
+.study-progress-bar { margin-bottom:.6rem }
+.study-footer { display:flex; gap:.4rem; flex-wrap:wrap }
+.study-contact-btn { background:rgba(26,188,156,.12); border:1px solid rgba(26,188,156,.3); color:var(--teal); padding:.3rem .7rem; border-radius:20px; cursor:pointer; font-size:.78rem; text-decoration:none; font-family:inherit }
+.study-join-btn { background:rgba(52,152,219,.12); border:1px solid rgba(52,152,219,.3); color:#3498db; padding:.3rem .7rem; border-radius:20px; cursor:pointer; font-size:.78rem; font-family:inherit; transition:all .2s }
+.study-join-btn.study-joined { background:rgba(46,204,113,.12); border-color:rgba(46,204,113,.3); color:#2ecc71; cursor:default }
+.study-chat-btn { background:rgba(155,89,182,.12); border:1px solid rgba(155,89,182,.3); color:#9b59b6; padding:.3rem .7rem; border-radius:20px; cursor:pointer; font-size:.78rem; font-family:inherit }
+
+/* ===== DASHBOARD ENHANCED ===== */
+.dash-stat-card {
+ background:var(--card); border:1px solid var(--border); border-radius:var(--r);
+ padding:.9rem .8rem; text-align:center; transition:all .2s;
+}
+.dash-stat-card:hover { transform:translateY(-2px); box-shadow:0 4px 16px rgba(0,0,0,.2) }
+.dsc-num { font-size:1.6rem; font-weight:800; margin-bottom:.2rem }
+.dsc-lbl { font-size:.74rem; color:var(--text2) }
+
+.dash-area-row { display:flex; align-items:center; gap:.6rem; margin-bottom:.6rem }
+.dar-label { font-size:.84rem; white-space:nowrap; min-width:100px }
+.dar-bar-wrap { flex:1; height:6px; background:rgba(255,255,255,.07); border-radius:3px; overflow:hidden }
+.dar-bar { height:100%; background:var(--teal); border-radius:3px; transition:width .6s }
+.dar-count { font-size:.8rem; color:var(--teal); font-weight:700; min-width:2rem; text-align:left }
+
+/* ===== HOSPITAL BADGE ===== */
+.badge-emergency {
+ font-size:.68rem; background:rgba(231,76,60,.15); color:#e74c3c;
+ border:1px solid rgba(231,76,60,.3); padding:.1rem .4rem; border-radius:20px; margin-right:.3rem
+}
+.hosp-rating { font-size:.8rem; color:#f39c12 }
+.hosp-dist { font-size:.75rem; color:var(--text2); font-weight:600; white-space:nowrap }
+.hosp-meta { font-size:.78rem; color:var(--text2); margin:.4rem 0 .5rem }
+.hosp-addr,.hosp-phone { margin-bottom:.15rem }
+
+
+/* ===================================================================
+ FEATURE SEARCH BAR - شريط البحث الموحد للمميزات
+=================================================================== */
+.feature-search-bar {
+ background: var(--card);
+ border: 1px solid var(--border);
+ border-radius: var(--r);
+ padding: .7rem .8rem;
+ margin-bottom: .8rem;
+}
+.fsb-row {
+ display: flex;
+ align-items: center;
+ gap: .4rem;
+ background: var(--dark3);
+ border: 1px solid var(--border);
+ border-radius: var(--rs);
+ padding: .35rem .7rem;
+ transition: border-color .2s;
+}
+.fsb-row:focus-within { border-color: var(--teal); box-shadow: 0 0 0 3px rgba(26,188,156,.1); }
+.fsb-icon { font-size: 1rem; flex-shrink: 0; opacity: .7; }
+.fsb-inp {
+ flex: 1;
+ background: none;
+ border: none;
+ color: var(--text);
+ font-family: inherit;
+ font-size: .88rem;
+ outline: none;
+ min-width: 80px;
+}
+.fsb-inp::placeholder { color: var(--text2); }
+.fsb-sel {
+ background: var(--dark3);
+ border: 1px solid var(--border);
+ color: var(--text);
+ font-family: inherit;
+ font-size: .8rem;
+ padding: .3rem .5rem;
+ border-radius: var(--rs);
+ outline: none;
+ cursor: pointer;
+}
+.fsb-btns {
+ display: flex;
+ gap: .4rem;
+ margin-top: .5rem;
+ flex-wrap: wrap;
+}
+.fsb-btn {
+ background: var(--dark3);
+ border: 1px solid var(--border);
+ color: var(--text2);
+ padding: .35rem .8rem;
+ border-radius: 20px;
+ cursor: pointer;
+ font-family: inherit;
+ font-size: .78rem;
+ transition: all .2s;
+ white-space: nowrap;
+}
+.fsb-btn:hover { background: rgba(26,188,156,.1); border-color: var(--teal); color: var(--teal); }
+.fsb-primary { background: rgba(26,188,156,.15); border-color: rgba(26,188,156,.4); color: var(--teal); }
+.fsb-add { background: rgba(52,152,219,.1); border-color: rgba(52,152,219,.3); color: #3498db; }
+.fsb-date-inp {
+ background: var(--dark3);
+ border: 1px solid var(--border);
+ color: var(--text);
+ font-family: inherit;
+ font-size: .78rem;
+ padding: .3rem .5rem;
+ border-radius: var(--rs);
+ outline: none;
+}
+
+/* ===================================================================
+ MAP FAB TOOLS - أدوات الخريطة العائمة
+=================================================================== */
+.map-fab-tools {
+ position: absolute;
+ bottom: 12px;
+ left: 12px;
+ z-index: 500;
+ display: flex;
+ flex-direction: column;
+ gap: .45rem;
+}
+.map-fab-btn {
+ width: 44px;
+ height: 44px;
+ border-radius: 50%;
+ border: none;
+ cursor: pointer;
+ font-size: 1.1rem;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ transition: all .2s;
+ box-shadow: 0 3px 10px rgba(0,0,0,.4);
+ font-family: inherit;
+}
+.fab-report { background: rgba(231,76,60,.9); color: #fff; }
+.fab-sos { background: rgba(231,76,60,.8); color: #fff; animation: hb 2s ease-in-out infinite; }
+.fab-locate { background: rgba(26,188,156,.9); color: #fff; }
+.fab-layers { background: rgba(52,73,94,.9); color: #fff; }
+.map-fab-btn:hover { transform: scale(1.1); }
+
+/* ===================================================================
+ MAP POPUP ENHANCED - نوافذ الخريطة المحسّنة
+=================================================================== */
+.custom-popup-inner { padding: .1rem 0; }
+.popup-title { font-size: .88rem; font-weight: 700; color: var(--text); margin-bottom: .3rem; line-height: 1.4; }
+.popup-area { font-size: .78rem; color: var(--text2); margin-bottom: .2rem; }
+.popup-votes { font-size: .75rem; color: var(--text2); margin-bottom: .3rem; }
+.popup-dist { font-size: .75rem; color: var(--teal); margin-bottom: .4rem; }
+.popup-actions { display: flex; gap: .3rem; flex-wrap: wrap; margin-top: .4rem; }
+
+/* ===================================================================
+ MAP LAYERS PANEL - لوحة طبقات الخريطة
+=================================================================== */
+.map-layers-panel {
+ position: absolute;
+ top: 108px;
+ left: 8px;
+ z-index: 600;
+ background: var(--dark2);
+ border: 1px solid var(--border);
+ border-radius: var(--r);
+ padding: .8rem;
+ min-width: 210px;
+ box-shadow: var(--sh);
+}
+.mlp-title { font-size: .82rem; font-weight: 700; color: var(--teal); margin-bottom: .6rem; }
+.mlp-options { display: flex; flex-direction: column; gap: .35rem; margin-bottom: .7rem; }
+.mlp-opt { display: flex; align-items: center; gap: .5rem; font-size: .82rem; color: var(--text); cursor: pointer; }
+.mlp-opt input { accent-color: var(--teal); }
+.mlp-toggles { display: flex; gap: .4rem; flex-wrap: wrap; }
+.mlp-toggle {
+ background: var(--dark3);
+ border: 1px solid var(--border);
+ color: var(--text2);
+ padding: .25rem .6rem;
+ border-radius: 20px;
+ cursor: pointer;
+ font-size: .75rem;
+ transition: all .2s;
+ font-family: inherit;
+}
+.mlp-toggle.active { background: rgba(26,188,156,.15); border-color: var(--teal); color: var(--teal); }
+
+/* ===================================================================
+ MAP ENHANCED SIZE & STYLE
+=================================================================== */
+.map-enhanced { height: 55vh; min-height: 300px; background: var(--dark3); position: relative; }
+.map-refresh-btn {
+ background: var(--dark3);
+ border: 1px solid var(--border);
+ color: var(--text2);
+ padding: .3rem .55rem;
+ border-radius: var(--rs);
+ cursor: pointer;
+ font-size: .9rem;
+ transition: all .2s;
+}
+.map-refresh-btn:hover { color: var(--teal); border-color: var(--teal); }
+.map-layers-btn {
+ background: var(--dark3);
+ border: 1px solid var(--border);
+ color: var(--text2);
+ padding: .45rem .75rem;
+ border-radius: var(--rs);
+ cursor: pointer;
+ font-size: 1rem;
+ transition: all .2s;
+}
+.map-layers-btn:hover { background: rgba(26,188,156,.1); border-color: var(--teal); }
+
+/* ===================================================================
+ IN-APP NOTIFICATIONS - إشعارات داخل التطبيق
+=================================================================== */
+.in-app-notif {
+ display: flex;
+ align-items: flex-start;
+ gap: .7rem;
+ background: var(--dark2);
+ border: 1px solid var(--border);
+ border-radius: var(--r);
+ padding: .7rem .9rem;
+ box-shadow: 0 4px 20px rgba(0,0,0,.4);
+ pointer-events: all;
+ cursor: pointer;
+ max-width: 360px;
+ width: 100%;
+ animation: slideInDown .3s ease-out;
+ border-right: 3px solid var(--teal);
+}
+.ian-icon { font-size: 1.3rem; flex-shrink: 0; }
+.ian-content { flex: 1; min-width: 0; }
+.ian-title { font-size: .85rem; font-weight: 700; color: var(--text); }
+.ian-body { font-size: .78rem; color: var(--text2); margin-top: .15rem; }
+.ian-close { background: none; border: none; color: var(--text2); cursor: pointer; font-size: .9rem; padding: .1rem .3rem; flex-shrink: 0; }
+@keyframes slideInDown {
+ from { opacity: 0; transform: translateY(-16px); }
+ to { opacity: 1; transform: translateY(0); }
+}
+
+/* ===================================================================
+ HELP FEATURES - ميزات طلبات المساعدة
+=================================================================== */
+.filt-urgent { background: rgba(231,76,60,.1); border-color: rgba(231,76,60,.25); color: #e74c3c; }
+.help-map-btn {
+ background: rgba(52,152,219,.1);
+ border: 1px solid rgba(52,152,219,.3);
+ color: #3498db;
+ padding: .3rem .65rem;
+ border-radius: 20px;
+ cursor: pointer;
+ font-size: .78rem;
+ font-family: inherit;
+ transition: all .2s;
+}
+.help-close-btn {
+ background: rgba(149,165,166,.1);
+ border: 1px solid rgba(149,165,166,.2);
+ color: #95a5a6;
+ padding: .3rem .65rem;
+ border-radius: 20px;
+ cursor: pointer;
+ font-size: .78rem;
+ font-family: inherit;
+}
+
+/* ===================================================================
+ NEWS BOOKMARK - حفظ الأخبار
+=================================================================== */
+.news-bookmark-btn {
+ background: none;
+ border: 1px solid var(--border);
+ color: var(--text2);
+ padding: .25rem .5rem;
+ border-radius: .5rem;
+ cursor: pointer;
+ font-size: .8rem;
+ transition: all .2s;
+}
+.news-bookmark-btn:hover { color: #f39c12; border-color: #f39c12; }
+
+/* ===================================================================
+ RIDES MAP BUTTON
+=================================================================== */
+.ride-map-btn {
+ background: rgba(26,188,156,.1);
+ border: 1px solid rgba(26,188,156,.25);
+ color: var(--teal);
+ padding: .3rem .65rem;
+ border-radius: 20px;
+ cursor: pointer;
+ font-size: .78rem;
+ font-family: inherit;
+ transition: all .2s;
+}
+
+/* ===================================================================
+ POLLS SHARE BUTTON
+=================================================================== */
+.poll-share-btn {
+ background: none;
+ border: 1px solid var(--border);
+ color: var(--text2);
+ padding: .25rem .6rem;
+ border-radius: 20px;
+ cursor: pointer;
+ font-size: .78rem;
+ font-family: inherit;
+ margin-top: .4rem;
+ transition: all .2s;
+}
+.poll-share-btn:hover { border-color: var(--teal); color: var(--teal); }
+
+/* ===================================================================
+ WATER MAP BUTTON
+=================================================================== */
+.water-map-btn {
+ background: rgba(52,152,219,.1);
+ border: 1px solid rgba(52,152,219,.25);
+ color: #3498db;
+ padding: .3rem .65rem;
+ border-radius: 20px;
+ cursor: pointer;
+ font-size: .78rem;
+ font-family: inherit;
+}
+
+/* ===================================================================
+ STUDY LEVEL FILTER ACTIVE STATE
+=================================================================== */
+.study-level-filt.active {
+ background: rgba(155,89,182,.15);
+ border-color: rgba(155,89,182,.4);
+ color: #9b59b6;
+}
+
+/* ===================================================================
+ HOSPITAL NEAR ME BUTTON
+=================================================================== */
+.hosp-near-btn {
+ background: rgba(26,188,156,.12);
+ border: 1px solid rgba(26,188,156,.3);
+ color: var(--teal);
+ padding: .3rem .7rem;
+ border-radius: 20px;
+ cursor: pointer;
+ font-size: .78rem;
+ font-family: inherit;
+}
+
+/* ===================================================================
+ RESPONSIVE IMPROVEMENTS
+=================================================================== */
+@media (max-width: 400px) {
+ .map-enhanced { height: 48vh; }
+ .map-fab-tools { bottom: 60px; }
+ .map-layers-panel { min-width: 180px; }
+ .fsb-row { padding: .3rem .5rem; }
+}
+
+/* ===================================================================
+ SECTION TRANSITIONS
+=================================================================== */
+.section { transition: opacity .18s ease; }
+/* pointer-events restored: sections handle clicks normally */
+
+/* ================================================================
+ 🎓 GROUP PAGE - صفحة المجموعة المستقلة
+================================================================ */
+.group-page {
+ position: fixed;
+ inset: 0;
+ background: var(--dark1, #0a0e1a);
+ z-index: 1000;
+ display: flex;
+ flex-direction: column;
+ overflow: hidden;
+ transition: transform .28s cubic-bezier(.4,0,.2,1), opacity .28s;
+}
+.group-page.hidden { display: none; }
+.group-page.sliding-in { transform: translateX(100%); }
+.group-page.sliding-out { transform: translateX(100%); opacity: 0; }
+
+/* Header */
+.gp-header {
+ display: flex; align-items: center; gap: .6rem;
+ padding: .7rem 1rem;
+ background: var(--dark2, #111827);
+ border-bottom: 1px solid var(--border, rgba(255,255,255,.08));
+ min-height: 58px;
+ flex-shrink: 0;
+}
+.gp-back {
+ background: none; border: none; color: var(--teal, #1abc9c);
+ font-size: .9rem; cursor: pointer; font-family: inherit;
+ padding: .3rem .5rem; border-radius: var(--rs,.5rem);
+ white-space: nowrap; flex-shrink: 0;
+}
+.gp-back:hover { background: rgba(26,188,156,.12); }
+.gp-title-wrap { display: flex; align-items: center; gap: .6rem; flex: 1; min-width: 0; }
+.gp-avatar {
+ width: 40px; height: 40px; border-radius: 50%;
+ background: linear-gradient(135deg,#9b59b6,#1abc9c);
+ display: flex; align-items: center; justify-content: center;
+ font-size: 1.25rem; flex-shrink: 0;
+}
+.gp-name { font-weight: 700; font-size: .95rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
+.gp-meta { font-size: .72rem; color: var(--text2, #8892a4); }
+.gp-actions-top { display: flex; gap: .3rem; flex-shrink: 0; }
+.gp-icon-btn {
+ width: 36px; height: 36px; border-radius: 50%;
+ background: rgba(255,255,255,.06); border: 1px solid var(--border,rgba(255,255,255,.08));
+ cursor: pointer; font-size: 1rem;
+ display: flex; align-items: center; justify-content: center;
+ transition: all .2s;
+}
+.gp-icon-btn:hover { background: rgba(26,188,156,.15); border-color: var(--teal,#1abc9c); }
+
+/* Tabs */
+.gp-tabs {
+ display: flex; background: var(--dark2,#111827);
+ border-bottom: 1px solid var(--border,rgba(255,255,255,.08));
+ flex-shrink: 0; overflow-x: auto; scrollbar-width: none;
+}
+.gp-tabs::-webkit-scrollbar { display: none; }
+.gp-tab {
+ flex: 1; min-width: max-content; padding: .65rem .8rem;
+ background: none; border: none; color: var(--text2,#8892a4);
+ cursor: pointer; font-family: inherit; font-size: .82rem;
+ border-bottom: 2px solid transparent; transition: all .2s;
+ white-space: nowrap;
+}
+.active-gp-tab { color: var(--teal,#1abc9c); border-bottom-color: var(--teal,#1abc9c); }
+.gp-tab-content { flex: 1; overflow-y: auto; display: flex; flex-direction: column; }
+.gp-tab-content.hidden { display: none; }
+
+/* Active Call Banner */
+.gp-call-banner {
+ display: flex; align-items: center; justify-content: space-between;
+ padding: .5rem 1rem; background: rgba(26,188,156,.12);
+ border-bottom: 1px solid rgba(26,188,156,.3); flex-shrink: 0;
+}
+.gp-call-banner.hidden { display: none; }
+.gcb-info { display: flex; align-items: center; gap: .5rem; font-size: .84rem; }
+.gcb-pulse {
+ width: 8px; height: 8px; border-radius: 50%; background: var(--teal,#1abc9c);
+ animation: callPulse 1.2s ease-in-out infinite;
+}
+@keyframes callPulse { 0%,100%{opacity:1;transform:scale(1)} 50%{opacity:.5;transform:scale(1.3)} }
+.gcb-time { font-size: .75rem; color: var(--text2,#8892a4); }
+.gcb-btns { display: flex; gap: .4rem; }
+.gcb-btn {
+ width: 34px; height: 34px; border-radius: 50%; border: none; cursor: pointer;
+ background: rgba(255,255,255,.1); font-size: .9rem;
+ display: flex; align-items: center; justify-content: center; transition: all .2s;
+}
+.gcb-btn:hover { background: rgba(255,255,255,.18); }
+.gcb-end { background: rgba(231,76,60,.2) !important; }
+.gcb-end:hover { background: rgba(231,76,60,.4) !important; }
+
+/* Video Grid */
+.gp-video-grid {
+ display: flex; flex-wrap: wrap; gap: .3rem; padding: .5rem;
+ background: #000; flex-shrink: 0;
+}
+.gp-video-grid.hidden { display: none; }
+.gp-video-self {
+ width: 120px; height: 90px; border-radius: .5rem;
+ object-fit: cover; border: 2px solid var(--teal,#1abc9c);
+}
+.gp-remote-videos { display: flex; flex-wrap: wrap; gap: .3rem; flex: 1; }
+.gp-video-remote-item {
+ flex: 1; min-width: 120px; height: 160px; border-radius: .5rem;
+ object-fit: cover; background: #111; border: 1px solid rgba(255,255,255,.1);
+}
+.gp-video-remote { flex: 1; min-width: 200px; height: 200px; border-radius: .5rem; object-fit: cover; background: #111; }
+
+/* Chat Messages */
+.gp-chat-messages {
+ flex: 1; overflow-y: auto; padding: .8rem;
+ display: flex; flex-direction: column; gap: .5rem;
+ scrollbar-width: thin; scrollbar-color: rgba(255,255,255,.1) transparent;
+}
+.gp-msg {
+ display: flex; flex-direction: column; max-width: 82%; gap: .15rem;
+ animation: msgIn .18s ease;
+}
+@keyframes msgIn { from{opacity:0;transform:translateY(6px)} to{opacity:1;transform:none} }
+.gp-msg.gp-msg-me { align-self: flex-start; }
+.gp-msg.gp-msg-other { align-self: flex-end; }
+.gp-msg-header { display: flex; align-items: center; gap: .4rem; font-size: .72rem; color: var(--text2,#8892a4); }
+.gp-msg-avatar {
+ width: 24px; height: 24px; border-radius: 50%;
+ background: linear-gradient(135deg,#9b59b6,#3498db);
+ display: flex; align-items: center; justify-content: center;
+ font-size: .7rem; flex-shrink: 0;
+}
+.gp-msg-bubble {
+ padding: .55rem .8rem; border-radius: 1rem;
+ font-size: .88rem; line-height: 1.5; word-break: break-word;
+ position: relative;
+}
+.gp-msg-me .gp-msg-bubble {
+ background: rgba(26,188,156,.18); border: 1px solid rgba(26,188,156,.25);
+ border-bottom-left-radius: .2rem;
+}
+.gp-msg-other .gp-msg-bubble {
+ background: var(--card, rgba(255,255,255,.06)); border: 1px solid var(--border,rgba(255,255,255,.08));
+ border-bottom-right-radius: .2rem;
+}
+.gp-msg-time { font-size: .65rem; color: var(--text2,#8892a4); padding: 0 .3rem; }
+.gp-msg-reactions {
+ display: flex; flex-wrap: wrap; gap: .2rem; margin-top: .2rem;
+}
+.gp-reaction-chip {
+ background: rgba(255,255,255,.07); border: 1px solid rgba(255,255,255,.12);
+ border-radius: 20px; padding: .12rem .4rem; font-size: .78rem; cursor: pointer;
+ transition: all .15s;
+}
+.gp-reaction-chip:hover { background: rgba(26,188,156,.15); }
+
+/* Reply preview inside message */
+.gp-msg-reply-quote {
+ font-size: .75rem; color: var(--text2,#8892a4);
+ border-right: 2px solid var(--teal,#1abc9c);
+ padding: .2rem .5rem; margin-bottom: .3rem;
+ border-radius: .3rem; background: rgba(255,255,255,.04);
+}
+
+/* Media in messages */
+.gp-msg-image { max-width: 220px; border-radius: .6rem; cursor: pointer; display: block; margin: .3rem 0; }
+.gp-msg-video { max-width: 220px; border-radius: .6rem; display: block; margin: .3rem 0; }
+.gp-msg-audio {
+ display: flex; align-items: center; gap: .5rem;
+ background: rgba(255,255,255,.06); padding: .4rem .7rem;
+ border-radius: .8rem; min-width: 160px;
+}
+.gma-play {
+ width: 32px; height: 32px; border-radius: 50%;
+ background: var(--teal,#1abc9c); border: none; cursor: pointer;
+ font-size: .9rem; display: flex; align-items: center; justify-content: center;
+}
+.gma-wave { flex: 1; height: 24px; display: flex; align-items: center; gap: 1px; }
+.gma-bar { width: 2px; background: var(--teal,#1abc9c); border-radius: 1px; opacity: .7; }
+.gma-dur { font-size: .72rem; color: var(--text2,#8892a4); }
+
+/* Typing indicator */
+.gp-typing {
+ display: flex; align-items: center; gap: .4rem;
+ padding: .4rem .8rem; font-size: .78rem; color: var(--text2,#8892a4);
+ flex-shrink: 0;
+}
+.gp-typing.hidden { display: none; }
+.gp-typing span {
+ width: 5px; height: 5px; border-radius: 50%; background: var(--text2,#8892a4);
+ animation: typingDot 1.2s ease-in-out infinite;
+}
+.gp-typing span:nth-child(2) { animation-delay: .2s; }
+.gp-typing span:nth-child(3) { animation-delay: .4s; }
+@keyframes typingDot { 0%,60%,100%{transform:none;opacity:.4} 30%{transform:translateY(-5px);opacity:1} }
+
+/* Reply preview bar */
+.gp-reply-preview {
+ display: flex; align-items: center; gap: .5rem;
+ padding: .5rem .8rem; background: rgba(26,188,156,.06);
+ border-top: 1px solid rgba(26,188,156,.2); flex-shrink: 0;
+}
+.gp-reply-preview.hidden { display: none; }
+.grp-content { flex: 1; }
+.grp-label { font-size: .7rem; color: var(--teal,#1abc9c); margin-bottom: .1rem; }
+.grp-text { font-size: .8rem; color: var(--text2,#8892a4); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
+.grp-cancel { background: none; border: none; color: var(--text2,#8892a4); cursor: pointer; font-size: 1rem; }
+
+/* Input Bar */
+.gp-input-bar {
+ display: flex; align-items: flex-end; gap: .35rem;
+ padding: .6rem .8rem; background: var(--dark2,#111827);
+ border-top: 1px solid var(--border,rgba(255,255,255,.08));
+ flex-shrink: 0;
+}
+.gpi-btn {
+ width: 36px; height: 36px; border-radius: 50%; border: none;
+ background: rgba(255,255,255,.06); cursor: pointer; font-size: 1rem;
+ display: flex; align-items: center; justify-content: center;
+ transition: all .2s; flex-shrink: 0;
+}
+.gpi-btn:hover { background: rgba(26,188,156,.15); }
+.gpi-text-wrap { flex: 1; position: relative; }
+.gpi-textarea {
+ width: 100%; background: rgba(255,255,255,.06);
+ border: 1px solid var(--border,rgba(255,255,255,.08));
+ border-radius: 1.2rem; color: var(--text, #e8ecf4);
+ padding: .55rem .9rem; font-family: inherit; font-size: .88rem;
+ resize: none; outline: none; max-height: 120px; overflow-y: auto;
+ transition: border-color .2s; line-height: 1.5;
+}
+.gpi-textarea:focus { border-color: var(--teal,#1abc9c); }
+.gpi-textarea::placeholder { color: var(--text2,#8892a4); }
+.gpi-send {
+ width: 38px; height: 38px; border-radius: 50%; border: none;
+ background: var(--teal,#1abc9c); color: #fff; cursor: pointer;
+ font-size: 1.1rem; display: flex; align-items: center; justify-content: center;
+ transition: all .2s; flex-shrink: 0;
+}
+.gpi-send:hover { background: #16a085; transform: scale(1.08); }
+
+/* Emoji Picker */
+.gp-emoji-picker {
+ background: var(--dark2,#111827); border-top: 1px solid var(--border,rgba(255,255,255,.08));
+ padding: .6rem; flex-shrink: 0; max-height: 160px; overflow-y: auto;
+}
+.gp-emoji-picker.hidden { display: none; }
+.gep-grid { display: flex; flex-wrap: wrap; gap: .3rem; }
+.gep-grid span, .gep-grid button {
+ width: 34px; height: 34px; border-radius: .4rem; border: none;
+ background: rgba(255,255,255,.05); cursor: pointer; font-size: 1.1rem;
+ display: flex; align-items: center; justify-content: center;
+ transition: background .15s;
+}
+.gep-grid span:hover, .gep-grid button:hover { background: rgba(26,188,156,.15); }
+
+/* Voice recording bar */
+.gp-voice-rec-bar {
+ display: flex; align-items: center; gap: .7rem;
+ padding: .6rem .8rem; background: rgba(231,76,60,.1);
+ border-top: 1px solid rgba(231,76,60,.3); flex-shrink: 0;
+ font-size: .85rem;
+}
+.gp-voice-rec-bar.hidden { display: none; }
+.gvr-pulse { animation: callPulse 1s ease-in-out infinite; }
+.gvr-cancel { background: none; border: 1px solid rgba(231,76,60,.5); color: #e74c3c; border-radius: 20px; padding: .2rem .6rem; cursor: pointer; font-family: inherit; font-size: .78rem; margin-right: auto; }
+
+/* Members List */
+.gp-members-header { display: flex; align-items: center; justify-content: space-between; padding: .8rem 1rem; border-bottom: 1px solid var(--border,rgba(255,255,255,.08)); flex-shrink: 0; }
+.gpm-count { font-weight: 700; font-size: .9rem; }
+.gpm-invite-btn { background: rgba(26,188,156,.15); border: 1px solid rgba(26,188,156,.3); color: var(--teal,#1abc9c); padding: .3rem .8rem; border-radius: 20px; cursor: pointer; font-family: inherit; font-size: .82rem; }
+.gp-members-list { flex: 1; overflow-y: auto; padding: .5rem .8rem; }
+.gp-member-row {
+ display: flex; align-items: center; gap: .7rem;
+ padding: .6rem .5rem; border-radius: var(--r,.8rem);
+ transition: background .15s; cursor: pointer;
+}
+.gp-member-row:hover { background: rgba(255,255,255,.04); }
+.gpm-avatar {
+ width: 40px; height: 40px; border-radius: 50%;
+ background: linear-gradient(135deg,#3498db,#9b59b6);
+ display: flex; align-items: center; justify-content: center;
+ font-size: 1.1rem; flex-shrink: 0;
+}
+.gpm-info { flex: 1; }
+.gpm-name { font-size: .88rem; font-weight: 600; }
+.gpm-role { font-size: .72rem; color: var(--text2,#8892a4); }
+.gpm-online { width: 8px; height: 8px; border-radius: 50%; background: #2ecc71; flex-shrink: 0; }
+.gpm-actions { display: flex; gap: .3rem; }
+.gpm-action-btn { background: none; border: 1px solid var(--border,rgba(255,255,255,.08)); border-radius: 20px; padding: .2rem .5rem; cursor: pointer; font-size: .75rem; color: var(--text2,#8892a4); font-family: inherit; transition: all .15s; }
+.gpm-action-btn:hover { border-color: var(--teal,#1abc9c); color: var(--teal,#1abc9c); }
+
+/* Media Grid */
+.gp-media-tabs { display: flex; padding: .5rem .8rem; gap: .4rem; border-bottom: 1px solid var(--border,rgba(255,255,255,.08)); flex-shrink: 0; }
+.gmt-tab { background: var(--dark3,rgba(255,255,255,.04)); border: 1px solid var(--border,rgba(255,255,255,.08)); color: var(--text2,#8892a4); padding: .3rem .7rem; border-radius: 20px; cursor: pointer; font-family: inherit; font-size: .78rem; transition: all .2s; }
+.gmt-tab.active { background: rgba(26,188,156,.15); border-color: rgba(26,188,156,.3); color: var(--teal,#1abc9c); }
+.gp-media-grid {
+ flex: 1; overflow-y: auto; padding: .6rem;
+ display: grid; grid-template-columns: repeat(3, 1fr); gap: .4rem;
+}
+.gp-media-thumb { border-radius: .5rem; overflow: hidden; aspect-ratio: 1; cursor: pointer; background: var(--dark3); position: relative; }
+.gp-media-thumb img, .gp-media-thumb video { width: 100%; height: 100%; object-fit: cover; }
+.gp-media-audio-item { grid-column: span 3; background: var(--card,rgba(255,255,255,.06)); border: 1px solid var(--border); border-radius: .6rem; padding: .6rem .8rem; display: flex; align-items: center; gap: .6rem; cursor: pointer; }
+
+/* Info Content */
+.gp-info-content { padding: 1rem; overflow-y: auto; }
+.gpi-section { margin-bottom: 1.2rem; }
+.gpi-section-title { font-weight: 700; font-size: .85rem; color: var(--teal,#1abc9c); margin-bottom: .6rem; }
+.gpi-row { display: flex; gap: .5rem; font-size: .84rem; margin-bottom: .5rem; padding: .5rem; background: var(--card,rgba(255,255,255,.04)); border-radius: .5rem; }
+.gpi-label { color: var(--text2,#8892a4); min-width: 80px; }
+.gpi-val { flex: 1; }
+.gpi-invite-btn { width: 100%; background: rgba(26,188,156,.12); border: 1px solid rgba(26,188,156,.3); color: var(--teal,#1abc9c); padding: .7rem; border-radius: var(--r,.8rem); cursor: pointer; font-family: inherit; font-size: .88rem; margin-top: .5rem; }
+.gpi-leave-btn { width: 100%; background: rgba(231,76,60,.1); border: 1px solid rgba(231,76,60,.3); color: #e74c3c; padding: .7rem; border-radius: var(--r,.8rem); cursor: pointer; font-family: inherit; font-size: .88rem; margin-top: .4rem; }
+
+/* Invite Modal */
+.gp-invite-modal {
+ position: absolute; inset: 0; background: rgba(0,0,0,.7);
+ display: flex; align-items: center; justify-content: center; z-index: 10;
+}
+.gp-invite-modal.hidden { display: none; }
+.gpim-inner { background: var(--dark2,#111827); border: 1px solid var(--border); border-radius: var(--r,.8rem); padding: 1.5rem; width: 90%; max-width: 360px; text-align: center; }
+.gpim-title { font-weight: 700; font-size: 1rem; margin-bottom: 1rem; }
+.gpim-link { font-size: .78rem; color: var(--teal,#1abc9c); word-break: break-all; padding: .6rem .8rem; background: rgba(26,188,156,.08); border-radius: .5rem; margin-bottom: .8rem; }
+.gpim-btns { display: flex; gap: .5rem; margin-bottom: .8rem; }
+.gpim-btn { flex: 1; background: var(--dark3); border: 1px solid var(--border); color: var(--text); padding: .55rem; border-radius: var(--rs); cursor: pointer; font-family: inherit; font-size: .82rem; transition: all .2s; }
+.gpim-share { background: rgba(26,188,156,.12); border-color: rgba(26,188,156,.3); color: var(--teal,#1abc9c); }
+.gpim-close { background: none; border: 1px solid var(--border); color: var(--text2,#8892a4); padding: .4rem .9rem; border-radius: 20px; cursor: pointer; font-family: inherit; font-size: .82rem; }
+.gpim-qr { margin: .8rem auto 0; width: 100px; height: 100px; }
+
+/* Incoming Call */
+.gp-incoming-call { position: absolute; inset: 0; background: rgba(0,0,0,.8); display: flex; align-items: center; justify-content: center; z-index: 20; animation: callFadeIn .3s ease; }
+.gp-incoming-call.hidden { display: none; }
+@keyframes callFadeIn { from{opacity:0} to{opacity:1} }
+.gpic-inner { background: var(--dark2,#111827); border: 1px solid var(--border); border-radius: 1.2rem; padding: 2rem 1.5rem; text-align: center; width: 88%; max-width: 320px; }
+.gpic-avatar { font-size: 3.5rem; margin-bottom: .8rem; animation: callPulse 1.5s ease-in-out infinite; }
+.gpic-name { font-size: 1.2rem; font-weight: 700; margin-bottom: .3rem; }
+.gpic-sub { font-size: .84rem; color: var(--text2,#8892a4); margin-bottom: 1.5rem; }
+.gpic-btns { display: flex; gap: 1rem; justify-content: center; }
+.gpic-accept { background: #27ae60; border: none; color: #fff; padding: .8rem 1.5rem; border-radius: 2rem; cursor: pointer; font-family: inherit; font-size: .9rem; transition: all .2s; }
+.gpic-reject { background: #e74c3c; border: none; color: #fff; padding: .8rem 1.5rem; border-radius: 2rem; cursor: pointer; font-family: inherit; font-size: .9rem; transition: all .2s; }
+.gpic-accept:hover { background: #2ecc71; }
+.gpic-reject:hover { background: #c0392b; }
+
+/* Context Menu for messages */
+.gp-context-menu {
+ position: fixed; background: var(--dark2,#111827);
+ border: 1px solid var(--border,rgba(255,255,255,.1));
+ border-radius: .7rem; padding: .3rem 0; z-index: 999; box-shadow: 0 8px 32px rgba(0,0,0,.5);
+ min-width: 160px;
+}
+.gp-context-menu.hidden { display: none; }
+.gcm-item {
+ display: flex; align-items: center; gap: .5rem;
+ padding: .6rem .9rem; cursor: pointer; font-size: .84rem;
+ transition: background .15s;
+}
+.gcm-item:hover { background: rgba(255,255,255,.06); }
+.gcm-item.gcm-danger { color: #e74c3c; }
+
+/* Study list card update */
+.study-card { cursor: pointer; transition: all .2s; }
+.study-card:hover { border-color: rgba(155,89,182,.4); transform: translateY(-1px); }
+
+
+/* ================================================================
+ 🎓 GROUP PAGE - صفحة المجموعة الكاملة
+ ================================================================ */
+
+/* Full-screen overlay page */
+.group-page {
+ position: fixed;
+ inset: 0;
+ background: var(--dark1);
+ z-index: 800;
+ display: flex;
+ flex-direction: column;
+ overflow: hidden;
+ transform: translateX(100%);
+ transition: transform 0.3s cubic-bezier(0.4,0,0.2,1);
+}
+.group-page.hidden { display: none !important; }
+
+/* Header */
+.gp-header {
+ display: flex;
+ align-items: center;
+ gap: .6rem;
+ padding: .8rem 1rem;
+ background: var(--dark2);
+ border-bottom: 1px solid var(--border);
+ min-height: 60px;
+ position: sticky;
+ top: 0;
+ z-index: 10;
+}
+.gp-back {
+ background: none;
+ border: none;
+ color: var(--accent);
+ font-size: 1rem;
+ cursor: pointer;
+ padding: .4rem .6rem;
+ border-radius: 8px;
+ white-space: nowrap;
+ transition: background .2s;
+}
+.gp-back:hover { background: var(--dark3); }
+.gp-title-wrap {
+ display: flex;
+ align-items: center;
+ gap: .7rem;
+ flex: 1;
+ min-width: 0;
+}
+.gp-avatar {
+ width: 42px;
+ height: 42px;
+ border-radius: 50%;
+ background: linear-gradient(135deg,#1abc9c20,#8e44ad20);
+ border: 2px solid var(--accent);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 1.3rem;
+ flex-shrink: 0;
+}
+.gp-name {
+ font-weight: 700;
+ font-size: 1rem;
+ color: var(--text1);
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+.gp-meta {
+ font-size: .75rem;
+ color: var(--text2);
+}
+.gp-actions-top {
+ display: flex;
+ gap: .3rem;
+}
+.gp-icon-btn {
+ background: var(--dark3);
+ border: 1px solid var(--border);
+ color: var(--text1);
+ border-radius: 8px;
+ padding: .45rem .55rem;
+ cursor: pointer;
+ font-size: 1rem;
+ transition: all .2s;
+}
+.gp-icon-btn:hover { background: var(--accent); color: #fff; }
+
+/* Tabs */
+.gp-tabs {
+ display: flex;
+ background: var(--dark2);
+ border-bottom: 1px solid var(--border);
+ overflow-x: auto;
+}
+.gp-tab {
+ flex: 1;
+ background: none;
+ border: none;
+ color: var(--text2);
+ padding: .7rem .5rem;
+ font-size: .82rem;
+ cursor: pointer;
+ border-bottom: 2px solid transparent;
+ transition: all .2s;
+ white-space: nowrap;
+}
+.gp-tab.active-gp-tab {
+ color: var(--accent);
+ border-bottom-color: var(--accent);
+}
+.gp-tab-content {
+ flex: 1;
+ overflow-y: auto;
+ display: flex;
+ flex-direction: column;
+}
+
+/* Call Banner */
+.gp-call-banner {
+ background: linear-gradient(135deg,#1abc9c,#27ae60);
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: .6rem 1rem;
+ flex-shrink: 0;
+}
+.gcb-info { display:flex; align-items:center; gap:.5rem; color:#fff; font-size:.88rem; }
+.gcb-pulse {
+ width:10px; height:10px; border-radius:50%; background:#fff;
+ animation: pulse 1s ease infinite;
+}
+.gcb-time { font-size:.8rem; opacity:.85; }
+.gcb-btns { display:flex; gap:.4rem; }
+.gcb-btn {
+ background: rgba(255,255,255,.2);
+ border: none;
+ border-radius: 8px;
+ padding: .45rem .6rem;
+ cursor: pointer;
+ font-size: .95rem;
+ color: #fff;
+ transition: background .2s;
+}
+.gcb-btn:hover { background: rgba(255,255,255,.35); }
+.gcb-end { background: #e74c3c !important; }
+
+/* Video Grid */
+.gp-video-grid {
+ background: #000;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: .5rem;
+ gap: .5rem;
+ max-height: 220px;
+ flex-shrink: 0;
+}
+.gp-video-self {
+ width: 100px;
+ height: 80px;
+ border-radius: 8px;
+ object-fit: cover;
+ border: 2px solid var(--accent);
+}
+.gp-remote-videos {
+ display: flex;
+ flex-wrap: wrap;
+ gap: .3rem;
+ flex: 1;
+}
+.gp-video-remote, .gp-remote-video {
+ flex: 1;
+ min-width: 100px;
+ height: 120px;
+ border-radius: 8px;
+ object-fit: cover;
+ background: #111;
+}
+
+/* Chat Messages */
+.gp-chat-messages {
+ flex: 1;
+ overflow-y: auto;
+ padding: .8rem;
+ display: flex;
+ flex-direction: column;
+ gap: .3rem;
+ background: var(--dark1);
+}
+.gp-empty-chat {
+ margin: auto;
+ text-align: center;
+ color: var(--text2);
+ padding: 2rem 1rem;
+}
+.gec-icon { font-size: 2.5rem; margin-bottom: .5rem; }
+
+.gp-date-divider {
+ display: flex;
+ align-items: center;
+ gap: .5rem;
+ margin: .5rem 0;
+ color: var(--text2);
+ font-size: .72rem;
+}
+.gp-date-divider::before,
+.gp-date-divider::after { content:''; flex:1; height:1px; background:var(--border); }
+
+/* Message Bubbles */
+.gp-msg {
+ display: flex;
+ flex-direction: column;
+ max-width: 82%;
+}
+.gpm-mine {
+ align-self: flex-end;
+ align-items: flex-end;
+}
+.gpm-other {
+ align-self: flex-start;
+ align-items: flex-start;
+}
+.gpm-author {
+ font-size: .73rem;
+ color: var(--accent);
+ margin-bottom: .15rem;
+ padding: 0 .4rem;
+}
+.gpm-text {
+ background: var(--dark3);
+ padding: .55rem .8rem;
+ border-radius: 16px;
+ font-size: .88rem;
+ color: var(--text1);
+ line-height: 1.5;
+ word-break: break-word;
+}
+.gpm-mine .gpm-text {
+ background: linear-gradient(135deg,#1abc9c,#16a085);
+ color: #fff;
+ border-bottom-right-radius: 4px;
+}
+.gpm-other .gpm-text {
+ background: var(--dark3);
+ border-bottom-left-radius: 4px;
+}
+.gpm-footer {
+ display: flex;
+ align-items: center;
+ gap: .4rem;
+ padding: .1rem .4rem;
+}
+.gpm-time { font-size: .67rem; color: var(--text2); }
+.gpm-actions { display:flex; gap:.2rem; opacity:0; transition:opacity .2s; }
+.gp-msg:hover .gpm-actions { opacity:1; }
+.gpma-btn {
+ background: none;
+ border: none;
+ cursor: pointer;
+ font-size: .8rem;
+ padding: .1rem .2rem;
+ border-radius: 6px;
+ opacity:.7;
+}
+.gpma-btn:hover { opacity:1; background:var(--dark3); }
+
+/* Reply */
+.gpm-reply {
+ background: var(--dark3);
+ border-left: 3px solid var(--accent);
+ padding: .3rem .6rem;
+ border-radius: 8px 8px 0 0;
+ margin-bottom: .1rem;
+ font-size: .75rem;
+ opacity: .85;
+}
+.gpmr-author { color: var(--accent); font-weight: 600; }
+.gpmr-text { color: var(--text2); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
+
+/* Reactions */
+.gpm-reactions {
+ display: flex;
+ flex-wrap: wrap;
+ gap: .2rem;
+ margin-top: .15rem;
+ padding: 0 .2rem;
+}
+.gpm-react {
+ background: var(--dark3);
+ border: 1px solid var(--border);
+ border-radius: 20px;
+ padding: .15rem .4rem;
+ font-size: .78rem;
+ cursor: pointer;
+ transition: all .2s;
+}
+.gpm-react:hover, .gpm-react.my-react {
+ background: var(--accent);
+ border-color: var(--accent);
+ color: #fff;
+}
+
+/* Emoji Reaction Menu */
+.emoji-react-menu {
+ background: var(--dark2);
+ border: 1px solid var(--border);
+ border-radius: 24px;
+ padding: .4rem .6rem;
+ display: flex;
+ gap: .25rem;
+ box-shadow: 0 4px 20px rgba(0,0,0,.5);
+ z-index: 9900;
+}
+.erm-emoji {
+ font-size: 1.3rem;
+ cursor: pointer;
+ transition: transform .15s;
+ padding: .1rem;
+}
+.erm-emoji:hover { transform: scale(1.4); }
+
+/* Media in messages */
+.gpm-media { margin: .3rem 0; max-width: 260px; }
+.gpm-img {
+ width: 100%;
+ border-radius: 12px;
+ cursor: zoom-in;
+ object-fit: cover;
+ max-height: 200px;
+}
+.gpm-video {
+ width: 100%;
+ border-radius: 12px;
+ max-height: 200px;
+}
+.gpm-audio-player {
+ display: flex;
+ align-items: center;
+ gap: .5rem;
+ background: var(--dark3);
+ padding: .5rem .8rem;
+ border-radius: 20px;
+}
+.gpa-icon { font-size: 1.2rem; }
+.gpm-audio { height: 30px; flex: 1; }
+
+/* Typing indicator */
+.gp-typing {
+ display: flex;
+ align-items: center;
+ gap: .3rem;
+ padding: .4rem .8rem;
+ color: var(--text2);
+ font-size: .8rem;
+}
+.gp-typing span {
+ width: 6px; height: 6px;
+ border-radius: 50%;
+ background: var(--text2);
+ animation: bounce 1.2s infinite;
+}
+.gp-typing span:nth-child(2) { animation-delay: .15s; }
+.gp-typing span:nth-child(3) { animation-delay: .3s; }
+@keyframes bounce { 0%,60%,100%{transform:translateY(0)} 30%{transform:translateY(-6px)} }
+
+/* Reply preview */
+.gp-reply-preview {
+ display: flex;
+ align-items: center;
+ gap: .5rem;
+ background: var(--dark2);
+ border-top: 1px solid var(--border);
+ padding: .4rem .8rem;
+ font-size: .8rem;
+}
+.grp-content { flex: 1; }
+.grp-label { color: var(--accent); font-weight: 600; font-size: .72rem; }
+.grp-text { color: var(--text2); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
+.grp-cancel {
+ background: none;
+ border: none;
+ color: var(--text2);
+ cursor: pointer;
+ font-size: 1rem;
+ padding: .2rem .4rem;
+}
+
+/* Input Bar */
+.gp-input-bar {
+ display: flex;
+ align-items: flex-end;
+ gap: .4rem;
+ padding: .6rem .8rem;
+ background: var(--dark2);
+ border-top: 1px solid var(--border);
+ flex-shrink: 0;
+}
+.gpi-btn {
+ background: var(--dark3);
+ border: 1px solid var(--border);
+ border-radius: 10px;
+ padding: .5rem .55rem;
+ cursor: pointer;
+ font-size: .95rem;
+ color: var(--text1);
+ transition: all .2s;
+ flex-shrink: 0;
+}
+.gpi-btn:hover { background: var(--accent); border-color: var(--accent); color: #fff; }
+.gpi-text-wrap { flex: 1; }
+.gpi-textarea {
+ width: 100%;
+ background: var(--dark3);
+ border: 1px solid var(--border);
+ border-radius: 16px;
+ padding: .6rem .9rem;
+ color: var(--text1);
+ font-size: .88rem;
+ resize: none;
+ outline: none;
+ line-height: 1.4;
+ min-height: 38px;
+ max-height: 120px;
+ overflow-y: auto;
+ transition: border-color .2s;
+ font-family: inherit;
+ direction: rtl;
+}
+.gpi-textarea:focus { border-color: var(--accent); }
+.gpi-send {
+ background: linear-gradient(135deg, var(--accent), #16a085);
+ border: none;
+ border-radius: 50%;
+ width: 40px;
+ height: 40px;
+ color: #fff;
+ font-size: 1rem;
+ cursor: pointer;
+ flex-shrink: 0;
+ transition: transform .15s;
+}
+.gpi-send:hover { transform: scale(1.1); }
+
+/* Emoji Picker */
+.gp-emoji-picker {
+ background: var(--dark2);
+ border-top: 1px solid var(--border);
+ padding: .6rem;
+ max-height: 160px;
+ overflow-y: auto;
+}
+.gep-grid {
+ display: flex;
+ flex-wrap: wrap;
+ gap: .3rem;
+ font-size: 1.4rem;
+ cursor: pointer;
+}
+
+/* Voice Recording Bar */
+.gp-voice-rec-bar {
+ display: flex;
+ align-items: center;
+ gap: .6rem;
+ padding: .6rem .8rem;
+ background: #c0392b20;
+ border-top: 1px solid #e74c3c40;
+ color: var(--text1);
+ font-size: .85rem;
+ flex-shrink: 0;
+}
+.gvr-pulse { animation: pulse 0.8s ease infinite; }
+.gvr-cancel {
+ background: #e74c3c;
+ border: none;
+ color: #fff;
+ border-radius: 8px;
+ padding: .3rem .6rem;
+ cursor: pointer;
+ font-size: .8rem;
+ margin-right: auto;
+}
+
+/* Members Tab */
+.gp-members-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: .8rem 1rem;
+ border-bottom: 1px solid var(--border);
+}
+.gpm-count { font-weight: 700; color: var(--text1); }
+.gpm-invite-btn {
+ background: var(--accent);
+ border: none;
+ color: #fff;
+ border-radius: 8px;
+ padding: .4rem .8rem;
+ cursor: pointer;
+ font-size: .85rem;
+}
+.gp-members-list { padding: .5rem; }
+.gpm-member {
+ display: flex;
+ align-items: center;
+ gap: .7rem;
+ padding: .7rem;
+ background: var(--dark2);
+ border-radius: 10px;
+ margin-bottom: .4rem;
+}
+.gpm-m-avatar {
+ width: 38px; height: 38px;
+ border-radius: 50%;
+ background: var(--dark3);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 1.1rem;
+ flex-shrink: 0;
+}
+.gpm-m-info { flex: 1; }
+.gpm-m-name { font-weight: 600; color: var(--text1); font-size: .9rem; }
+.gpm-m-id { font-size: .7rem; color: var(--text2); }
+.gpm-admin-badge {
+ background: var(--accent);
+ color: #fff;
+ border-radius: 6px;
+ padding: .1rem .3rem;
+ font-size: .65rem;
+ margin-right: .3rem;
+}
+.gpm-dm-btn {
+ background: var(--dark3);
+ border: 1px solid var(--border);
+ color: var(--text1);
+ border-radius: 8px;
+ padding: .35rem .6rem;
+ cursor: pointer;
+ font-size: .8rem;
+ white-space: nowrap;
+}
+.gpm-dm-btn:hover { background: var(--accent); color: #fff; }
+
+/* Media Tab */
+.gp-media-tabs {
+ display: flex;
+ background: var(--dark2);
+ border-bottom: 1px solid var(--border);
+ padding: .3rem .5rem;
+ gap: .3rem;
+}
+.gmt-tab {
+ background: none;
+ border: 1px solid var(--border);
+ border-radius: 8px;
+ color: var(--text2);
+ padding: .35rem .7rem;
+ font-size: .8rem;
+ cursor: pointer;
+ transition: all .2s;
+}
+.gmt-tab.active { background: var(--accent); color: #fff; border-color: var(--accent); }
+.gp-media-grid {
+ padding: .6rem;
+ display: grid;
+ grid-template-columns: repeat(3, 1fr);
+ gap: .4rem;
+}
+.gpmed-item { position: relative; }
+.gpmed-img {
+ width: 100%;
+ aspect-ratio: 1;
+ object-fit: cover;
+ border-radius: 8px;
+ cursor: zoom-in;
+}
+.gpmed-video {
+ width: 100%;
+ border-radius: 8px;
+ max-height: 150px;
+}
+.gpmed-caption {
+ font-size: .65rem;
+ color: var(--text2);
+ text-align: center;
+ margin-top: .15rem;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+.gpmed-audio-item {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ background: var(--dark2);
+ border-radius: 10px;
+ padding: .6rem;
+ gap: .3rem;
+ grid-column: span 3;
+}
+
+/* Info Tab */
+.gp-info-content { padding: .8rem; }
+.gpi-section {
+ background: var(--dark2);
+ border-radius: 12px;
+ padding: .8rem;
+ margin-bottom: .8rem;
+}
+.gpi-row {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: .6rem 0;
+ border-bottom: 1px solid var(--border);
+}
+.gpi-row:last-child { border-bottom: none; }
+.gpi-label { color: var(--text2); font-size: .85rem; }
+.gpi-val { color: var(--text1); font-size: .85rem; font-weight: 600; }
+.gpi-actions { display: flex; flex-direction: column; gap: .5rem; }
+.gpi-btn {
+ width: 100%;
+ padding: .75rem;
+ border-radius: 10px;
+ border: none;
+ cursor: pointer;
+ font-size: .9rem;
+ font-weight: 600;
+ transition: opacity .2s;
+}
+.gpi-invite { background: linear-gradient(135deg, var(--accent), #16a085); color: #fff; }
+.gpi-leave { background: var(--dark3); border: 1px solid #e74c3c40; color: #e74c3c; }
+.gpi-leave:hover { background: #e74c3c20; }
+
+/* Invite Modal */
+.gp-invite-modal {
+ position: absolute;
+ inset: 0;
+ background: rgba(0,0,0,.75);
+ display: flex;
+ align-items: flex-end;
+ z-index: 200;
+ animation: fadeIn .2s ease;
+}
+.gp-invite-modal.hidden { display: none; }
+.gpim-inner {
+ background: var(--dark2);
+ border-radius: 20px 20px 0 0;
+ padding: 1.5rem 1.2rem;
+ width: 100%;
+ max-height: 80vh;
+ overflow-y: auto;
+}
+.gpim-title { font-size: 1.1rem; font-weight: 700; text-align: center; margin-bottom: 1rem; color: var(--text1); }
+.gpim-link {
+ background: var(--dark3);
+ border: 1px solid var(--accent);
+ border-radius: 10px;
+ padding: .8rem;
+ font-size: .8rem;
+ color: var(--accent);
+ word-break: break-all;
+ text-align: center;
+ margin-bottom: .8rem;
+}
+.gpim-btns { display: flex; gap: .5rem; margin-bottom: .8rem; }
+.gpim-btn {
+ flex: 1;
+ background: var(--dark3);
+ border: 1px solid var(--border);
+ border-radius: 10px;
+ color: var(--text1);
+ padding: .7rem;
+ cursor: pointer;
+ font-size: .88rem;
+ font-weight: 600;
+}
+.gpim-share { background: var(--accent); color: #fff; border-color: var(--accent); }
+.gpim-qr { text-align: center; padding: .5rem; color: var(--text2); font-size: .8rem; }
+.gpim-qr-text { word-break: break-all; font-size: .72rem; }
+.gpim-close {
+ width: 100%;
+ background: none;
+ border: 1px solid var(--border);
+ border-radius: 10px;
+ color: var(--text2);
+ padding: .65rem;
+ cursor: pointer;
+ margin-top: .5rem;
+}
+
+/* Incoming Call Overlay */
+.gp-incoming-call {
+ position: absolute;
+ inset: 0;
+ background: rgba(0,0,0,.85);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ z-index: 300;
+ animation: fadeIn .3s ease;
+}
+.gp-incoming-call.hidden { display: none; }
+.gpic-inner {
+ text-align: center;
+ background: var(--dark2);
+ border-radius: 20px;
+ padding: 2rem 2.5rem;
+ box-shadow: 0 10px 40px rgba(0,0,0,.5);
+}
+.gpic-avatar { font-size: 3rem; margin-bottom: .5rem; animation: bounce 1s infinite; }
+.gpic-name { font-size: 1.2rem; font-weight: 700; color: var(--text1); margin-bottom: .25rem; }
+.gpic-sub { font-size: .85rem; color: var(--text2); margin-bottom: 1.5rem; }
+.gpic-btns { display: flex; gap: 1rem; justify-content: center; }
+.gpic-accept { background: #27ae60; border: none; color: #fff; border-radius: 50px; padding: .8rem 1.5rem; cursor: pointer; font-size: .95rem; font-weight: 700; }
+.gpic-reject { background: #e74c3c; border: none; color: #fff; border-radius: 50px; padding: .8rem 1.5rem; cursor: pointer; font-size: .95rem; font-weight: 700; }
+
+/* ================================================================
+ 📋 STUDY GROUP CARDS (improved)
+ ================================================================ */
+.study-card {
+ background: var(--dark2);
+ border-radius: 14px;
+ padding: 1rem;
+ margin-bottom: .7rem;
+ border-left: 4px solid var(--accent);
+ transition: transform .15s, box-shadow .15s;
+}
+.study-card:hover { transform: translateY(-2px); box-shadow: 0 4px 16px rgba(0,0,0,.3); }
+.sc-header { display: flex; align-items: center; gap: .7rem; margin-bottom: .6rem; }
+.sc-avatar {
+ width: 44px; height: 44px; border-radius: 12px;
+ display: flex; align-items: center; justify-content: center;
+ font-size: 1.3rem; flex-shrink: 0;
+}
+.sc-info { flex: 1; min-width: 0; }
+.sc-name { font-weight: 700; font-size: .95rem; color: var(--text1); }
+.sc-subject { font-size: .78rem; color: var(--text2); }
+.sc-badge {
+ border-radius: 8px;
+ padding: .2rem .5rem;
+ font-size: .72rem;
+ font-weight: 700;
+ white-space: nowrap;
+}
+.sc-stats { display: flex; gap: .5rem; flex-wrap: wrap; margin-bottom: .6rem; }
+.sc-stat { font-size: .78rem; color: var(--text2); }
+.sc-actions { display: flex; gap: .5rem; align-items: center; flex-wrap: wrap; }
+.sc-open-btn {
+ background: var(--accent);
+ border: none;
+ color: #fff;
+ border-radius: 8px;
+ padding: .45rem .9rem;
+ cursor: pointer;
+ font-size: .82rem;
+ font-weight: 600;
+ flex: 1;
+}
+.sc-join-btn {
+ background: var(--dark3);
+ border: 1px solid var(--border);
+ color: var(--text1);
+ border-radius: 8px;
+ padding: .45rem .8rem;
+ cursor: pointer;
+ font-size: .82rem;
+}
+.sc-member-badge {
+ color: #27ae60;
+ font-size: .8rem;
+ font-weight: 600;
+}
+.sc-full-badge {
+ color: var(--text2);
+ font-size: .78rem;
+}
+
+/* ================================================================
+ DM Typing in status
+ ================================================================ */
+#dmChatStatus.typing { color: var(--accent); }
+
+
+/* ================================================================
+ 🎓 GROUP PAGE STYLES - صفحة المجموعة المستقلة
+================================================================ */
+
+/* Full-screen overlay page */
+.group-page {
+ position: fixed;
+ inset: 0;
+ z-index: 9000;
+ background: var(--dark1, #0a0e1a);
+ display: flex;
+ flex-direction: column;
+ overflow: hidden;
+ direction: rtl;
+}
+.group-page.hidden { display: none !important; }
+
+/* Header */
+.gp-header {
+ display: flex;
+ align-items: center;
+ gap: .7rem;
+ padding: .75rem 1rem;
+ background: var(--dark2, #111827);
+ border-bottom: 1px solid rgba(255,255,255,.08);
+ position: sticky;
+ top: 0;
+ z-index: 10;
+ min-height: 60px;
+}
+.gp-back {
+ background: rgba(255,255,255,.08);
+ border: none;
+ color: var(--text1, #e2e8f0);
+ border-radius: 50%;
+ width: 36px; height: 36px;
+ font-size: 1.1rem;
+ cursor: pointer;
+ display: flex; align-items: center; justify-content: center;
+ flex-shrink: 0;
+ transition: background .2s;
+}
+.gp-back:hover { background: rgba(255,255,255,.15); }
+.gp-title-wrap { display: flex; align-items: center; gap: .6rem; flex: 1; min-width: 0; }
+.gp-avatar {
+ width: 42px; height: 42px;
+ border-radius: 50%;
+ background: linear-gradient(135deg, #9b59b6, #6c3483);
+ display: flex; align-items: center; justify-content: center;
+ font-size: 1.3rem;
+ flex-shrink: 0;
+}
+.gp-name { font-weight: 700; font-size: .95rem; color: var(--text1); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
+.gp-meta { font-size: .75rem; color: var(--text2, #94a3b8); }
+.gp-actions-top { display: flex; gap: .35rem; flex-shrink: 0; }
+.gp-icon-btn {
+ background: rgba(255,255,255,.07);
+ border: none;
+ color: var(--text1);
+ width: 36px; height: 36px;
+ border-radius: 50%;
+ font-size: 1rem;
+ cursor: pointer;
+ display: flex; align-items: center; justify-content: center;
+ transition: background .2s;
+}
+.gp-icon-btn:hover { background: rgba(255,255,255,.15); }
+
+/* Tabs */
+.gp-tabs {
+ display: flex;
+ background: var(--dark2);
+ border-bottom: 1px solid rgba(255,255,255,.06);
+ padding: 0 .5rem;
+ overflow-x: auto;
+ flex-shrink: 0;
+}
+.gp-tab {
+ flex: 1;
+ padding: .7rem .4rem;
+ background: none;
+ border: none;
+ color: var(--text2);
+ font-size: .8rem;
+ cursor: pointer;
+ border-bottom: 2px solid transparent;
+ white-space: nowrap;
+ transition: all .2s;
+ font-family: inherit;
+}
+.gp-tab:hover { color: var(--text1); }
+.active-gp-tab { color: #9b59b6 !important; border-bottom-color: #9b59b6 !important; font-weight: 600; }
+
+/* Tab content */
+.gp-tab-content {
+ flex: 1;
+ overflow-y: auto;
+ display: flex;
+ flex-direction: column;
+}
+.gp-tab-content.hidden { display: none !important; }
+
+/* Call Banner */
+.gp-call-banner {
+ background: linear-gradient(135deg, #1a1a2e, #16213e);
+ border-bottom: 1px solid rgba(26,188,156,.3);
+ padding: .6rem 1rem;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ flex-shrink: 0;
+}
+.gp-call-banner.hidden { display: none !important; }
+.gcb-info { display: flex; align-items: center; gap: .6rem; font-size: .85rem; }
+.gcb-pulse {
+ width: 10px; height: 10px;
+ border-radius: 50%;
+ background: #1abc9c;
+ animation: pulseDot 1.4s infinite;
+}
+@keyframes pulseDot { 0%,100%{opacity:1;transform:scale(1)} 50%{opacity:.6;transform:scale(1.3)} }
+.gcb-time { color: #1abc9c; font-weight: 700; font-size: .9rem; }
+.gcb-btns { display: flex; gap: .4rem; }
+.gcb-btn {
+ background: rgba(255,255,255,.1);
+ border: none;
+ color: #fff;
+ width: 34px; height: 34px;
+ border-radius: 50%;
+ font-size: .9rem;
+ cursor: pointer;
+ transition: background .2s;
+}
+.gcb-btn:hover { background: rgba(255,255,255,.2); }
+.gcb-end { background: #e74c3c !important; }
+.gcb-end:hover { background: #c0392b !important; }
+
+/* Video Grid */
+.gp-video-grid {
+ background: #000;
+ flex-shrink: 0;
+ max-height: 40vh;
+ display: flex;
+ gap: 4px;
+ padding: 4px;
+ overflow: hidden;
+}
+.gp-video-grid.hidden { display: none !important; }
+.gp-video-self {
+ width: 120px;
+ border-radius: 8px;
+ object-fit: cover;
+ border: 2px solid #9b59b6;
+ flex-shrink: 0;
+}
+.gp-remote-videos {
+ flex: 1;
+ display: flex;
+ flex-wrap: wrap;
+ gap: 4px;
+}
+.gp-video-remote-item {
+ width: calc(50% - 2px);
+ aspect-ratio: 4/3;
+ border-radius: 6px;
+ object-fit: cover;
+ background: #1a1a2e;
+}
+.gp-video-remote {
+ flex: 1;
+ border-radius: 8px;
+ object-fit: cover;
+ background: #111;
+}
+
+/* Messages area */
+.gp-chat-messages {
+ flex: 1;
+ overflow-y: auto;
+ padding: 1rem .8rem;
+ display: flex;
+ flex-direction: column;
+ gap: .5rem;
+ scroll-behavior: smooth;
+}
+.gp-empty-chat {
+ text-align: center;
+ color: var(--text2);
+ padding: 3rem 1rem;
+ font-size: .9rem;
+}
+
+/* Message bubbles */
+.gp-msg {
+ max-width: 80%;
+ display: flex;
+ flex-direction: column;
+ gap: .2rem;
+ position: relative;
+}
+.gp-msg-mine {
+ align-self: flex-start;
+ align-items: flex-start;
+}
+.gp-msg-other {
+ align-self: flex-end;
+ align-items: flex-end;
+}
+.gp-msg-author {
+ font-size: .72rem;
+ color: #9b59b6;
+ font-weight: 600;
+ padding: 0 .2rem;
+}
+.gp-msg-text {
+ background: var(--dark3, #1e293b);
+ padding: .55rem .8rem;
+ border-radius: 12px 12px 12px 4px;
+ font-size: .88rem;
+ color: var(--text1);
+ line-height: 1.5;
+ word-break: break-word;
+ white-space: pre-wrap;
+}
+.gp-msg-mine .gp-msg-text {
+ background: linear-gradient(135deg, #6c3483, #9b59b6);
+ border-radius: 12px 12px 4px 12px;
+ color: #fff;
+}
+.gp-msg-time {
+ font-size: .68rem;
+ color: var(--text2);
+ padding: 0 .3rem;
+}
+
+/* Reply preview in bubble */
+.gp-msg-reply {
+ background: rgba(255,255,255,.07);
+ border-right: 3px solid #9b59b6;
+ padding: .3rem .6rem;
+ border-radius: 6px;
+ margin-bottom: .2rem;
+ font-size: .78rem;
+}
+.gmr-author { color: #9b59b6; font-weight: 600; display: block; }
+.gmr-text { color: var(--text2); display: block; }
+
+/* Media in messages */
+.gp-msg-img {
+ max-width: 220px;
+ max-height: 180px;
+ border-radius: 10px;
+ object-fit: cover;
+ cursor: zoom-in;
+ border: 2px solid rgba(255,255,255,.08);
+}
+.gp-msg-video {
+ max-width: 240px;
+ max-height: 160px;
+ border-radius: 10px;
+}
+.gp-audio-bubble {
+ display: flex;
+ flex-direction: column;
+ gap: .3rem;
+ background: var(--dark3);
+ padding: .5rem .7rem;
+ border-radius: 10px;
+ min-width: 180px;
+}
+.gp-audio-player { width: 100%; max-width: 220px; height: 36px; }
+.gp-audio-label { font-size: .72rem; color: var(--text2); }
+
+/* Reactions */
+.gp-reactions {
+ display: flex;
+ flex-wrap: wrap;
+ gap: .25rem;
+ margin-top: .2rem;
+}
+.gp-react-pill {
+ background: rgba(255,255,255,.08);
+ border: 1px solid rgba(255,255,255,.1);
+ border-radius: 20px;
+ padding: .15rem .4rem;
+ font-size: .8rem;
+ cursor: pointer;
+ display: flex; align-items: center; gap: .2rem;
+ transition: all .15s;
+}
+.gp-react-pill small { font-size: .7rem; color: var(--text2); }
+.gp-react-mine { background: rgba(155,89,182,.25); border-color: rgba(155,89,182,.5); }
+
+/* Quick react bar */
+.gp-quick-react {
+ display: flex;
+ gap: .2rem;
+ opacity: 0;
+ transition: opacity .2s;
+ margin-top: .15rem;
+}
+.gp-msg:hover .gp-quick-react { opacity: 1; }
+.gqr-btn {
+ background: rgba(255,255,255,.07);
+ border: none;
+ border-radius: 20px;
+ padding: .15rem .35rem;
+ font-size: .8rem;
+ cursor: pointer;
+ transition: background .15s;
+}
+.gqr-btn:hover { background: rgba(255,255,255,.18); }
+
+/* Typing indicator */
+.gp-typing {
+ display: flex;
+ align-items: center;
+ gap: .4rem;
+ padding: .4rem .8rem;
+ flex-shrink: 0;
+}
+.gp-typing.hidden { display: none !important; }
+.gp-typing span {
+ width: 7px; height: 7px;
+ background: var(--text2);
+ border-radius: 50%;
+ animation: typingBounce .9s infinite ease-in-out;
+}
+.gp-typing span:nth-child(2) { animation-delay: .15s; }
+.gp-typing span:nth-child(3) { animation-delay: .3s; }
+@keyframes typingBounce { 0%,80%,100%{transform:scale(0.8);opacity:.5} 40%{transform:scale(1.2);opacity:1} }
+.gp-typing small { font-size: .75rem; color: var(--text2); }
+
+/* Reply preview bar */
+.gp-reply-preview {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ background: var(--dark3);
+ border-top: 1px solid rgba(255,255,255,.07);
+ border-right: 3px solid #9b59b6;
+ padding: .4rem .8rem;
+ flex-shrink: 0;
+}
+.gp-reply-preview.hidden { display: none !important; }
+.grp-content { flex: 1; }
+.grp-label { font-size: .72rem; color: #9b59b6; }
+.grp-text { font-size: .8rem; color: var(--text2); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
+.grp-cancel {
+ background: none; border: none; color: var(--text2);
+ font-size: 1rem; cursor: pointer; padding: .2rem .4rem;
+}
+
+/* Input bar */
+.gp-input-bar {
+ display: flex;
+ align-items: flex-end;
+ gap: .4rem;
+ padding: .6rem .8rem;
+ background: var(--dark2);
+ border-top: 1px solid rgba(255,255,255,.07);
+ flex-shrink: 0;
+}
+.gpi-btn {
+ background: rgba(255,255,255,.07);
+ border: none;
+ border-radius: 50%;
+ width: 38px; height: 38px;
+ font-size: 1rem;
+ cursor: pointer;
+ display: flex; align-items: center; justify-content: center;
+ flex-shrink: 0;
+ transition: background .15s;
+ color: var(--text1);
+}
+.gpi-btn:hover { background: rgba(255,255,255,.15); }
+.gpi-btn:active { transform: scale(.92); }
+.gpi-text-wrap { flex: 1; }
+.gpi-textarea {
+ width: 100%;
+ background: var(--dark3);
+ border: 1px solid rgba(255,255,255,.1);
+ border-radius: 20px;
+ padding: .55rem .9rem;
+ color: var(--text1);
+ font-size: .88rem;
+ resize: none;
+ min-height: 38px;
+ max-height: 120px;
+ font-family: inherit;
+ direction: rtl;
+ line-height: 1.4;
+}
+.gpi-textarea:focus { outline: none; border-color: rgba(155,89,182,.5); }
+.gpi-send {
+ background: linear-gradient(135deg, #6c3483, #9b59b6);
+ border: none;
+ border-radius: 50%;
+ width: 38px; height: 38px;
+ color: #fff;
+ font-size: 1rem;
+ cursor: pointer;
+ display: flex; align-items: center; justify-content: center;
+ flex-shrink: 0;
+ transition: transform .15s, opacity .15s;
+}
+.gpi-send:hover { transform: scale(1.08); }
+.gpi-send:active { transform: scale(.92); }
+
+/* Emoji picker */
+.gp-emoji-picker {
+ position: absolute;
+ bottom: 70px;
+ right: .8rem;
+ background: var(--dark2);
+ border: 1px solid rgba(255,255,255,.1);
+ border-radius: 12px;
+ padding: .8rem;
+ z-index: 100;
+ box-shadow: 0 8px 32px rgba(0,0,0,.6);
+ max-width: 320px;
+}
+.gp-emoji-picker.hidden { display: none !important; }
+.gep-grid {
+ display: flex;
+ flex-wrap: wrap;
+ gap: .3rem;
+ font-size: 1.4rem;
+ line-height: 1;
+ cursor: pointer;
+}
+.gep-grid span:hover { transform: scale(1.3); }
+
+/* Voice recording bar */
+.gp-voice-rec-bar {
+ display: flex;
+ align-items: center;
+ gap: .7rem;
+ background: rgba(231,76,60,.12);
+ border-top: 1px solid rgba(231,76,60,.3);
+ padding: .5rem .8rem;
+ font-size: .85rem;
+ flex-shrink: 0;
+}
+.gp-voice-rec-bar.hidden { display: none !important; }
+.gvr-pulse { animation: pulseDot 1s infinite; }
+.gvr-cancel { background: rgba(231,76,60,.2); border: 1px solid rgba(231,76,60,.4); color: #e74c3c; padding: .25rem .6rem; border-radius: 6px; cursor: pointer; font-size: .8rem; margin-right: auto; }
+
+/* Members list */
+.gp-members-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: .8rem 1rem;
+ border-bottom: 1px solid rgba(255,255,255,.07);
+}
+.gpm-count { font-weight: 600; font-size: .9rem; }
+.gpm-invite-btn {
+ background: linear-gradient(135deg, #6c3483, #9b59b6);
+ border: none;
+ color: #fff;
+ padding: .35rem .8rem;
+ border-radius: 20px;
+ font-size: .8rem;
+ cursor: pointer;
+}
+.gp-members-list { padding: .5rem; }
+.gpm-item {
+ display: flex;
+ align-items: center;
+ gap: .7rem;
+ padding: .6rem .8rem;
+ border-radius: 10px;
+ transition: background .15s;
+}
+.gpm-item:hover { background: rgba(255,255,255,.04); }
+.gpm-avatar {
+ width: 40px; height: 40px;
+ background: linear-gradient(135deg, #6c3483, #8e44ad);
+ border-radius: 50%;
+ display: flex; align-items: center; justify-content: center;
+ font-size: 1.2rem;
+ flex-shrink: 0;
+}
+.gpm-info { flex: 1; }
+.gpm-name { font-size: .88rem; font-weight: 600; }
+.gpm-sub { font-size: .75rem; color: var(--text2); }
+.gpm-dm-btn {
+ background: rgba(255,255,255,.07);
+ border: none;
+ color: var(--text1);
+ padding: .3rem .6rem;
+ border-radius: 8px;
+ font-size: .78rem;
+ cursor: pointer;
+}
+
+/* Media grid */
+.gp-media-tabs {
+ display: flex;
+ gap: .3rem;
+ padding: .7rem .8rem;
+ border-bottom: 1px solid rgba(255,255,255,.07);
+ flex-shrink: 0;
+}
+.gmt-tab {
+ background: rgba(255,255,255,.07);
+ border: none;
+ color: var(--text2);
+ padding: .35rem .7rem;
+ border-radius: 20px;
+ font-size: .78rem;
+ cursor: pointer;
+ transition: all .15s;
+}
+.gmt-tab.active { background: linear-gradient(135deg, #6c3483, #9b59b6); color: #fff; }
+.gp-media-grid {
+ flex: 1;
+ display: flex;
+ flex-wrap: wrap;
+ gap: 4px;
+ padding: .5rem;
+ align-content: flex-start;
+ overflow-y: auto;
+}
+.gpm-thumb {
+ width: calc(33.33% - 3px);
+ aspect-ratio: 1;
+ object-fit: cover;
+ border-radius: 6px;
+ cursor: zoom-in;
+}
+.gpm-audio-item { width: 100%; padding: .3rem; }
+
+/* Info tab */
+.gp-info-content { padding: 1rem; }
+.gpi-section { background: var(--dark2); border-radius: 12px; overflow: hidden; margin-bottom: 1rem; }
+.gpi-row {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: .7rem 1rem;
+ border-bottom: 1px solid rgba(255,255,255,.06);
+ font-size: .88rem;
+}
+.gpi-row:last-child { border-bottom: none; }
+.gpi-label { color: var(--text2); font-size: .82rem; }
+.gpi-actions { display: flex; flex-direction: column; gap: .5rem; }
+.gpi-btn {
+ background: rgba(255,255,255,.07);
+ border: 1px solid rgba(255,255,255,.1);
+ color: var(--text1);
+ padding: .7rem 1rem;
+ border-radius: 10px;
+ text-align: center;
+ cursor: pointer;
+ font-size: .88rem;
+ width: 100%;
+ transition: background .15s;
+}
+.gpi-leave { border-color: rgba(231,76,60,.4); color: #e74c3c; }
+
+/* Invite modal */
+.gp-invite-modal {
+ position: absolute;
+ inset: 0;
+ background: rgba(0,0,0,.7);
+ z-index: 200;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 1rem;
+}
+.gp-invite-modal.hidden { display: none !important; }
+.gpim-inner {
+ background: var(--dark2);
+ border-radius: 16px;
+ padding: 1.5rem;
+ width: 100%;
+ max-width: 380px;
+ display: flex;
+ flex-direction: column;
+ gap: .8rem;
+ border: 1px solid rgba(155,89,182,.3);
+}
+.gpim-title { font-size: 1.1rem; font-weight: 700; text-align: center; }
+.gpim-link {
+ background: var(--dark3);
+ border: 1px solid rgba(255,255,255,.1);
+ border-radius: 8px;
+ padding: .6rem .8rem;
+ font-size: .78rem;
+ color: #1abc9c;
+ word-break: break-all;
+ text-align: center;
+}
+.gpim-btns { display: flex; gap: .5rem; }
+.gpim-btn {
+ flex: 1;
+ background: rgba(255,255,255,.08);
+ border: 1px solid rgba(255,255,255,.12);
+ color: var(--text1);
+ padding: .6rem;
+ border-radius: 8px;
+ cursor: pointer;
+ font-size: .85rem;
+ text-align: center;
+ transition: background .15s;
+}
+.gpim-share { background: linear-gradient(135deg, #6c3483, #9b59b6); color: #fff; border: none; }
+.gpim-qr { min-height: 40px; }
+.gp-qr-text { font-size: .75rem; color: var(--text2); text-align: center; background: var(--dark3); padding: .5rem; border-radius: 8px; word-break: break-all; }
+.gpim-close { background: none; border: none; color: var(--text2); cursor: pointer; text-align: center; font-size: .85rem; }
+
+/* Incoming call overlay */
+.gp-incoming-call {
+ position: absolute;
+ inset: 0;
+ background: rgba(0,0,0,.8);
+ z-index: 300;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+.gp-incoming-call.hidden { display: none !important; }
+.gpic-inner {
+ background: var(--dark2);
+ border-radius: 20px;
+ padding: 2rem;
+ text-align: center;
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
+ align-items: center;
+ border: 1px solid rgba(26,188,156,.3);
+ min-width: 240px;
+}
+.gpic-avatar { font-size: 3rem; animation: pulseDot 1.2s infinite; }
+.gpic-name { font-size: 1.2rem; font-weight: 700; }
+.gpic-sub { font-size: .85rem; color: var(--text2); }
+.gpic-btns { display: flex; gap: 1rem; }
+.gpic-accept, .gpic-reject {
+ border: none;
+ border-radius: 50px;
+ padding: .7rem 1.5rem;
+ font-size: .9rem;
+ cursor: pointer;
+ font-weight: 600;
+}
+.gpic-accept { background: #1abc9c; color: #fff; }
+.gpic-reject { background: #e74c3c; color: #fff; }
+
+/* ================================================================
+ 📚 STUDY CARD STYLES - بطاقات مجموعات التعلم
+================================================================ */
+.study-card {
+ background: var(--dark2);
+ border: 1px solid rgba(155,89,182,.2);
+ border-radius: 14px;
+ padding: 1rem;
+ margin-bottom: .7rem;
+ cursor: pointer;
+ transition: all .2s;
+}
+.study-card:hover { border-color: rgba(155,89,182,.5); transform: translateY(-1px); box-shadow: 0 6px 20px rgba(155,89,182,.15); }
+.study-card-header { display: flex; align-items: center; gap: .7rem; margin-bottom: .7rem; }
+.study-card-icon {
+ width: 44px; height: 44px;
+ background: linear-gradient(135deg, #6c3483, #9b59b6);
+ border-radius: 50%;
+ display: flex; align-items: center; justify-content: center;
+ font-size: 1.3rem;
+ flex-shrink: 0;
+}
+.study-card-info { flex: 1; }
+.study-card-name { font-weight: 700; font-size: .92rem; }
+.study-card-sub { font-size: .75rem; color: var(--text2); }
+.study-card-badge {
+ background: rgba(255,255,255,.07);
+ border-radius: 20px;
+ padding: .2rem .5rem;
+ font-size: .72rem;
+ color: var(--text2);
+}
+.scb-member { background: rgba(26,188,156,.15); color: #1abc9c; }
+.study-card-progress { margin-bottom: .5rem; }
+.scp-bar { height: 5px; background: rgba(255,255,255,.08); border-radius: 3px; margin-bottom: .3rem; }
+.scp-fill { height: 100%; background: linear-gradient(90deg, #6c3483, #9b59b6); border-radius: 3px; }
+.scp-text { font-size: .73rem; color: var(--text2); }
+.study-card-area { font-size: .75rem; color: var(--text2); margin-bottom: .5rem; }
+.study-card-actions { display: flex; gap: .5rem; margin-top: .6rem; }
+.study-open-btn {
+ flex: 1;
+ background: linear-gradient(135deg, #6c3483, #9b59b6);
+ border: none;
+ color: #fff;
+ padding: .5rem;
+ border-radius: 8px;
+ font-size: .82rem;
+ cursor: pointer;
+ font-weight: 600;
+}
+.study-join-btn {
+ background: rgba(26,188,156,.15);
+ border: 1px solid rgba(26,188,156,.3);
+ color: #1abc9c;
+ padding: .5rem .8rem;
+ border-radius: 8px;
+ font-size: .82rem;
+ cursor: pointer;
+}
+
+
+/* Avatar picker */
+.avatar-pick {
+ width: 36px; height: 36px;
+ display: flex; align-items: center; justify-content: center;
+ font-size: 1.4rem;
+ border-radius: 50%;
+ border: 2px solid transparent;
+ cursor: pointer;
+ transition: all .15s;
+ background: rgba(255,255,255,.05);
+}
+.avatar-pick:hover { transform: scale(1.2); border-color: rgba(155,89,182,.4); }
+.active-avatar { border-color: #9b59b6 !important; background: rgba(155,89,182,.2) !important; transform: scale(1.1); }
+
+/* ================================================================
+ 🔥 VIRAL FEATURES CSS
+ نقاط | متصدرون | تحديات | طوارئ | مشاركة سريعة | شريط الزخم
+================================================================ */
+
+/* ── @keyframes ─────────────────────────────────────────────── */
+@keyframes pointsFloat {
+ 0% { opacity:1; transform:translateX(-50%) translateY(0); }
+ 100% { opacity:0; transform:translateX(-50%) translateY(-60px); }
+}
+@keyframes achievePop {
+ 0% { opacity:0; transform:translateX(120px); }
+ 20% { opacity:1; transform:translateX(0); }
+ 80% { opacity:1; transform:translateX(0); }
+ 100% { opacity:0; transform:translateX(120px); }
+}
+@keyframes emPulse {
+ 0%,100% { transform:scale(1); opacity:.7; }
+ 50% { transform:scale(1.5); opacity:0; }
+}
+@keyframes emBlink {
+ 0%,100% { background:rgba(231,76,60,.15); }
+ 50% { background:rgba(231,76,60,.35); }
+}
+@keyframes heatGlow {
+ 0%,100% { box-shadow:0 0 8px rgba(231,76,60,.4); }
+ 50% { box-shadow:0 0 20px rgba(231,76,60,.8); }
+}
+@keyframes slideDown {
+ from { opacity:0; transform:translateY(-12px); }
+ to { opacity:1; transform:translateY(0); }
+}
+@keyframes challengePulse {
+ 0%,100% { box-shadow:0 0 0 0 rgba(52,152,219,.4); }
+ 50% { box-shadow:0 0 0 10px rgba(52,152,219,0); }
+}
+
+/* ── Viral Momentum Bar ─────────────────────────────────────── */
+.viral-momentum-bar {
+ display:flex; align-items:center; gap:0;
+ background:rgba(255,255,255,.04);
+ border:1px solid var(--border);
+ border-radius:var(--r);
+ padding:.6rem 0;
+ overflow:hidden;
+ animation:slideDown .4s ease;
+}
+.vmb-item {
+ flex:1; text-align:center; padding:.3rem .5rem;
+}
+.vmb-sep {
+ width:1px; background:var(--border); align-self:stretch; flex-shrink:0;
+}
+.vmb-dot {
+ width:8px; height:8px; border-radius:50%; margin:0 auto .3rem;
+}
+.vmb-green {
+ background:#2ecc71;
+ box-shadow:0 0 6px #2ecc71;
+ animation:emPulse 2s infinite;
+}
+.vmb-num {
+ font-size:1.1rem; font-weight:800; color:var(--teal);
+ line-height:1;
+}
+.vmb-lbl {
+ font-size:.62rem; color:var(--text2); margin-top:.15rem;
+ white-space:nowrap;
+}
+.trending-num {
+ color:#e67e22 !important; font-size:.85rem !important;
+}
+
+/* ── Leaderboard Mini (Home) ────────────────────────────────── */
+.leaderboard-mini {
+ display:flex; flex-direction:column; gap:.4rem;
+}
+.lb-mini-item {
+ display:flex; align-items:center; gap:.6rem;
+ background:rgba(255,255,255,.04);
+ border:1px solid var(--border);
+ border-radius:var(--r);
+ padding:.55rem .85rem;
+ transition:all .2s;
+ cursor:pointer;
+}
+.lb-mini-item:hover { background:rgba(26,188,156,.08); border-color:rgba(26,188,156,.3); }
+.lb-mini-me {
+ background:linear-gradient(135deg,rgba(26,188,156,.12),rgba(52,152,219,.08)) !important;
+ border-color:var(--teal) !important;
+}
+.lb-mini-medal { font-size:1.1rem; width:1.4rem; text-align:center; }
+.lb-mini-name { flex:1; font-size:.85rem; font-weight:600; color:var(--text); }
+.lb-mini-pts { font-size:.8rem; color:var(--teal); font-weight:700; }
+.lb-mini-empty {
+ text-align:center; padding:1rem; color:var(--text2);
+ font-size:.85rem;
+}
+.lb-mini-more {
+ margin-top:.3rem; width:100%; padding:.5rem;
+ background:transparent; border:1px solid var(--border);
+ border-radius:var(--r); color:var(--teal); cursor:pointer;
+ font-family:inherit; font-size:.8rem; font-weight:600;
+ transition:all .2s;
+}
+.lb-mini-more:hover { background:rgba(26,188,156,.1); }
+
+/* ── Leaderboard Full Page ──────────────────────────────────── */
+.lb-tabs {
+ display:flex; gap:.4rem; padding:.8rem .9rem .4rem;
+ background:var(--bg2); border-bottom:1px solid var(--border);
+}
+.lb-tab {
+ flex:1; padding:.5rem .4rem;
+ background:transparent; border:1px solid var(--border);
+ border-radius:var(--r); cursor:pointer;
+ font-family:inherit; font-size:.78rem; color:var(--text2);
+ transition:all .2s;
+}
+.lb-tab:hover { color:var(--text); }
+.active-lb-tab {
+ background:linear-gradient(135deg,var(--teal),var(--blue)) !important;
+ color:#fff !important; border-color:transparent !important;
+}
+.lb-list {
+ flex:1; overflow-y:auto; padding:.8rem .9rem;
+ display:flex; flex-direction:column; gap:.5rem;
+}
+.lb-item {
+ display:flex; align-items:center; gap:.7rem;
+ background:rgba(255,255,255,.04);
+ border:1px solid var(--border);
+ border-radius:var(--r);
+ padding:.6rem .8rem;
+ transition:all .2s;
+ animation:slideDown .3s ease;
+}
+.lb-item:hover { background:rgba(26,188,156,.08); }
+.lb-item-me {
+ background:linear-gradient(135deg,rgba(26,188,156,.12),rgba(52,152,219,.08)) !important;
+ border-color:var(--teal) !important;
+ box-shadow:0 0 0 2px rgba(26,188,156,.2);
+}
+.lb-rank { font-size:1.3rem; width:1.8rem; text-align:center; }
+.lb-avatar{ font-size:1.4rem; width:2rem; text-align:center; }
+.lb-info { flex:1; min-width:0; }
+.lb-name { font-size:.9rem; font-weight:700; color:var(--text); white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
+.lb-area { font-size:.72rem; color:var(--text2); margin-top:.1rem; }
+.lb-pts { text-align:center; min-width:50px; }
+.lb-pts-num{ font-size:1rem; font-weight:800; color:var(--teal); }
+.lb-pts-lbl{ font-size:.65rem; color:var(--text2); }
+.lb-my-rank {
+ position:sticky; bottom:0;
+ background:linear-gradient(135deg,var(--teal),var(--blue));
+ padding:.8rem 1.2rem;
+ display:flex; align-items:center; justify-content:space-between;
+ cursor:pointer;
+}
+.lbmr-label { font-size:.8rem; color:rgba(255,255,255,.8); }
+.lbmr-rank { font-size:1.6rem; font-weight:800; color:#fff; }
+.lbmr-pts { font-size:.9rem; color:rgba(255,255,255,.9); font-weight:700; }
+
+/* ── Daily Challenge Card (Home) ────────────────────────────── */
+.daily-challenge-card {
+ background:linear-gradient(135deg,rgba(52,152,219,.1),rgba(155,89,182,.08));
+ border:1px solid rgba(52,152,219,.25);
+ border-radius:var(--r);
+ padding:1rem 1.1rem;
+ cursor:pointer;
+ transition:all .2s;
+ animation:challengePulse 3s infinite;
+}
+.daily-challenge-card:hover {
+ background:linear-gradient(135deg,rgba(52,152,219,.18),rgba(155,89,182,.14));
+ transform:translateY(-1px);
+}
+.dcc-badge {
+ display:inline-block;
+ background:linear-gradient(135deg,#3498db,#9b59b6);
+ color:#fff; padding:.2rem .7rem;
+ border-radius:30px; font-size:.72rem; font-weight:700;
+ margin-bottom:.5rem;
+}
+.dcc-title { font-size:1.05rem; font-weight:800; color:var(--text); margin-bottom:.2rem; }
+.dcc-desc { font-size:.82rem; color:var(--text2); margin-bottom:.6rem; line-height:1.4; }
+.dcc-progress { margin-bottom:.5rem; }
+.dcp-bar {
+ background:rgba(255,255,255,.08);
+ border-radius:30px; height:6px; overflow:hidden;
+ margin-bottom:.3rem;
+}
+.dcp-bar.large { height:10px; margin-bottom:.5rem; }
+.dcp-fill {
+ height:100%;
+ background:linear-gradient(90deg,#3498db,#2ecc71);
+ border-radius:30px;
+ transition:width .5s ease;
+}
+.dcp-text { font-size:.75rem; color:var(--text2); }
+.dcp-nums { font-size:.85rem; color:var(--text2); text-align:center; }
+.dcc-reward{ font-size:.8rem; color:#f39c12; font-weight:600; }
+
+/* ── Daily Challenge Modal ──────────────────────────────────── */
+.dc-card { max-width:340px !important; }
+.dc-modal-body { padding:.5rem 0 1rem; text-align:center; }
+.dc-icon { font-size:2.5rem; margin-bottom:.5rem; }
+.dc-m-title { font-size:1.1rem; font-weight:800; color:var(--text); margin-bottom:.3rem; }
+.dc-m-desc { font-size:.85rem; color:var(--text2); line-height:1.5; margin-bottom:1rem; }
+.dc-m-progress { margin-bottom:.8rem; }
+.dc-m-reward { font-size:.9rem; color:#f39c12; font-weight:700; margin-bottom:.4rem; }
+.dc-m-streak { font-size:.85rem; color:#e67e22; }
+.dc-join-btn {
+ width:100%; padding:.85rem;
+ background:linear-gradient(135deg,#3498db,#9b59b6);
+ border:none; border-radius:var(--r);
+ color:#fff; font-family:inherit; font-size:1rem; font-weight:700;
+ cursor:pointer; transition:all .2s;
+}
+.dc-join-btn:hover { opacity:.9; transform:translateY(-1px); }
+
+/* ── Viral Alerts List ──────────────────────────────────────── */
+.viral-alerts-list { display:flex; flex-direction:column; gap:.5rem; }
+.viral-empty {
+ text-align:center; padding:1.5rem; color:var(--text2); font-size:.85rem;
+}
+.viral-card {
+ display:flex; align-items:flex-start; gap:.6rem;
+ background:rgba(255,255,255,.04);
+ border:1px solid var(--border);
+ border-radius:var(--r);
+ padding:.7rem .8rem;
+ cursor:pointer;
+ transition:all .2s;
+ animation:slideDown .3s ease;
+ position:relative;
+}
+.viral-card:hover {
+ background:rgba(231,76,60,.06);
+ border-color:rgba(231,76,60,.3);
+ animation:heatGlow 2s infinite;
+}
+.vc-type { font-size:1.1rem; flex-shrink:0; margin-top:.1rem; }
+.vc-content { flex:1; min-width:0; }
+.vc-msg { font-size:.85rem; font-weight:600; color:var(--text); margin-bottom:.25rem; line-height:1.3; }
+.vc-area { font-size:.72rem; color:var(--text2); margin-bottom:.3rem; }
+.vc-heat-bar { height:3px; background:rgba(255,255,255,.08); border-radius:3px; overflow:hidden; }
+.vc-heat-fill { height:100%; background:linear-gradient(90deg,#f39c12,#e74c3c); border-radius:3px; }
+.vc-stats { display:flex; flex-direction:column; gap:.2rem; align-items:flex-end; flex-shrink:0; }
+.vc-stat { font-size:.72rem; color:var(--text2); white-space:nowrap; }
+.vc-share-btn {
+ position:absolute; bottom:.5rem; left:.5rem;
+ background:rgba(26,188,156,.15); border:1px solid rgba(26,188,156,.3);
+ border-radius:20px; padding:.2rem .6rem;
+ font-size:.75rem; cursor:pointer; color:var(--teal);
+ transition:all .2s;
+}
+.vc-share-btn:hover { background:rgba(26,188,156,.3); }
+
+/* ── Quick Share Overlay ────────────────────────────────────── */
+.qs-overlay {
+ position:fixed; inset:0; z-index:9000;
+ background:rgba(0,0,0,.65);
+ display:flex; align-items:center; justify-content:center;
+ padding:1rem;
+ backdrop-filter:blur(4px);
+}
+.qs-overlay.hidden { display:none; }
+.qs-card {
+ background:var(--bg2);
+ border:1px solid var(--border);
+ border-radius:calc(var(--r)*1.5);
+ padding:1.2rem;
+ width:100%; max-width:360px;
+ animation:slideDown .3s ease;
+}
+.qs-header {
+ display:flex; align-items:center; justify-content:space-between;
+ margin-bottom:.9rem;
+}
+.qs-title { font-size:1rem; font-weight:800; color:var(--text); }
+.qs-close {
+ background:rgba(255,255,255,.08); border:none;
+ width:28px; height:28px; border-radius:50%;
+ cursor:pointer; color:var(--text2); font-size:.85rem;
+ display:flex; align-items:center; justify-content:center;
+ transition:all .2s;
+}
+.qs-close:hover { background:rgba(255,255,255,.15); color:var(--text); }
+.qs-preview {
+ background:rgba(255,255,255,.04);
+ border:1px solid var(--border);
+ border-radius:var(--r);
+ padding:.8rem;
+ margin-bottom:.9rem;
+}
+.qsp-type { font-size:.8rem; font-weight:700; margin-bottom:.3rem; }
+.qsp-msg { font-size:.9rem; color:var(--text); font-weight:600; margin-bottom:.3rem; line-height:1.4; }
+.qsp-area { font-size:.75rem; color:var(--text2); margin-bottom:.3rem; }
+.qsp-via { font-size:.7rem; color:var(--teal); }
+.qs-platforms {
+ display:grid; grid-template-columns:1fr 1fr 1fr;
+ gap:.5rem; margin-bottom:.8rem;
+}
+.qs-platform {
+ padding:.55rem .4rem;
+ border:1px solid var(--border);
+ border-radius:var(--r);
+ background:rgba(255,255,255,.04);
+ cursor:pointer; font-family:inherit;
+ font-size:.75rem; font-weight:600;
+ color:var(--text); transition:all .2s;
+ text-align:center;
+}
+.qs-platform:hover { transform:translateY(-2px); border-color:var(--teal); }
+.qsp-wa:hover { background:rgba(37,211,102,.15); border-color:#25d366; color:#25d366; }
+.qsp-tg:hover { background:rgba(0,136,204,.15); border-color:#0088cc; color:#0088cc; }
+.qsp-tw:hover { background:rgba(29,161,242,.15); border-color:#1da1f2; color:#1da1f2; }
+.qsp-fb:hover { background:rgba(24,119,242,.15); border-color:#1877f2; color:#1877f2; }
+.qsp-cp:hover { background:rgba(26,188,156,.15); border-color:var(--teal); color:var(--teal); }
+.qsp-nat:hover { background:rgba(155,89,182,.15); border-color:#9b59b6; color:#9b59b6; }
+.qs-stats-preview {
+ display:flex; gap:.8rem; justify-content:center;
+ font-size:.78rem; color:var(--text2);
+}
+
+/* ── Emergency Mode Overlay ─────────────────────────────────── */
+.emergency-overlay {
+ position:fixed; inset:0; z-index:8500;
+ background:rgba(0,0,0,.85);
+ display:flex; flex-direction:column;
+ align-items:center; justify-content:center;
+ padding:1.5rem; gap:1.2rem;
+ backdrop-filter:blur(6px);
+ animation:emBlink 2s infinite;
+}
+.emergency-overlay.hidden { display:none; }
+.em-header { text-align:center; }
+.em-pulse-ring {
+ width:60px; height:60px; border-radius:50%;
+ border:3px solid #e74c3c;
+ margin:0 auto .8rem;
+ animation:emPulse 1s infinite;
+}
+.em-title { font-size:1.3rem; font-weight:900; color:#e74c3c; }
+.em-subtitle{ font-size:.85rem; color:rgba(255,255,255,.7); margin-top:.3rem; }
+.em-stats {
+ display:flex; gap:1.5rem;
+ background:rgba(231,76,60,.1);
+ border:1px solid rgba(231,76,60,.3);
+ border-radius:var(--r);
+ padding:.8rem 1.5rem;
+}
+.em-stat { text-align:center; }
+.em-num { font-size:1.5rem; font-weight:900; color:#e74c3c; }
+.em-lbl { font-size:.7rem; color:rgba(255,255,255,.6); }
+.em-actions { display:grid; grid-template-columns:1fr 1fr; gap:.7rem; width:100%; max-width:340px; }
+.em-btn {
+ padding:.75rem; border:none; border-radius:var(--r);
+ cursor:pointer; font-family:inherit;
+ font-size:.9rem; font-weight:700;
+ transition:all .2s; text-align:center;
+}
+.em-btn:hover { transform:translateY(-2px); opacity:.9; }
+.em-btn-sos { background:#e74c3c; color:#fff; }
+.em-btn-help { background:#27ae60; color:#fff; }
+.em-btn-report { background:#f39c12; color:#fff; }
+.em-btn-map { background:#3498db; color:#fff; }
+.em-close {
+ background:rgba(255,255,255,.1);
+ border:1px solid rgba(255,255,255,.2);
+ border-radius:var(--r);
+ color:rgba(255,255,255,.7); padding:.5rem 1.5rem;
+ cursor:pointer; font-family:inherit; font-size:.85rem;
+ transition:all .2s;
+}
+.em-close:hover { color:#fff; background:rgba(255,255,255,.2); }
+
+/* Emergency theme – tints entire app */
+body.emergency-theme { --bg1:#1a0a0a; --bg2:#200e0e; }
+body.emergency-theme .bottom-nav { border-top-color:rgba(231,76,60,.5); }
+
+/* ── Achievement Toast ──────────────────────────────────────── */
+.achievement-toast {
+ position:fixed; bottom:80px; left:1rem; right:1rem;
+ z-index:99999;
+ display:flex; align-items:center; gap:.8rem;
+ background:linear-gradient(135deg,#f39c12,#e67e22);
+ border-radius:calc(var(--r)*1.5);
+ padding:.8rem 1rem;
+ box-shadow:0 8px 24px rgba(243,156,18,.4);
+ animation:achievePop 4s ease forwards;
+ max-width:360px; margin:0 auto;
+}
+.achievement-toast.hidden { display:none; }
+.at-icon { font-size:2rem; flex-shrink:0; }
+.at-title { font-size:.95rem; font-weight:800; color:#fff; }
+.at-sub { font-size:.78rem; color:rgba(255,255,255,.85); margin-top:.1rem; }
+
+/* ── Points Popup (floating) ────────────────────────────────── */
+.points-popup {
+ animation:pointsFloat 2s ease forwards;
+}
+.pp-pts { font-size:1rem; font-weight:900; }
+.pp-label { font-size:.75rem; margin-right:.4rem; }
+
+/* ── Leaderboard button in bottom nav or quick actions ─────── */
+.nav-lb-dot {
+ position:absolute; top:2px; left:2px;
+ width:8px; height:8px; border-radius:50%;
+ background:#f39c12;
+}
+
+/* ── Referral Banner ─────────────────────────────────────────── */
+.referral-banner {
+ background:linear-gradient(135deg,rgba(26,188,156,.12),rgba(52,152,219,.08));
+ border:1px solid rgba(26,188,156,.25);
+ border-radius:var(--r);
+ padding:.8rem 1rem;
+ display:flex; align-items:center; gap:.8rem;
+ cursor:pointer; transition:all .2s;
+ margin-top:.5rem;
+}
+.referral-banner:hover { transform:translateY(-1px); }
+.rb-icon { font-size:1.5rem; }
+.rb-text { flex:1; }
+.rb-title { font-size:.9rem; font-weight:700; color:var(--text); }
+.rb-sub { font-size:.75rem; color:var(--text2); margin-top:.1rem; }
+.rb-pts { font-size:.85rem; color:var(--teal); font-weight:700; }
+
+
+/* ================================================================
+ 📸 PROFILE PHOTO UPLOAD + CARD REDESIGN
+ صورة الملف الشخصي + إعادة تصميم البطاقات
+================================================================ */
+
+/* ── Profile Photo Display (main view) ──────────────────────── */
+.profile-photo-display {
+ width:80px; height:80px; border-radius:50%;
+ object-fit:cover;
+ border:3px solid var(--teal);
+ box-shadow:0 4px 16px rgba(26,188,156,.35);
+ cursor:pointer;
+ transition:transform .2s, box-shadow .2s;
+}
+.profile-photo-display:hover {
+ transform:scale(1.07);
+ box-shadow:0 6px 20px rgba(26,188,156,.5);
+}
+
+/* ── Photo Section in Edit Form ─────────────────────────────── */
+.pe-photo-section {
+ display:flex; align-items:center; gap:1rem;
+ padding:.8rem 0 1rem;
+}
+.pe-photo-wrap { flex-shrink:0; }
+.pe-photo-ring {
+ position:relative;
+ width:90px; height:90px;
+ border-radius:50%;
+}
+.pe-photo-avatar {
+ width:90px; height:90px; border-radius:50%;
+ background:linear-gradient(135deg,var(--teal),#3498db);
+ display:flex; align-items:center; justify-content:center;
+ font-size:2rem; font-weight:900; color:#fff;
+ border:3px solid rgba(26,188,156,.4);
+ cursor:pointer; transition:all .2s;
+ box-shadow:0 4px 14px rgba(0,0,0,.3);
+}
+.pe-photo-avatar:hover { transform:scale(1.05); box-shadow:0 6px 18px rgba(26,188,156,.4); }
+.pe-photo-img {
+ width:90px; height:90px; border-radius:50%;
+ object-fit:cover;
+ border:3px solid var(--teal);
+ cursor:pointer;
+ box-shadow:0 4px 14px rgba(26,188,156,.3);
+ transition:all .2s;
+}
+.pe-photo-img:hover { transform:scale(1.05); }
+.pe-photo-btns {
+ display:flex; gap:.35rem; margin-top:.5rem;
+ justify-content:center;
+}
+.pe-photo-btn {
+ padding:.3rem .55rem;
+ border:1px solid var(--border);
+ border-radius:20px;
+ background:rgba(255,255,255,.05);
+ cursor:pointer; font-family:inherit;
+ font-size:.68rem; font-weight:600;
+ color:var(--text2); transition:all .2s;
+ white-space:nowrap;
+}
+.pe-photo-btn:hover { color:var(--text); border-color:var(--teal); background:rgba(26,188,156,.08); }
+.pe-photo-cam:hover { border-color:#e67e22; color:#e67e22; }
+.pe-photo-gal:hover { border-color:#3498db; color:#3498db; }
+.pe-photo-emoji:hover{ border-color:#9b59b6; color:#9b59b6; }
+
+/* ── Emoji Avatar Picker ──────────────────────────────────────── */
+.emoji-avatar-picker {
+ background:var(--bg2);
+ border:1px solid var(--border);
+ border-radius:var(--r);
+ padding:1rem;
+ margin-bottom:.8rem;
+ animation:slideDown .3s ease;
+}
+.emoji-avatar-picker.hidden { display:none; }
+.eap-title {
+ font-size:.85rem; font-weight:700; color:var(--text);
+ margin-bottom:.7rem; text-align:center;
+}
+.eap-grid {
+ display:grid; grid-template-columns:repeat(7,1fr);
+ gap:.4rem; margin-bottom:.7rem;
+}
+.eap-btn {
+ width:36px; height:36px; border-radius:50%;
+ border:2px solid transparent;
+ background:rgba(255,255,255,.05);
+ font-size:1.2rem; cursor:pointer;
+ display:flex; align-items:center; justify-content:center;
+ transition:all .15s;
+}
+.eap-btn:hover { border-color:var(--teal); background:rgba(26,188,156,.1); transform:scale(1.15); }
+.eap-btn.active-eap { border-color:var(--teal); background:rgba(26,188,156,.2); }
+.eap-close {
+ width:100%; padding:.5rem;
+ background:rgba(255,255,255,.06);
+ border:1px solid var(--border);
+ border-radius:var(--r);
+ color:var(--text2); cursor:pointer;
+ font-family:inherit; font-size:.82rem;
+ transition:all .2s;
+}
+.eap-close:hover { color:var(--text); background:rgba(255,255,255,.1); }
+
+/* ── Info Cards Redesign ───────────────────────────────────────── */
+/* Override existing pib styles for more visual appeal */
+.profile-info-block {
+ background:var(--card) !important;
+ border:1px solid var(--border) !important;
+ border-radius:calc(var(--r)*1.2) !important;
+ margin-bottom:.8rem !important;
+ overflow:hidden !important;
+ box-shadow:0 2px 12px rgba(0,0,0,.15) !important;
+ transition:box-shadow .2s !important;
+}
+.profile-info-block:hover { box-shadow:0 4px 20px rgba(0,0,0,.25) !important; }
+.pib-title {
+ font-size:.82rem !important; font-weight:800 !important;
+ color:var(--teal) !important;
+ padding:.65rem 1rem !important;
+ border-bottom:1px solid var(--border) !important;
+ background:rgba(26,188,156,.06) !important;
+ letter-spacing:.02em !important;
+}
+.pib-row {
+ display:flex !important; align-items:center !important; gap:.7rem !important;
+ padding:.65rem 1rem !important;
+ border-bottom:1px solid rgba(255,255,255,.04) !important;
+ transition:background .15s !important;
+}
+.pib-row:last-child { border-bottom:none !important; }
+.pib-row:hover { background:rgba(255,255,255,.03) !important; }
+.pib-icon { font-size:1.1rem; flex-shrink:0; width:1.5rem; text-align:center; }
+.pib-content { flex:1; min-width:0; }
+.pib-label { font-size:.7rem; color:var(--text2); display:block; margin-bottom:.1rem; }
+.pib-val {
+ font-size:.88rem; color:var(--text);
+ font-weight:600; word-break:break-word;
+}
+.pib-val-highlight { color:var(--teal) !important; font-size:1rem !important; font-weight:800 !important; }
+.pib-val-link { color:#3498db !important; text-decoration:none !important; }
+.pib-val-link:hover { text-decoration:underline !important; }
+.pib-action-btn {
+ background:rgba(255,255,255,.06) !important;
+ border:1px solid var(--border) !important;
+ border-radius:20px !important;
+ padding:.25rem .6rem !important;
+ font-size:.75rem !important; font-weight:600 !important;
+ color:var(--text2) !important; cursor:pointer !important;
+ transition:all .2s !important; white-space:nowrap !important;
+ text-decoration:none !important;
+ display:inline-flex !important; align-items:center !important; gap:.2rem !important;
+}
+.pib-action-btn:hover { color:var(--text) !important; border-color:var(--teal) !important; background:rgba(26,188,156,.1) !important; }
+.pib-btn-green { border-color:rgba(46,204,113,.4) !important; color:#2ecc71 !important; }
+.pib-btn-blue { border-color:rgba(52,152,219,.4) !important; color:#3498db !important; }
+.pib-bio { line-height:1.5 !important; white-space:pre-wrap !important; }
+
+/* ── Badges Block Redesign ────────────────────────────────────── */
+.profile-badges-block {
+ background:var(--card) !important;
+ border:1px solid var(--border) !important;
+ border-radius:calc(var(--r)*1.2) !important;
+ padding:.8rem 1rem !important;
+ margin-bottom:.8rem !important;
+ box-shadow:0 2px 12px rgba(0,0,0,.15) !important;
+}
+.profile-badges-grid { display:flex; flex-wrap:wrap; gap:.5rem; margin-top:.5rem; }
+.activity-badge {
+ padding:.3rem .75rem;
+ border-radius:20px;
+ font-size:.75rem; font-weight:700;
+ border:1px solid;
+ display:inline-flex; align-items:center; gap:.25rem;
+ white-space:nowrap;
+}
+.ab-new { border-color:rgba(26,188,156,.4); color:var(--teal); background:rgba(26,188,156,.08); }
+.ab-report { border-color:rgba(231,76,60,.4); color:#e74c3c; background:rgba(231,76,60,.08); }
+.ab-active { border-color:rgba(243,156,18,.4); color:#f39c12; background:rgba(243,156,18,.08); }
+.ab-veteran { border-color:rgba(155,89,182,.4); color:#9b59b6; background:rgba(155,89,182,.08); }
+.ab-contact { border-color:rgba(46,204,113,.4); color:#2ecc71; background:rgba(46,204,113,.08); }
+.ab-pro { border-color:rgba(52,152,219,.4); color:#3498db; background:rgba(52,152,219,.08); }
+.ab-bio { border-color:rgba(241,196,15,.4); color:#f1c40f; background:rgba(241,196,15,.08); }
+.ab-verified { border-color:rgba(26,188,156,.6); color:var(--teal); background:rgba(26,188,156,.15); }
+
+/* ── Quick Actions Grid Redesign ─────────────────────────────── */
+.profile-quick-actions-grid {
+ display:grid; grid-template-columns:repeat(4,1fr);
+ gap:.5rem; margin-bottom:.8rem;
+}
+.pqa-btn {
+ background:var(--card) !important;
+ border:1px solid var(--border) !important;
+ border-radius:calc(var(--r)*1.2) !important;
+ padding:.75rem .4rem !important;
+ display:flex; flex-direction:column;
+ align-items:center; gap:.35rem;
+ cursor:pointer; font-family:inherit;
+ transition:all .2s !important;
+ box-shadow:0 2px 8px rgba(0,0,0,.1) !important;
+}
+.pqa-btn:hover {
+ background:rgba(26,188,156,.08) !important;
+ border-color:rgba(26,188,156,.3) !important;
+ transform:translateY(-2px) !important;
+ box-shadow:0 4px 14px rgba(26,188,156,.2) !important;
+}
+.pqa-icon { font-size:1.3rem; }
+.pqa-lbl { font-size:.68rem; font-weight:700; color:var(--text2); text-align:center; line-height:1.2; }
+
+/* ── Stats Strip Redesign ────────────────────────────────────── */
+.profile-stats-strip {
+ display:grid; grid-template-columns:repeat(4,1fr) !important;
+ background:var(--card) !important;
+ border:1px solid var(--border) !important;
+ border-radius:calc(var(--r)*1.2) !important;
+ margin:.6rem 0 !important;
+ overflow:hidden !important;
+ box-shadow:0 2px 10px rgba(0,0,0,.1) !important;
+}
+.pss-item {
+ display:flex; flex-direction:column; align-items:center;
+ padding:.7rem .4rem;
+ transition:background .15s;
+}
+.pss-item:hover { background:rgba(255,255,255,.04); }
+.pss-divider { width:1px; background:var(--border); align-self:stretch; }
+.pss-num { font-size:1.1rem; font-weight:900; color:var(--teal); }
+.pss-lbl { font-size:.62rem; color:var(--text2); margin-top:.15rem; text-align:center; }
+
+/* ── Main Actions Row Redesign ───────────────────────────────── */
+.profile-main-actions {
+ display:grid; grid-template-columns:repeat(3,1fr) !important;
+ gap:.45rem; margin:.5rem 0 !important;
+}
+.pma-btn {
+ display:flex; flex-direction:column; align-items:center; gap:.3rem !important;
+ background:var(--card) !important;
+ border:1px solid var(--border) !important;
+ border-radius:calc(var(--r)*1.2) !important;
+ padding:.65rem .4rem !important;
+ cursor:pointer; font-family:inherit;
+ transition:all .2s !important;
+ box-shadow:0 2px 8px rgba(0,0,0,.1) !important;
+}
+.pma-btn:hover {
+ transform:translateY(-2px) !important;
+ box-shadow:0 4px 14px rgba(0,0,0,.25) !important;
+}
+.pma-msg:hover { border-color:rgba(26,188,156,.5) !important; background:rgba(26,188,156,.08) !important; }
+.pma-map:hover { border-color:rgba(52,152,219,.5) !important; background:rgba(52,152,219,.08) !important; }
+.pma-share:hover { border-color:rgba(155,89,182,.5) !important; background:rgba(155,89,182,.08) !important; }
+.pma-qr:hover { border-color:rgba(243,156,18,.5) !important; background:rgba(243,156,18,.08) !important; }
+.pma-sos:hover { border-color:rgba(231,76,60,.5) !important; background:rgba(231,76,60,.08) !important; }
+.pma-report:hover{ border-color:rgba(231,76,60,.5) !important; background:rgba(231,76,60,.08) !important; }
+.pma-icon { font-size:1.3rem; }
+.pma-lbl { font-size:.7rem; font-weight:700; color:var(--text2); }
+
+/* ── Identity Card Redesign ──────────────────────────────────── */
+.profile-identity-card {
+ background:var(--card) !important;
+ border-radius:0 0 calc(var(--r)*1.5) calc(var(--r)*1.5) !important;
+ padding:0 1rem 1.2rem !important;
+ position:relative !important;
+ box-shadow:0 4px 16px rgba(0,0,0,.2) !important;
+}
+.profile-hero-badges {
+ display:flex; flex-wrap:wrap; gap:.3rem; margin-top:.4rem;
+}
+.profile-badge {
+ padding:.2rem .6rem;
+ border-radius:20px;
+ font-size:.7rem; font-weight:700;
+ border:1px solid;
+}
+.pbl { border-color:rgba(26,188,156,.4); color:var(--teal); background:rgba(26,188,156,.08); }
+.pbd { border-color:rgba(46,204,113,.4); color:#2ecc71; background:rgba(46,204,113,.08); }
+.pblvl{ border-color:rgba(243,156,18,.4); color:#f39c12; background:rgba(243,156,18,.08); }
+
+/* ── Photo upload progress overlay ──────────────────────────── */
+.photo-upload-overlay {
+ position:absolute; inset:0; border-radius:50%;
+ background:rgba(0,0,0,.6);
+ display:flex; align-items:center; justify-content:center;
+ font-size:.7rem; font-weight:700; color:#fff;
+ pointer-events:none;
+}
+
+/* ── Points card in profile (injected) ──────────────────────── */
+.profile-points-card {
+ background:linear-gradient(135deg,rgba(26,188,156,.1),rgba(52,152,219,.07));
+ border:1px solid rgba(26,188,156,.25);
+ border-radius:calc(var(--r)*1.2);
+ padding:1rem;
+ margin-bottom:.8rem;
+ box-shadow:0 2px 12px rgba(26,188,156,.1);
+}
+.ppc-header { display:flex; align-items:center; gap:.8rem; margin-bottom:.7rem; }
+.ppc-level-icon { font-size:2rem; }
+.ppc-info { flex:1; }
+.ppc-title { font-size:.95rem; font-weight:800; color:var(--text); }
+.ppc-pts { font-size:.82rem; color:var(--teal); font-weight:700; }
+.ppc-streak{ font-size:.78rem; color:#e67e22; margin-top:.15rem; }
+.ppc-badges-row { display:flex; flex-wrap:wrap; gap:.4rem; margin-bottom:.6rem; }
+.ppc-badge-chip {
+ font-size:1.1rem; width:34px; height:34px;
+ border-radius:50%;
+ background:rgba(255,255,255,.06);
+ border:1px solid var(--border);
+ display:flex; align-items:center; justify-content:center;
+ cursor:default;
+ transition:transform .15s;
+}
+.ppc-badge-chip:hover { transform:scale(1.2); }
+.ppc-actions { display:flex; gap:.5rem; }
+.ppc-act-btn {
+ flex:1; padding:.5rem;
+ background:rgba(255,255,255,.05);
+ border:1px solid var(--border);
+ border-radius:var(--r);
+ cursor:pointer; font-family:inherit;
+ font-size:.75rem; font-weight:700;
+ transition:all .2s; text-align:center;
+}
+.ppc-act-btn:hover { background:rgba(26,188,156,.1); border-color:var(--teal); color:var(--teal); }
+
+/* ── Form card section title ─────────────────────────────────── */
+.pe-section-title {
+ font-size:.82rem; font-weight:800;
+ color:var(--teal);
+ margin-bottom:.55rem;
+ padding-bottom:.4rem;
+ border-bottom:1px solid rgba(26,188,156,.15);
+}
+.form-card {
+ background:rgba(255,255,255,.03);
+ border:1px solid var(--border);
+ border-radius:var(--r);
+ padding:.85rem;
+ margin-bottom:.8rem;
+}
+.form-card .inp { margin-bottom:.5rem; }
+.form-card .inp:last-child { margin-bottom:0; }
+
+/* ── Pe edit form header ─────────────────────────────────────── */
+.pe-header {
+ display:flex; align-items:center; justify-content:space-between;
+ margin-bottom:1rem; padding-bottom:.7rem;
+ border-bottom:1px solid var(--border);
+ font-size:.95rem; font-weight:800; color:var(--text);
+}
+.pe-close-btn {
+ width:28px; height:28px; border-radius:50%;
+ background:rgba(255,255,255,.08); border:1px solid var(--border);
+ cursor:pointer; color:var(--text2); font-size:.85rem;
+ display:flex; align-items:center; justify-content:center;
+ transition:all .2s;
+}
+.pe-close-btn:hover { background:rgba(231,76,60,.15); color:#e74c3c; border-color:#e74c3c; }
+
+
+
+/* ================================================================
+ 📱 PROFILE — WhatsApp Style 2026 (نهائي نظيف)
+================================================================ */
+
+#sec-profile { background:#111b21; min-height:100vh; padding-bottom:80px; }
+
+/* ── COVER ── */
+.pv4-cover {
+ position:relative; height:195px; overflow:hidden;
+ background:linear-gradient(160deg,#0d2b22 0%,#0c1e26 55%,#111b21 100%);
+}
+.pv4-cover::after {
+ content:''; position:absolute; inset:0;
+ background:linear-gradient(to bottom,transparent 45%,#111b21 100%);
+ pointer-events:none;
+}
+.pv4-cover-mesh {
+ position:absolute; inset:0;
+ background:radial-gradient(circle at 30% 50%,rgba(0,168,132,.18) 0%,transparent 60%),
+ radial-gradient(circle at 80% 20%,rgba(52,152,219,.12) 0%,transparent 55%);
+}
+.pv4-cover-glow { position:absolute; inset:0; pointer-events:none; }
+.pv4-cover-glow::before {
+ content:''; position:absolute; top:18px; right:28px;
+ width:110px; height:110px; border-radius:50%;
+ background:radial-gradient(circle,rgba(0,168,132,.22) 0%,transparent 70%);
+ animation:glowPulse 4s ease-in-out infinite;
+}
+.pv4-cover-glow::after {
+ content:''; position:absolute; top:45px; left:18px;
+ width:75px; height:75px; border-radius:50%;
+ background:radial-gradient(circle,rgba(52,152,219,.15) 0%,transparent 70%);
+ animation:glowPulse 4s ease-in-out infinite .8s;
+}
+@keyframes glowPulse{0%,100%{opacity:.6;transform:scale(1)}50%{opacity:1;transform:scale(1.1)}}
+
+.pv4-cover-top {
+ position:absolute; top:0; left:0; right:0;
+ display:flex; justify-content:space-between; align-items:center;
+ padding:.65rem .85rem; z-index:5;
+}
+.pv4-icon-btn {
+ background:rgba(17,27,33,.55); border:1px solid rgba(255,255,255,.1);
+ backdrop-filter:blur(8px); color:#e9edef;
+ width:36px; height:36px; border-radius:50%; cursor:pointer;
+ display:flex; align-items:center; justify-content:center; transition:all .2s;
+}
+.pv4-icon-btn:hover { background:rgba(0,168,132,.3); border-color:rgba(0,168,132,.5); }
+.pv4-icon-btn svg { width:15px; height:15px; }
+.pv4-edit-btn { background:rgba(0,168,132,.2); border-color:rgba(0,168,132,.45); }
+.pv4-level-chip {
+ display:flex; align-items:center; gap:.3rem;
+ background:rgba(17,27,33,.65); backdrop-filter:blur(8px);
+ border:1px solid rgba(0,168,132,.3);
+ padding:.28rem .75rem; border-radius:20px;
+ font-size:.73rem; color:#00a884; font-weight:700;
+}
+
+/* ── AVATAR ── */
+.pv4-avatar-stage {
+ position:absolute; bottom:-2px; left:50%; transform:translateX(-50%);
+ z-index:6;
+}
+.pv4-xp-ring-wrap { position:relative; width:108px; height:108px; }
+.pv4-xp-svg {
+ position:absolute; top:0; left:0; width:108px; height:108px;
+ transform:rotate(-90deg);
+}
+.pv4-xp-track { fill:none; stroke:#1f2c34; stroke-width:4; }
+.pv4-xp-arc {
+ fill:none; stroke-width:4; stroke-linecap:round;
+ stroke-dasharray:0 339.3;
+ transition:stroke-dasharray .8s cubic-bezier(.4,0,.2,1);
+}
+.pv4-avatar-wrap {
+ position:absolute; top:4px; left:4px;
+ width:100px; height:100px; border-radius:50%;
+ cursor:pointer; border:3px solid #111b21; background:#1f2c34;
+ overflow:hidden; transition:all .25s;
+ box-shadow:0 4px 20px rgba(0,0,0,.45);
+}
+.pv4-avatar-wrap:hover { transform:scale(1.04); box-shadow:0 0 0 2px #00a884,0 8px 28px rgba(0,0,0,.5); }
+.pv4-avatar-emoji { width:100%; height:100%; display:flex; align-items:center; justify-content:center; font-size:2.8rem; }
+.pv4-avatar-photo { width:100%; height:100%; object-fit:cover; display:block; }
+.pv4-avatar-lens {
+ position:absolute; inset:0; background:rgba(0,0,0,.5);
+ display:flex; align-items:center; justify-content:center;
+ font-size:1.4rem; opacity:0; transition:opacity .2s; border-radius:50%;
+}
+.pv4-avatar-wrap:hover .pv4-avatar-lens { opacity:1; }
+.pv4-online-dot {
+ position:absolute; bottom:6px; right:6px;
+ width:14px; height:14px; border-radius:50%;
+ background:#00a884; border:2px solid #111b21;
+}
+
+/* ── IDENTITY CARD ── */
+.pv4-id-card {
+ text-align:center; padding:3.8rem 1rem .9rem; background:#111b21;
+}
+.pv4-name-row {
+ display:flex; align-items:center; justify-content:center; gap:.5rem; margin-bottom:.3rem;
+}
+.pv4-name { font-size:1.35rem; font-weight:800; color:#e9edef; letter-spacing:-.3px; margin:0; }
+.pv4-verified { font-size:1rem; }
+.pv4-jobtitle { font-size:.85rem; color:#8696a0; margin-bottom:.35rem; }
+.pv4-location {
+ display:flex; align-items:center; justify-content:center; gap:.3rem;
+ font-size:.82rem; color:#8696a0; margin-bottom:.6rem;
+}
+.pv4-location svg { opacity:.7; }
+.pv4-chips-row {
+ display:flex; align-items:center; justify-content:center;
+ flex-wrap:wrap; gap:.4rem;
+}
+.pv4-chip { padding:.2rem .7rem; border-radius:20px; font-size:.7rem; font-weight:600; }
+.pv4-chip-nabdh { background:rgba(0,168,132,.15); color:#00a884; border:1px solid rgba(0,168,132,.25); }
+.pv4-chip-online { background:rgba(0,200,81,.1); color:#00c851; border:1px solid rgba(0,200,81,.2); }
+.pv4-chip-level { background:rgba(52,152,219,.1); color:#3498db; border:1px solid rgba(52,152,219,.2); }
+
+/* ── XP BAR ── */
+.pv4-xp-bar-wrap {
+ margin:.5rem 1rem; padding:.8rem 1rem;
+ background:#1f2c34; border-radius:12px; border:1px solid #222d34;
+}
+.pv4-xp-meta { display:flex; justify-content:space-between; align-items:center; margin-bottom:.5rem; }
+.pv4-xp-label { font-size:.73rem; color:#8696a0; }
+.pv4-xp-pts { font-size:.73rem; color:#00a884; font-weight:700; }
+.pv4-xp-track { width:100%; height:6px; background:#2a3942; border-radius:6px; overflow:hidden; }
+.pv4-xp-fill {
+ height:100%; background:linear-gradient(90deg,#00a884,#25d366);
+ border-radius:6px; transition:width .8s cubic-bezier(.4,0,.2,1);
+ position:relative; overflow:hidden;
+}
+.pv4-xp-shimmer {
+ position:absolute; top:0; left:-60%; width:60%; height:100%;
+ background:linear-gradient(90deg,transparent,rgba(255,255,255,.35),transparent);
+ animation:shimmer 2.2s infinite 1s;
+}
+@keyframes shimmer{0%{left:-60%}100%{left:120%}}
+.pv4-streak-row {
+ display:flex; align-items:center; gap:.4rem;
+ margin-top:.5rem; font-size:.77rem; color:#f1c40f;
+}
+.pv4-streak-fire { font-size:.9rem; }
+
+/* ── STATS ── */
+.pv4-stats-row {
+ display:grid; grid-template-columns:repeat(4,1fr);
+ margin:.5rem 1rem; background:#1f2c34;
+ border-radius:12px; border:1px solid #222d34; overflow:hidden;
+}
+.pv4-stat {
+ padding:.85rem .4rem; text-align:center;
+ transition:background .18s; position:relative;
+}
+.pv4-stat:not(:last-child)::after {
+ content:''; position:absolute; top:20%; right:0;
+ height:60%; width:1px; background:#222d34;
+}
+.pv4-stat:hover { background:#2a3942; }
+.pv4-stat-num { display:block; font-size:1.2rem; font-weight:800; color:#e9edef; line-height:1; }
+.pv4-stat-lbl { font-size:.68rem; color:#8696a0; margin-top:.28rem; }
+
+/* ── PUBLIC PHONE ── */
+.pv4-pub-phone {
+ display:flex; align-items:center; justify-content:space-between;
+ margin:.5rem 1rem; background:#1f2c34; border-radius:12px;
+ padding:.8rem 1rem; border:1px solid rgba(0,168,132,.2);
+}
+.pv4-pub-phone-left { display:flex; align-items:center; gap:.7rem; }
+.pv4-pub-phone-icon { font-size:1.4rem; }
+.pv4-pub-phone-lbl { font-size:.7rem; color:#8696a0; }
+.pv4-pub-phone-num { font-size:.92rem; font-weight:700; color:#e9edef; }
+.pv4-call-btn {
+ background:#00a884; color:#fff; border:none;
+ padding:.44rem .95rem; border-radius:20px;
+ cursor:pointer; font-size:.8rem; font-weight:600;
+ display:flex; align-items:center; gap:.4rem; transition:all .2s;
+}
+.pv4-call-btn:hover { background:#008f72; transform:scale(1.04); }
+
+/* ── ACTION BUTTONS ── */
+.pv4-actions {
+ display:grid; grid-template-columns:repeat(3,1fr);
+ gap:.5rem; margin:.5rem 1rem;
+}
+.pv4-act {
+ background:#1f2c34; border:1px solid #222d34;
+ border-radius:12px; padding:.72rem .4rem;
+ cursor:pointer; display:flex; flex-direction:column;
+ align-items:center; gap:.38rem; transition:all .2s; color:#d1d7db;
+}
+.pv4-act:hover { background:#2a3942; border-color:#00a884; transform:translateY(-1px); }
+.pv4-act-ico { width:38px; height:38px; border-radius:50%; display:flex; align-items:center; justify-content:center; }
+.pv4-act-ico svg { width:17px; height:17px; }
+.pv4-act span { font-size:.7rem; color:#8696a0; }
+.pv4-act-msg { background:rgba(0,168,132,.15); color:#00a884; }
+.pv4-act-map { background:rgba(52,152,219,.15); color:#3498db; }
+.pv4-act-share { background:rgba(155,89,182,.15); color:#9b59b6; }
+.pv4-act-qr { background:rgba(241,196,15,.12); color:#f1c40f; }
+.pv4-act-sos { background:rgba(231,76,60,.12); color:#e74c3c; }
+.pv4-act-report { background:rgba(230,126,34,.12); color:#e67e22; }
+
+/* ── SCROLL AREA ── */
+.pv4-scroll-area { padding:.5rem 1rem 1rem; }
+
+/* ── INFO CARDS ── */
+.pv4-info-card {
+ background:#1f2c34; border-radius:12px;
+ border:1px solid #222d34; overflow:hidden; margin-bottom:.6rem;
+}
+.pv4-info-head {
+ display:flex; align-items:center; gap:.65rem;
+ padding:.72rem 1rem; border-bottom:1px solid #222d34;
+ background:rgba(0,168,132,.05);
+}
+.pv4-info-icon { font-size:1rem; }
+.pv4-info-title { font-size:.83rem; font-weight:700; color:#00a884; }
+.pv4-info-row {
+ display:flex; align-items:center; gap:.75rem;
+ padding:.72rem 1rem; border-bottom:1px solid #1a2429;
+ transition:background .15s;
+}
+.pv4-info-row:last-child { border-bottom:none; }
+.pv4-info-row:hover { background:#2a3942; }
+.pv4-row-icon-wrap {
+ width:36px; height:36px; border-radius:50%;
+ background:#2a3942; display:flex; align-items:center; justify-content:center;
+ font-size:.93rem; flex-shrink:0;
+}
+.pv4-bg-green { background:rgba(0,168,132,.15)!important; }
+.pv4-bg-blue { background:rgba(52,152,219,.12)!important; }
+.pv4-row-body { flex:1; min-width:0; }
+.pv4-row-lbl { font-size:.68rem; color:#8696a0; display:block; }
+.pv4-row-val { font-size:.86rem; color:#e9edef; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; display:block; }
+.pv4-row-green { background:rgba(0,168,132,.04); }
+.pv4-val-green { color:#00a884!important; }
+.pv4-val-link { color:#3498db!important; text-decoration:none; }
+.pv4-val-link:hover { text-decoration:underline; }
+.pv4-bio-val { white-space:normal!important; font-size:.82rem!important; color:#8696a0!important; }
+.pv4-row-action {
+ background:#2a3942; border:none; color:#8696a0;
+ width:33px; height:33px; border-radius:50%; cursor:pointer; font-size:.9rem;
+ display:flex; align-items:center; justify-content:center;
+ transition:all .2s; text-decoration:none; flex-shrink:0;
+}
+.pv4-row-action:hover { background:#00a884; color:#fff; }
+.pv4-btn-green { background:rgba(0,168,132,.15)!important; color:#00a884!important; }
+.pv4-btn-green:hover { background:#00a884!important; color:#fff!important; }
+.pv4-btn-blue { background:rgba(52,152,219,.15)!important; color:#3498db!important; }
+.pv4-btn-blue:hover { background:#3498db!important; color:#fff!important; }
+
+/* ── POINTS CARD ── */
+.pv4-points-card {
+ background:#1f2c34; border-radius:14px;
+ border:1px solid rgba(0,168,132,.2);
+ padding:1rem; margin-bottom:.7rem;
+ position:relative; overflow:hidden;
+}
+.pv4-pc-glow {
+ position:absolute; top:-30px; right:-30px;
+ width:120px; height:120px; border-radius:50%;
+ background:radial-gradient(circle,rgba(0,168,132,.15) 0%,transparent 70%);
+ pointer-events:none;
+}
+.pv4-pc-top { display:flex; align-items:center; gap:.85rem; margin-bottom:.85rem; }
+.pv4-pc-level-wrap {
+ width:54px; height:54px; border-radius:50%;
+ background:rgba(0,168,132,.1); border:2px solid rgba(0,168,132,.3);
+ display:flex; align-items:center; justify-content:center;
+ font-size:1.55rem; position:relative; flex-shrink:0;
+}
+.pv4-pc-level-ring {
+ position:absolute; inset:-3px; border-radius:50%;
+ border:2px solid transparent;
+ background:linear-gradient(#1f2c34,#1f2c34) padding-box,
+ linear-gradient(135deg,#00a884,#25d366) border-box;
+}
+.pv4-pc-info { flex:1; }
+.pv4-pc-title { font-size:.98rem; font-weight:800; color:#e9edef; }
+.pv4-pc-pts { font-size:.8rem; color:#00a884; font-weight:700; }
+.pv4-pc-streak { font-size:.73rem; color:#f1c40f; margin-top:.2rem; }
+.pv4-lb-btn {
+ background:#2a3942; border:1px solid #222d34; color:#8696a0;
+ padding:.38rem .72rem; border-radius:8px; cursor:pointer; font-size:.73rem;
+ display:flex; align-items:center; gap:.3rem; transition:all .2s;
+}
+.pv4-lb-btn:hover { background:#00a884; color:#fff; border-color:#00a884; }
+
+/* Activity strip */
+.pv4-activity-strip {
+ margin-bottom:.65rem; padding:.65rem .75rem;
+ background:#2a3942; border:1px solid #222d34; border-radius:12px;
+}
+.pv4-activity-head { display:flex; align-items:center; justify-content:space-between; margin-bottom:.6rem; }
+.pv4-activity-title { display:flex; align-items:center; gap:.4rem; font-size:.76rem; font-weight:700; color:#8696a0; }
+.pv4-activity-dots { display:flex; gap:.3rem; align-items:center; }
+.pv4-activity-dots span { width:6px; height:6px; border-radius:50%; background:#1f2c34; }
+.pv4-activity-dots span.active { background:#00a884; }
+.pv4-activity-bars { display:flex; align-items:flex-end; gap:4px; height:38px; }
+.pv4-abar {
+ flex:1; background:#1f2c34; border-radius:3px 3px 0 0;
+ position:relative; transition:height .6s cubic-bezier(.4,0,.2,1); min-height:4px;
+}
+.pv4-abar.active { background:linear-gradient(to top,#00a884,#25d366); }
+.pv4-abar span {
+ position:absolute; bottom:-16px; left:50%; transform:translateX(-50%);
+ font-size:.54rem; color:#8696a0; white-space:nowrap;
+}
+
+/* Badges */
+.pv4-pc-badges { display:flex; flex-wrap:wrap; gap:.4rem; margin-bottom:.8rem; }
+.pv4-badge {
+ padding:.23rem .68rem; border-radius:20px; font-size:.7rem; font-weight:700;
+ background:rgba(0,168,132,.12); color:#00a884; border:1px solid rgba(0,168,132,.2);
+}
+.pv4-badge-new { background:rgba(52,152,219,.1)!important; color:#3498db!important; border-color:rgba(52,152,219,.2)!important; }
+.pv4-badge-gold { background:rgba(241,196,15,.1)!important; color:#f1c40f!important; border-color:rgba(241,196,15,.2)!important; }
+.pv4-badge-red { background:rgba(231,76,60,.08)!important; color:#e74c3c!important; border-color:rgba(231,76,60,.2)!important; }
+.pv4-badge-hero { background:rgba(155,89,182,.1)!important; color:#9b59b6!important; border-color:rgba(155,89,182,.2)!important; }
+
+/* PC actions */
+.pv4-pc-actions { display:flex; gap:.5rem; }
+.pv4-pc-act {
+ flex:1; display:flex; flex-direction:column; align-items:center; gap:.22rem;
+ background:#2a3942; border:1px solid #222d34; border-radius:10px;
+ padding:.58rem .35rem; cursor:pointer; transition:all .2s;
+ color:#d1d7db; font-size:.7rem;
+}
+.pv4-pc-act:hover { background:#00a884; color:#fff; border-color:#00a884; }
+.pv4-pc-act-ico { font-size:1.05rem; }
+.pv4-pc-act-pts {
+ font-size:.63rem; color:#00a884; font-weight:700;
+ background:rgba(0,168,132,.12); padding:.03rem .28rem; border-radius:5px;
+}
+.pv4-pc-act:hover .pv4-pc-act-pts { color:#fff; background:rgba(255,255,255,.15); }
+
+/* Badges grid */
+.pv4-badges-grid { display:flex; flex-wrap:wrap; gap:.4rem; padding:.72rem 1rem; }
+
+/* Quick grid */
+.pv4-quick-grid {
+ display:grid; grid-template-columns:repeat(3,1fr);
+ gap:.5rem; margin-bottom:.6rem;
+}
+.pv4-quick {
+ background:#1f2c34; border:1px solid #222d34;
+ border-radius:12px; padding:.68rem .35rem;
+ cursor:pointer; display:flex; flex-direction:column;
+ align-items:center; gap:.32rem;
+ transition:all .2s; color:#8696a0; font-size:.73rem;
+}
+.pv4-quick:hover { background:#2a3942; border-color:#00a884; color:#e9edef; }
+.pv4-quick-ico { width:37px; height:37px; border-radius:50%; display:flex; align-items:center; justify-content:center; font-size:1.05rem; }
+.pv4-q-red { background:rgba(231,76,60,.12); color:#e74c3c; }
+.pv4-q-gold { background:rgba(241,196,15,.12); color:#f1c40f; }
+.pv4-q-teal { background:rgba(0,168,132,.12); color:#00a884; }
+.pv4-q-blue { background:rgba(52,152,219,.12); color:#3498db; }
+.pv4-q-purple { background:rgba(155,89,182,.12); color:#9b59b6; }
+.pv4-q-orange { background:rgba(230,126,34,.12); color:#e67e22; }
+
+/* ── EDIT FORM ── */
+.pv4-edit-form {
+ background:#1f2c34; border-radius:14px;
+ border:1px solid #222d34; overflow:hidden; margin-bottom:.6rem;
+}
+.pv4-ef-header {
+ display:flex; align-items:center; justify-content:space-between;
+ padding:.88rem 1rem; background:rgba(0,168,132,.07);
+ border-bottom:1px solid #222d34;
+}
+.pv4-ef-title {
+ display:flex; align-items:center; gap:.5rem;
+ font-size:.9rem; font-weight:700; color:#00a884;
+}
+.pv4-ef-close {
+ background:#2a3942; border:none; color:#8696a0;
+ width:30px; height:30px; border-radius:50%; cursor:pointer; font-size:.85rem;
+ display:flex; align-items:center; justify-content:center; transition:all .2s;
+}
+.pv4-ef-close:hover { background:#e74c3c; color:#fff; }
+
+/* Photo section */
+.pv4-ef-photo-section {
+ display:flex; flex-direction:column; align-items:center;
+ padding:1.1rem 1rem 1rem; background:#111b21; border-bottom:1px solid #222d34;
+}
+.pv4-ef-photo-ring {
+ width:86px; height:86px; border-radius:50%;
+ border:3px solid #00a884; overflow:hidden; cursor:pointer;
+ position:relative; background:#2a3942; transition:all .25s;
+}
+.pv4-ef-photo-ring:hover { border-color:#25d366; transform:scale(1.04); }
+.pv4-ef-avatar { width:100%; height:100%; display:flex; align-items:center; justify-content:center; font-size:2.4rem; }
+.pv4-ef-photo { width:100%; height:100%; object-fit:cover; }
+.pv4-ef-photo-overlay {
+ position:absolute; inset:0; background:rgba(0,0,0,.45);
+ display:flex; align-items:center; justify-content:center;
+ font-size:1.35rem; opacity:0; transition:opacity .2s; border-radius:50%;
+}
+.pv4-ef-photo-ring:hover .pv4-ef-photo-overlay { opacity:1; }
+.pv4-ef-photo-name { margin-top:.58rem; font-size:.88rem; font-weight:700; color:#e9edef; }
+.pv4-ef-photo-btns { display:flex; gap:.45rem; margin-top:.6rem; flex-wrap:wrap; justify-content:center; }
+.pv4-ef-photo-btn {
+ background:#1f2c34; border:1px solid #222d34; color:#8696a0;
+ padding:.4rem .82rem; border-radius:20px; cursor:pointer; font-size:.73rem;
+ display:flex; align-items:center; gap:.32rem; transition:all .2s;
+}
+.pv4-ef-photo-btn:hover { background:#00a884; color:#fff; border-color:#00a884; }
+
+/* Emoji picker */
+.emoji-avatar-picker { padding:.82rem; border-bottom:1px solid #222d34; }
+.eap-title { font-size:.8rem; font-weight:700; color:#00a884; margin-bottom:.6rem; text-align:center; }
+.eap-grid { display:grid; grid-template-columns:repeat(6,1fr); gap:.38rem; max-height:155px; overflow-y:auto; }
+.eap-grid button {
+ background:#2a3942; border:1px solid #222d34;
+ font-size:1.35rem; padding:.38rem; border-radius:8px; cursor:pointer;
+ transition:all .15s; aspect-ratio:1;
+}
+.eap-grid button:hover { background:#00a884; transform:scale(1.08); }
+.eap-close {
+ display:block; width:100%; margin-top:.6rem;
+ background:#1f2c34; border:1px solid #222d34; color:#8696a0;
+ padding:.48rem; border-radius:8px; cursor:pointer; font-size:.78rem; transition:all .2s;
+}
+.eap-close:hover { background:#e74c3c; color:#fff; }
+
+/* Form cards */
+.pv4-ef-card { padding:.82rem 1rem; border-bottom:1px solid #222d34; }
+.pv4-ef-card:last-child { border-bottom:none; }
+.pv4-ef-card-head {
+ display:flex; align-items:center; gap:.52rem;
+ font-size:.8rem; font-weight:700; color:#00a884; margin-bottom:.62rem;
+}
+.pv4-ef-card-icon { font-size:.93rem; }
+.pv4-ef-card-green { background:rgba(0,168,132,.03); }
+
+/* Inputs */
+.pv4-inp {
+ width:100%; box-sizing:border-box;
+ background:#111b21; border:1px solid #2a3942;
+ color:#e9edef; padding:.63rem .82rem; border-radius:8px;
+ font-size:.86rem; font-family:inherit; margin-bottom:.48rem;
+ transition:border-color .2s,box-shadow .2s; outline:none;
+}
+.pv4-inp:focus { border-color:#00a884; box-shadow:0 0 0 3px rgba(0,168,132,.1); }
+.pv4-inp::placeholder { color:#4a5568; }
+.pv4-ta { resize:vertical; min-height:70px; }
+.pv4-ef-hint {
+ font-size:.7rem; color:#8696a0;
+ margin-top:-.3rem; margin-bottom:.3rem;
+ padding:.28rem .5rem; background:rgba(0,168,132,.05);
+ border-radius:6px; border-right:2px solid #00a884;
+}
+
+/* Toggles */
+.pv4-toggle-row {
+ display:flex; align-items:center; gap:.82rem;
+ padding:.62rem 0; cursor:pointer; border-bottom:1px solid #1a2429;
+}
+.pv4-toggle-row:last-child { border-bottom:none; }
+.pv4-toggle { position:relative; width:44px; height:24px; flex-shrink:0; }
+.pv4-toggle input { opacity:0; width:0; height:0; position:absolute; }
+.pv4-toggle-thumb {
+ position:absolute; inset:0; border-radius:24px; background:#2a3942; cursor:pointer; transition:all .25s;
+}
+.pv4-toggle-thumb::before {
+ content:''; position:absolute; left:3px; top:3px; width:18px; height:18px;
+ border-radius:50%; background:#8696a0; transition:all .25s;
+}
+.pv4-toggle input:checked + .pv4-toggle-thumb { background:#00a884; }
+.pv4-toggle input:checked + .pv4-toggle-thumb::before { transform:translateX(20px); background:#fff; }
+.pv4-toggle-lbl { font-size:.84rem; color:#d1d7db; }
+.pv4-outline-btn {
+ background:transparent; border:1px solid #00a884; color:#00a884;
+ padding:.53rem 1rem; border-radius:8px; cursor:pointer;
+ font-size:.8rem; font-family:inherit;
+ display:flex; align-items:center; gap:.43rem; transition:all .2s;
+}
+.pv4-outline-btn:hover { background:#00a884; color:#fff; }
+
+/* Footer buttons */
+.pv4-ef-footer {
+ display:flex; gap:.62rem; padding:.82rem 1rem;
+ background:#111b21; border-top:1px solid #222d34;
+}
+.pv4-save-btn {
+ flex:1; background:#00a884; color:#fff; border:none;
+ padding:.68rem; border-radius:8px; cursor:pointer;
+ font-size:.86rem; font-weight:700; font-family:inherit;
+ display:flex; align-items:center; justify-content:center; gap:.43rem;
+ transition:all .2s;
+}
+.pv4-save-btn:hover { background:#008f72; }
+.pv4-save-btn:disabled { opacity:.55; cursor:not-allowed; }
+.pv4-cancel-btn {
+ background:#2a3942; color:#8696a0; border:none;
+ padding:.68rem 1.1rem; border-radius:8px; cursor:pointer;
+ font-size:.86rem; font-family:inherit; transition:all .2s;
+}
+.pv4-cancel-btn:hover { background:#e74c3c; color:#fff; }
+
+/* QR Modal */
+.profile-qr-modal {
+ position:fixed; inset:0; background:rgba(0,0,0,.65); z-index:500;
+ display:flex; align-items:center; justify-content:center;
+ backdrop-filter:blur(4px);
+}
+.profile-qr-modal.hidden { display:none!important; }
+.pqm-inner {
+ background:#1f2c34; border-radius:18px; padding:1.5rem;
+ border:1px solid rgba(0,168,132,.22);
+ width:275px; max-width:90vw; text-align:center; position:relative;
+ box-shadow:0 20px 60px rgba(0,0,0,.5);
+}
+.pqm-close {
+ position:absolute; top:.72rem; left:.72rem;
+ background:#2a3942; border:none; color:#8696a0;
+ width:30px; height:30px; border-radius:50%; cursor:pointer; font-size:.85rem;
+ display:flex; align-items:center; justify-content:center; transition:all .2s;
+}
+.pqm-close:hover { background:#e74c3c; color:#fff; }
+
+/* Old overrides */
+.pro-cover,.profile-cover { display:none!important; }
+
+/* ================================================================
+ 📎 UNIFIED MEDIA MENU — قائمة الوسائط الموحدة v2026
+================================================================ */
+
+/* ── Wrapper (relative anchor for the popup) ─────────────────── */
+.gpi-media-wrap {
+ position: relative;
+ flex-shrink: 0;
+}
+
+/* ── The + attach button ─────────────────────────────────────── */
+.gpi-attach-btn {
+ width: 38px; height: 38px;
+ border-radius: 50%;
+ background: rgba(0,168,132,.12);
+ border: 1.5px solid rgba(0,168,132,.28);
+ color: #00a884;
+ display: flex; align-items: center; justify-content: center;
+ cursor: pointer;
+ transition: all .22s cubic-bezier(.4,0,.2,1);
+ flex-shrink: 0;
+}
+.gpi-attach-btn:hover,
+.gpi-attach-btn.gpi-attach-open {
+ background: #00a884;
+ border-color: #00a884;
+ color: #fff;
+ transform: rotate(45deg) scale(1.08);
+ box-shadow: 0 4px 16px rgba(0,168,132,.35);
+}
+
+/* ── Popup media menu ────────────────────────────────────────── */
+.gpi-media-menu {
+ position: absolute;
+ bottom: calc(100% + 10px);
+ right: 0;
+ background: #1f2c34;
+ border: 1px solid rgba(0,168,132,.22);
+ border-radius: 16px;
+ padding: .5rem .4rem;
+ display: flex;
+ flex-direction: column;
+ gap: .15rem;
+ min-width: 160px;
+ z-index: 300;
+ box-shadow: 0 8px 32px rgba(0,0,0,.45);
+ animation: mediaMenuIn .18s cubic-bezier(.4,0,.2,1);
+}
+.gpi-media-menu.hidden { display: none !important; }
+
+@keyframes mediaMenuIn {
+ from { opacity:0; transform:translateY(8px) scale(.95); }
+ to { opacity:1; transform:translateY(0) scale(1); }
+}
+
+/* ── Each menu row ───────────────────────────────────────────── */
+.gpi-media-item {
+ display: flex;
+ align-items: center;
+ gap: .65rem;
+ padding: .6rem .75rem;
+ border: none;
+ background: transparent;
+ border-radius: 10px;
+ cursor: pointer;
+ color: #e9edef;
+ font-family: inherit;
+ font-size: .88rem;
+ font-weight: 600;
+ transition: background .18s;
+ text-align: start;
+ width: 100%;
+}
+.gpi-media-item:hover { background: rgba(255,255,255,.07); }
+
+.gpi-media-ico {
+ width: 34px; height: 34px;
+ border-radius: 50%;
+ display: flex; align-items: center; justify-content: center;
+ font-size: 1.05rem;
+ flex-shrink: 0;
+}
+
+/* voice record row — slightly bigger */
+.gpi-media-item.gpi-media-voice .gpi-media-ico {
+ width: 36px; height: 36px;
+}
+
+/* ── Quick mic button (hold to record) ──────────────────────── */
+.gpi-voice-quick {
+ width: 38px; height: 38px;
+ border-radius: 50%;
+ background: rgba(231,76,60,.1);
+ border: 1.5px solid rgba(231,76,60,.25);
+ color: #e74c3c;
+ display: flex; align-items: center; justify-content: center;
+ cursor: pointer;
+ flex-shrink: 0;
+ transition: all .2s;
+ user-select: none;
+ -webkit-user-select: none;
+}
+.gpi-voice-quick:hover { background: rgba(231,76,60,.2); border-color:rgba(231,76,60,.5); }
+.gpi-voice-quick:active {
+ background: #e74c3c; color: #fff;
+ transform: scale(1.12);
+ box-shadow: 0 0 0 6px rgba(231,76,60,.2);
+ animation: recPulse .6s ease-in-out infinite;
+}
+@keyframes recPulse {
+ 0%,100% { box-shadow: 0 0 0 4px rgba(231,76,60,.2); }
+ 50% { box-shadow: 0 0 0 10px rgba(231,76,60,.05); }
+}
+
+/* ── Emoji button ────────────────────────────────────────────── */
+.gpi-emoji-btn {
+ font-size: 1.2rem;
+ background: none;
+ border: none;
+ cursor: pointer;
+ padding: .3rem .2rem;
+ opacity: .75;
+ transition: opacity .15s, transform .15s;
+ flex-shrink: 0;
+}
+.gpi-emoji-btn:hover { opacity: 1; transform: scale(1.2); }
+
+/* ── Send button ─────────────────────────────────────────────── */
+.gpi-send {
+ width: 42px; height: 42px;
+ border-radius: 50%;
+ background: linear-gradient(135deg, #00a884, #008069);
+ border: none;
+ color: #fff;
+ display: flex; align-items: center; justify-content: center;
+ cursor: pointer;
+ flex-shrink: 0;
+ transition: all .2s;
+ box-shadow: 0 3px 12px rgba(0,168,132,.3);
+}
+.gpi-send:hover { background: linear-gradient(135deg,#00c49a,#00a884); transform:scale(1.08); }
+.gpi-send:active { transform: scale(.95); }
+.gpi-send svg { color: #fff; }
+
+/* ── Input bar adjustments ───────────────────────────────────── */
+.gp-input-bar {
+ display: flex;
+ align-items: center;
+ gap: .4rem;
+ padding: .5rem .75rem .5rem .6rem;
+ background: #1f2c34;
+ border-top: 1px solid rgba(255,255,255,.06);
+ position: relative;
+}
+.gpi-text-wrap { flex: 1; position: relative; }
+.gpi-textarea {
+ width: 100%;
+ background: #2a3942;
+ border: 1px solid rgba(255,255,255,.08);
+ border-radius: 22px;
+ color: #e9edef;
+ font-family: inherit;
+ font-size: .9rem;
+ padding: .55rem .95rem;
+ resize: none;
+ outline: none;
+ max-height: 120px;
+ line-height: 1.45;
+ transition: border-color .2s;
+ display: block;
+}
+.gpi-textarea:focus { border-color: rgba(0,168,132,.4); }
+.gpi-textarea::placeholder { color: #8696a0; }
+
+
+/* ================================================================
+ 🎬 MEDIA MESSAGE RENDERING — عرض رسائل الوسائط
+================================================================ */
+.msg-media-wrap {
+ margin-top: .4rem;
+ border-radius: 10px;
+ overflow: hidden;
+ max-width: 260px;
+}
+.msg-media-img {
+ width: 100%; border-radius: 10px; display: block;
+ cursor: pointer; transition: opacity .2s;
+}
+.msg-media-img:hover { opacity: .9; }
+.msg-media-video {
+ width: 100%; border-radius: 10px; display: block;
+ max-height: 220px; background: #000;
+}
+.msg-media-audio {
+ width: 100%; margin-top: .3rem;
+ border-radius: 30px;
+ height: 36px;
+ accent-color: #00a884;
+}
+.msg-media-file {
+ display: flex; align-items: center; gap: .6rem;
+ background: rgba(255,255,255,.06);
+ border: 1px solid rgba(255,255,255,.1);
+ border-radius: 10px;
+ padding: .55rem .75rem;
+ text-decoration: none;
+ color: #e9edef;
+ font-size: .82rem;
+ transition: background .2s;
+}
+.msg-media-file:hover { background: rgba(255,255,255,.12); }
+.msg-media-file-ico { font-size: 1.4rem; }
+.msg-media-file-name {
+ flex: 1; overflow: hidden; text-overflow: ellipsis;
+ white-space: nowrap; font-weight: 600;
+}
+.msg-media-file-size { font-size: .72rem; color: #8696a0; }
+
+
+/* ================================================================
+ ✨ MISSING ANIMATIONS & UTILITIES
+================================================================ */
+
+/* bump animation for counters */
+@keyframes bump {
+ 0% { transform: scale(1); }
+ 40% { transform: scale(1.2); color: #00a884; }
+ 100% { transform: scale(1); }
+}
+.bump { animation: bump .4s ease; }
+
+/* Slide-in / fade-out for in-app notifications */
+.in-app-notif.slide-in {
+ animation: notifSlideIn .3s cubic-bezier(.4,0,.2,1) forwards;
+}
+.in-app-notif.fade-out {
+ animation: notifFadeOut .5s ease forwards;
+}
+@keyframes notifSlideIn {
+ from { opacity:0; transform:translateX(100%); }
+ to { opacity:1; transform:translateX(0); }
+}
+@keyframes notifFadeOut {
+ from { opacity:1; transform:translateX(0); }
+ to { opacity:0; transform:translateX(100%); }
+}
+
+/* pv4 row-bio override */
+.pv4-row-bio .pv4-row-body { flex-direction: column; align-items: flex-start; }
+.pv4-bio-val { font-size:.82rem; color:#e9edef; line-height:1.6; margin-top:.2rem; white-space:pre-wrap; }
+
+/* pv4 edit form photo ring */
+.pv4-ef-photo-ring {
+ width:80px; height:80px; border-radius:50%;
+ border:2.5px solid rgba(0,168,132,.4);
+ display:flex; align-items:center; justify-content:center;
+ position:relative; cursor:pointer; overflow:hidden;
+ background:#1f2c34; transition:border-color .2s;
+}
+.pv4-ef-photo-ring:hover { border-color:#00a884; }
+.pv4-ef-photo-overlay {
+ position:absolute; inset:0;
+ background:rgba(0,0,0,.4);
+ display:flex; align-items:center; justify-content:center;
+ font-size:1.3rem; opacity:0; transition:opacity .2s;
+ border-radius:50%;
+}
+.pv4-ef-photo-ring:hover .pv4-ef-photo-overlay { opacity:1; }
+.pv4-ef-photo { width:100%;height:100%;object-fit:cover;border-radius:50%; }
+.pv4-ef-avatar {
+ font-size:2rem; line-height:1;
+ display:flex; align-items:center; justify-content:center;
+}
+
+/* pv4 activity bar today highlight */
+.pv4-act-bar.today {
+ background: linear-gradient(180deg,#00a884,#008069)!important;
+ border-radius:4px 4px 0 0;
+}
+.pv4-dot.active { background:#00a884!important; }
+
+
+/* ================================================================
+ 🎯 UNIFIED MEDIA SYSTEM — All Chat Contexts
+ ================================================================ */
+
+/* ── Chat Input Row — upgrade with media wrap ── */
+.chat-input-row {
+ display: flex;
+ gap: .5rem;
+ padding: .7rem .9rem;
+ border-top: 1px solid var(--border);
+ background: var(--dark2);
+ align-items: flex-end;
+ flex-wrap: nowrap;
+}
+.chat-inp {
+ flex: 1; min-width: 0;
+ background: var(--dark3);
+ border: 1px solid var(--border);
+ color: var(--text);
+ padding: .6rem .9rem;
+ border-radius: 22px;
+ font-family: inherit;
+ font-size: .88rem;
+ outline: none;
+ transition: border-color .2s;
+}
+.chat-inp:focus { border-color: var(--teal); }
+.chat-inp::placeholder { color: var(--text2); }
+.chat-send-btn {
+ background: linear-gradient(135deg, var(--teal), var(--teal2));
+ border: none; color: #fff;
+ width: 38px; height: 38px;
+ border-radius: 50%;
+ cursor: pointer;
+ display: flex; align-items: center; justify-content: center;
+ transition: transform .15s, box-shadow .15s;
+ flex-shrink: 0;
+}
+.chat-send-btn:hover { transform: scale(1.08); box-shadow: 0 3px 12px rgba(26,188,156,.35); }
+.chat-send-btn svg { display: block; }
+
+/* ── Inline media rendering in public/DM messages ── */
+.chat-media-wrap {
+ margin: .35rem 0;
+ max-width: 240px;
+}
+.chat-media-img {
+ max-width: 100%;
+ max-height: 200px;
+ border-radius: 10px;
+ object-fit: cover;
+ cursor: zoom-in;
+ display: block;
+ border: 1px solid rgba(255,255,255,.08);
+}
+.chat-media-video {
+ max-width: 100%;
+ max-height: 180px;
+ border-radius: 10px;
+ display: block;
+ background: #000;
+}
+.chat-audio-player {
+ display: flex;
+ align-items: center;
+ gap: .5rem;
+ background: rgba(255,255,255,.05);
+ border: 1px solid rgba(255,255,255,.1);
+ border-radius: 22px;
+ padding: .4rem .8rem;
+ min-width: 180px;
+ max-width: 240px;
+}
+.chat-audio-ico { font-size: 1.1rem; flex-shrink: 0; }
+.chat-audio {
+ height: 28px;
+ flex: 1;
+ outline: none;
+ accent-color: var(--teal);
+}
+.chat-file-link {
+ display: flex;
+ align-items: center;
+ gap: .5rem;
+ background: rgba(26,188,156,.1);
+ border: 1px solid rgba(26,188,156,.25);
+ border-radius: 10px;
+ padding: .5rem .8rem;
+ color: var(--teal);
+ text-decoration: none;
+ font-size: .82rem;
+ transition: background .2s;
+}
+.chat-file-link:hover { background: rgba(26,188,156,.18); }
+.chat-msg-body { font-size: .88rem; line-height: 1.5; }
+
+/* ── Group chat — file rendering ── */
+.gpm-file-link {
+ display: flex;
+ align-items: center;
+ gap: .5rem;
+ background: rgba(26,188,156,.08);
+ border: 1px solid rgba(26,188,156,.2);
+ border-radius: 10px;
+ padding: .5rem .8rem;
+ color: var(--teal);
+ text-decoration: none;
+ font-size: .82rem;
+ transition: background .2s;
+}
+.gpm-file-link:hover { background: rgba(26,188,156,.16); }
+
+/* ── Study chat — input bar ── */
+#studyInputBar {
+ border-radius: 0 0 .7rem .7rem;
+ background: var(--card);
+ border-left: 1px solid rgba(142,68,173,.2);
+ border-right: 1px solid rgba(142,68,173,.2);
+}
+#studyInputBar .gpi-attach-btn:hover,
+#studyInputBar .gpi-attach-btn.gpi-attach-open {
+ border-color: rgba(142,68,173,.6);
+ color: #9b59b6;
+ background: rgba(142,68,173,.12);
+}
+#studyInputBar .gpi-send {
+ background: linear-gradient(135deg,#9b59b6,#8e44ad);
+}
+#studyInputBar .gpi-voice-quick:hover,
+#studyInputBar .gpi-voice-quick:active {
+ background: rgba(142,68,173,.2);
+ border-color: rgba(142,68,173,.5);
+}
+
+/* ── Study voice recording bar ── */
+#studyVoiceRecBar {
+ display: flex;
+ align-items: center;
+ gap: .6rem;
+ background: rgba(231,76,60,.08);
+ border: 1px solid rgba(231,76,60,.25);
+ border-top: none;
+ padding: .5rem .9rem;
+ border-radius: 0 0 .7rem .7rem;
+ font-size: .82rem;
+ color: var(--text);
+}
+#studyVoiceRecBar.hidden { display: none !important; }
+
+/* ── Public chat — voice recording bar ── */
+#pubVoiceRecordingBar {
+ margin: 0;
+ border-radius: 0 0 var(--r) var(--r);
+}
+
+/* ── Chat modal voice button ── */
+.chat-input-row .gpi-voice-quick {
+ width: 38px;
+ height: 38px;
+ flex-shrink: 0;
+}
+
+/* ── Chat modal enhanced layout ── */
+.chat-modal { display: flex; flex-direction: column; }
+
+/* ── Study chat messages — media rendering ── */
+.study-msg-media { margin: .3rem 0; max-width: 220px; }
+.study-msg-media img {
+ max-width: 100%;
+ max-height: 180px;
+ border-radius: 8px;
+ object-fit: cover;
+ cursor: zoom-in;
+ display: block;
+}
+.study-msg-media audio {
+ height: 28px;
+ width: 100%;
+ max-width: 200px;
+ accent-color: #9b59b6;
+}
+
+/* ── Camera capture item in media menus ── */
+.gpi-media-item[onclick*="camera"] .gpi-media-ico,
+.gpi-media-item[onclick*="'camera'"] .gpi-media-ico {
+ background: rgba(39,174,96,.15);
+ color: #27ae60;
+}
+
+/* ── DM chat — enhanced message media ── */
+.dm-msg .chat-media-wrap { max-width: 220px; }
+
+/* ── Media menu: 6 items grid adaptation ── */
+.gpi-media-menu {
+ display: grid;
+ grid-template-columns: repeat(3, 1fr);
+}
+
+/* Mobile: 2-column for small screens */
+@media (max-width: 360px) {
+ .gpi-media-menu { grid-template-columns: repeat(2, 1fr); }
+}
+
+/* ── Voice quick button tooltip ── */
+.gpi-voice-quick::after {
+ content: attr(title);
+ position: absolute;
+ bottom: calc(100% + 6px);
+ right: 50%;
+ transform: translateX(50%);
+ background: rgba(0,0,0,.8);
+ color: #fff;
+ font-size: .7rem;
+ white-space: nowrap;
+ padding: 3px 7px;
+ border-radius: 6px;
+ pointer-events: none;
+ opacity: 0;
+ transition: opacity .2s;
+}
+.gpi-voice-quick:hover::after { opacity: 1; }
+.gpi-voice-quick { position: relative; }
+
+
+/* ================================================================
+ 🎬 COMPREHENSIVE MEDIA FIXES — video, study, pub chat
+ ================================================================ */
+
+/* ── study chat video ── */
+.study-msg-video {
+ max-width: 100%;
+ max-height: 180px;
+ border-radius: 10px;
+ display: block;
+ background: #000;
+ margin: .3rem 0;
+}
+
+/* ── public chat (chat-modal) input row media wrap ── */
+.chat-input-row .gpi-media-wrap { position: relative; }
+.chat-input-row .gpi-media-menu {
+ bottom: calc(100% + 8px);
+ left: 0;
+ right: auto;
+}
+
+/* ── gpi-media-menu baseline fixes ── */
+.gpi-media-menu {
+ position: absolute;
+ bottom: calc(100% + 8px);
+ left: 0;
+ z-index: 9999;
+ background: var(--card, #1e2533);
+ border: 1px solid rgba(255,255,255,.1);
+ border-radius: 14px;
+ padding: .5rem;
+ min-width: 200px;
+ box-shadow: 0 8px 32px rgba(0,0,0,.45);
+ display: grid;
+ grid-template-columns: repeat(3, 1fr);
+ gap: .3rem;
+ animation: menuPopUp .18s ease;
+}
+@keyframes menuPopUp {
+ from { opacity:0; transform:translateY(8px) scale(.96); }
+ to { opacity:1; transform:translateY(0) scale(1); }
+}
+.gpi-media-menu.hidden { display: none !important; }
+
+/* ── gpi-media-item ── */
+.gpi-media-item {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: .3rem;
+ padding: .55rem .4rem;
+ background: none;
+ border: none;
+ border-radius: 10px;
+ cursor: pointer;
+ color: var(--text, #e8eaf0);
+ font-size: .75rem;
+ font-family: inherit;
+ transition: background .15s;
+ white-space: nowrap;
+}
+.gpi-media-item:hover { background: rgba(255,255,255,.07); }
+.gpi-media-ico {
+ width: 36px;
+ height: 36px;
+ border-radius: 10px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 1.15rem;
+ flex-shrink: 0;
+}
+
+/* ── voice recording bar pulse ── */
+@keyframes vrPulse {
+ 0%,100% { opacity:1; transform:scale(1); }
+ 50% { opacity:.4; transform:scale(1.4); }
+}
+.gvr-pulse { animation: vrPulse 1s infinite; display:inline-block; }
+
+/* ── gpi-attach-btn open state ── */
+.gpi-attach-btn.gpi-attach-open {
+ background: rgba(26,188,156,.18);
+ border-color: rgba(26,188,156,.4);
+ color: var(--teal, #1abc9c);
+}
+
+/* ── full chat media: video in gpm-media ── */
+.gpm-video {
+ max-width: 100%;
+ max-height: 200px;
+ border-radius: 10px;
+ display: block;
+ background: #000;
+}
+
+/* ── chat-media-video size clamp ── */
+.chat-media-video {
+ max-width: min(260px, 100%);
+ max-height: 200px;
+ border-radius: 10px;
+ display: block;
+ background: #000;
+}
+
+/* ── chat-media-img size clamp ── */
+.chat-media-img {
+ max-width: min(260px, 100%);
+ max-height: 200px;
+ border-radius: 10px;
+ cursor: zoom-in;
+ display: block;
+ object-fit: cover;
+}
+
+/* ── chat-media-wrap ── */
+.chat-media-wrap {
+ margin: .25rem 0;
+ max-width: 270px;
+}
+
+/* ── gpi-send button ── */
+.gpi-send {
+ background: linear-gradient(135deg, var(--teal, #1abc9c), var(--teal2, #16a085));
+ border: none;
+ color: #fff;
+ width: 38px;
+ height: 38px;
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ cursor: pointer;
+ flex-shrink: 0;
+ transition: transform .15s, opacity .15s;
+}
+.gpi-send:hover { transform: scale(1.08); }
+.gpi-send:active { opacity: .8; }
+
+/* ── voice recording bar ── */
+.gp-voice-rec-bar {
+ display: flex;
+ align-items: center;
+ gap: .6rem;
+ padding: .6rem 1rem;
+ background: rgba(231,76,60,.1);
+ border-top: 1px solid rgba(231,76,60,.25);
+ font-size: .85rem;
+ color: #e74c3c;
+}
+.gvr-cancel {
+ margin-right: auto;
+ background: rgba(231,76,60,.15);
+ border: 1px solid rgba(231,76,60,.3);
+ color: #e74c3c;
+ border-radius: 8px;
+ padding: .25rem .65rem;
+ cursor: pointer;
+ font-size: .8rem;
+ font-family: inherit;
+}
+.gvr-cancel:hover { background: rgba(231,76,60,.25); }
+
+/* ── gpi-textarea auto-resize ── */
+.gpi-textarea {
+ flex: 1;
+ background: transparent;
+ border: none;
+ color: var(--text, #e8eaf0);
+ font-family: inherit;
+ font-size: .9rem;
+ resize: none;
+ outline: none;
+ line-height: 1.45;
+ max-height: 120px;
+ overflow-y: auto;
+ padding: .35rem 0;
+}
+.gpi-textarea::placeholder { color: var(--text2, #8892a4); }
+
+/* ── gpi-text-wrap ── */
+.gpi-text-wrap {
+ flex: 1;
+ background: var(--dark3, #232b3a);
+ border: 1px solid var(--border, rgba(255,255,255,.07));
+ border-radius: 20px;
+ padding: .4rem .9rem;
+ display: flex;
+ align-items: center;
+ min-width: 0;
+ transition: border-color .2s;
+}
+.gpi-text-wrap:focus-within { border-color: var(--teal, #1abc9c); }
+
+/* ── gp-input-bar layout ── */
+.gp-input-bar {
+ display: flex;
+ align-items: flex-end;
+ gap: .45rem;
+ padding: .65rem .9rem;
+ background: var(--dark2, #1a2030);
+ border-top: 1px solid var(--border, rgba(255,255,255,.06));
+ position: relative;
+}
+
+/* ── gpi-attach-btn ── */
+.gpi-attach-btn {
+ width: 38px;
+ height: 38px;
+ border-radius: 50%;
+ background: rgba(255,255,255,.06);
+ border: 1px solid rgba(255,255,255,.1);
+ color: var(--text, #e8eaf0);
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex-shrink: 0;
+ transition: all .2s;
+}
+.gpi-attach-btn:hover {
+ background: rgba(26,188,156,.12);
+ border-color: rgba(26,188,156,.3);
+ color: var(--teal, #1abc9c);
+}
+
+/* ── gpi-voice-quick ── */
+.gpi-voice-quick {
+ width: 38px;
+ height: 38px;
+ border-radius: 50%;
+ background: rgba(255,255,255,.06);
+ border: 1px solid rgba(255,255,255,.1);
+ color: var(--text2, #8892a4);
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex-shrink: 0;
+ transition: all .2s;
+ position: relative;
+ -webkit-user-select: none;
+ user-select: none;
+}
+.gpi-voice-quick:hover, .gpi-voice-quick:active {
+ background: rgba(231,76,60,.15);
+ border-color: rgba(231,76,60,.35);
+ color: #e74c3c;
+}
+
+/* ── gpi-emoji-btn ── */
+.gpi-btn.gpi-emoji-btn {
+ background: none;
+ border: none;
+ font-size: 1.2rem;
+ cursor: pointer;
+ padding: .25rem;
+ border-radius: 8px;
+ transition: transform .15s;
+ flex-shrink: 0;
+ line-height: 1;
+}
+.gpi-btn.gpi-emoji-btn:hover { transform: scale(1.2); }
+
+/* ── gpi-media-wrap ── */
+.gpi-media-wrap { position: relative; flex-shrink: 0; }
+
+/* ── Mobile RTL adjustments ── */
+@media (max-width: 480px) {
+ .gpi-media-menu { min-width: 170px; grid-template-columns: repeat(2, 1fr); }
+ .gpi-media-item { font-size: .7rem; padding: .45rem .3rem; }
+ .gpi-media-ico { width: 30px; height: 30px; font-size: 1rem; }
+}
+
+/* ============================================================
+ 🚀 ENHANCEMENTS v7.0 - Skeleton, Animations, PWA, Performance
+============================================================ */
+
+/* ── Skeleton Loader ─────────────────────────────────────── */
+@keyframes sk-shimmer {
+ 0% { background-position: -400px 0 }
+ 100% { background-position: 400px 0 }
+}
+.sk-card {
+ display: flex; gap: .8rem; align-items: flex-start;
+ background: var(--dark3); border-radius: var(--r);
+ padding: .9rem; margin-bottom: .7rem;
+ animation: none;
+}
+.sk-card .sk-icon,
+.sk-card .sk-avatar,
+.sk-card .sk-img {
+ background: linear-gradient(90deg, var(--dark4) 25%, var(--dark5) 50%, var(--dark4) 75%);
+ background-size: 800px 100%;
+ animation: sk-shimmer 1.4s infinite linear;
+ flex-shrink: 0; border-radius: var(--rs);
+}
+.sk-card .sk-icon { width: 44px; height: 44px; border-radius: 50%; }
+.sk-card .sk-avatar { width: 48px; height: 48px; border-radius: 50%; }
+.sk-card.sk-market .sk-img { width: 80px; height: 72px; border-radius: var(--rs); }
+.sk-body { flex: 1; display: flex; flex-direction: column; gap: .5rem; }
+.sk-line {
+ height: 12px; border-radius: 6px;
+ background: linear-gradient(90deg, var(--dark4) 25%, var(--dark5) 50%, var(--dark4) 75%);
+ background-size: 800px 100%;
+ animation: sk-shimmer 1.4s infinite linear;
+}
+.sk-w90 { width: 90% } .sk-w80 { width: 80% } .sk-w70 { width: 70% }
+.sk-w60 { width: 60% } .sk-w50 { width: 50% } .sk-w40 { width: 40% } .sk-w30 { width: 30% }
+
+/* ── Touch Feedback ──────────────────────────────────────── */
+.touch-active { transform: scale(0.97) !important; opacity: .85 !important; transition: transform .1s, opacity .1s !important; }
+
+/* ── Scroll-to-top button ────────────────────────────────── */
+#scrollTopBtn {
+ position: fixed; bottom: 5.5rem; left: 1rem;
+ width: 42px; height: 42px;
+ background: var(--dark3); border: 1px solid var(--border2);
+ color: var(--text); border-radius: 50%;
+ display: flex; align-items: center; justify-content: center;
+ font-size: 1.1rem; cursor: pointer; z-index: 800;
+ box-shadow: var(--sh2); transition: all .3s ease;
+}
+#scrollTopBtn:hover { background: var(--teal); border-color: var(--teal); }
+#scrollTopBtn.hidden { display: none !important; }
+
+/* ── Trending list ───────────────────────────────────────── */
+.trend-item {
+ display: flex; align-items: center; gap: .7rem;
+ padding: .6rem .8rem; background: var(--dark3);
+ border-radius: var(--rs); margin-bottom: .4rem;
+ cursor: pointer; transition: background .2s;
+ border: 1px solid var(--border);
+}
+.trend-item:hover { background: var(--dark4); }
+.trend-num { font-size: .8rem; font-weight: 700; color: var(--teal); min-width: 18px; text-align: center; }
+.trend-body { flex: 1; display: flex; justify-content: space-between; align-items: center; gap: .5rem; overflow: hidden; }
+.trend-text { font-size: .85rem; color: var(--text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
+.trend-type { font-size: .9rem; flex-shrink: 0; }
+
+/* ── Connection quality banner ───────────────────────────── */
+/* (merged into single definition above) */
+
+/* ── PWA Install Banner (enhanced) ──────────────────────── */
+.pwa-install-banner {
+ position: fixed; bottom: 4.2rem; left: .6rem; right: .6rem;
+ background: linear-gradient(135deg, var(--dark3), var(--dark4));
+ border: 1px solid var(--teal); border-radius: var(--r);
+ padding: .8rem 1rem; z-index: 800;
+ display: flex; align-items: center; justify-content: space-between;
+ box-shadow: 0 4px 24px rgba(26,188,156,.2);
+ animation: slideUp .4s ease;
+}
+@keyframes slideUp {
+ from { transform: translateY(20px); opacity: 0 }
+ to { transform: translateY(0); opacity: 1 }
+}
+.pwa-install-banner.hidden { display: none !important; }
+.pwa-banner-content { display: flex; align-items: center; gap: .7rem; }
+.pwa-banner-icon { font-size: 1.8rem; }
+.pwa-banner-text { display: flex; flex-direction: column; }
+.pwa-banner-text strong { font-size: .9rem; color: var(--text); }
+.pwa-banner-text span { font-size: .75rem; color: var(--text2); }
+.pwa-banner-actions { display: flex; gap: .4rem; }
+.pwa-install-btn {
+ background: var(--teal); color: #fff; border: none;
+ padding: .4rem .8rem; border-radius: var(--rs); cursor: pointer;
+ font-family: inherit; font-size: .8rem; font-weight: 700;
+ transition: background .2s;
+}
+.pwa-install-btn:hover { background: var(--teal2); }
+.pwa-dismiss-btn {
+ background: transparent; color: var(--text2); border: 1px solid var(--border);
+ padding: .4rem .6rem; border-radius: var(--rs); cursor: pointer;
+ font-family: inherit; font-size: .8rem; transition: all .2s;
+}
+.pwa-dismiss-btn:hover { background: rgba(255,255,255,.05); }
+
+/* ── Section entrance animations ────────────────────────── */
+.section.active-sec .section-pad > * {
+ animation: fadeInUp .3s ease both;
+}
+@keyframes fadeInUp {
+ from { opacity: 0; transform: translateY(12px) }
+ to { opacity: 1; transform: translateY(0) }
+}
+.section.active-sec .section-pad > *:nth-child(1) { animation-delay: .03s }
+.section.active-sec .section-pad > *:nth-child(2) { animation-delay: .06s }
+.section.active-sec .section-pad > *:nth-child(3) { animation-delay: .09s }
+.section.active-sec .section-pad > *:nth-child(4) { animation-delay: .12s }
+.section.active-sec .section-pad > *:nth-child(5) { animation-delay: .15s }
+
+/* ── Alert cards hover effect (improved) ────────────────── */
+.alert-item {
+ transition: transform .2s ease, box-shadow .2s ease;
+}
+.alert-item:hover { transform: translateY(-1px); box-shadow: 0 6px 24px rgba(0,0,0,.4) !important; }
+.alert-item:active { transform: translateY(0) scale(.99); }
+
+/* ── Market card hover (improved) ───────────────────────── */
+.market-card {
+ transition: transform .2s ease, box-shadow .2s ease;
+}
+.market-card:hover { transform: translateY(-2px); }
+
+/* ── Dashboard stat cards (improved) ────────────────────── */
+.dash-stat-card {
+ transition: transform .2s, background .2s;
+}
+.dash-stat-card:hover { transform: scale(1.03); background: var(--dark4) !important; }
+
+/* ── Button press animation ──────────────────────────────── */
+button:active, .btn:active { transform: scale(.96) !important; }
+
+/* ── Topbar stats chip animation ─────────────────────────── */
+.stat-chip {
+ transition: background .2s;
+}
+.stat-chip:hover { background: rgba(255,255,255,.08); border-radius: var(--rxs); }
+
+/* ── Image full viewer ───────────────────────────────────── */
+#fullImgViewer { animation: fadeIn .2s ease; }
+@keyframes fadeIn { from { opacity: 0 } to { opacity: 1 } }
+
+/* ── Toast enhanced ─────────────────────────────────────── */
+.toast {
+ position: fixed; bottom: 5.5rem; left: 50%; transform: translateX(-50%);
+ background: var(--dark3); color: var(--text);
+ padding: .65rem 1.4rem; border-radius: 2rem;
+ font-size: .88rem; font-weight: 600; z-index: 9990;
+ box-shadow: 0 4px 20px rgba(0,0,0,.5);
+ border: 1px solid var(--border2);
+ animation: toastIn .3s ease;
+ max-width: min(90vw, 360px); text-align: center;
+}
+.toast.success { border-color: var(--green); background: rgba(46,204,113,.12); }
+.toast.error { border-color: var(--red); background: rgba(231,76,60,.12); }
+.toast.warning { border-color: var(--orange); background: rgba(230,126,34,.12); }
+.toast.info { border-color: var(--blue); background: rgba(52,152,219,.12); }
+.toast.hidden { display: none !important; }
+@keyframes toastIn {
+ from { opacity: 0; transform: translateX(-50%) translateY(10px) }
+ to { opacity: 1; transform: translateX(-50%) translateY(0) }
+}
+
+/* ── Loading spinner ─────────────────────────────────────── */
+.spinner {
+ display: inline-block; width: 20px; height: 20px;
+ border: 2px solid var(--border2); border-top-color: var(--teal);
+ border-radius: 50%; animation: spin .7s linear infinite;
+}
+@keyframes spin { to { transform: rotate(360deg) } }
+
+/* ── Empty state (improved) ──────────────────────────────── */
+.empty-state {
+ text-align: center; padding: 2.5rem 1rem;
+ display: flex; flex-direction: column; align-items: center; gap: .6rem;
+}
+.empty-icon { font-size: 2.8rem; line-height: 1; }
+.empty-title { font-size: 1rem; font-weight: 700; color: var(--text); }
+.empty-sub { font-size: .85rem; color: var(--text2); }
+.empty-btn {
+ margin-top: .6rem; background: var(--teal); color: #fff;
+ border: none; padding: .5rem 1.2rem; border-radius: 2rem;
+ cursor: pointer; font-family: inherit; font-size: .85rem; font-weight: 600;
+ transition: background .2s;
+}
+.empty-btn:hover { background: var(--teal2); }
+
+/* ── Stats chips in topbar ───────────────────────────────── */
+.topbar-stats .stat-chip {
+ padding: .25rem .5rem; border-radius: var(--rxs);
+ background: rgba(255,255,255,.04);
+ font-size: .8rem; cursor: default;
+}
+
+/* ── Section transition ──────────────────────────────────── */
+.section { display: none; }
+.section.active-sec { display: block; animation: secIn .25s ease; }
+@keyframes secIn { from { opacity:.7; transform:translateY(4px) } to { opacity:1; transform:translateY(0) } }
+
+/* ── Notif badge (improved) ──────────────────────────────── */
+.notif-badge {
+ position: fixed; top: 3.8rem; left: 50%; transform: translateX(-50%);
+ background: rgba(26,188,156,.9); color: #fff;
+ padding: .4rem 1rem; border-radius: 2rem;
+ font-size: .82rem; font-weight: 600; z-index: 9000;
+ max-width: 90vw; text-align: center;
+ box-shadow: 0 4px 16px rgba(26,188,156,.3);
+ animation: slideDown .3s ease;
+}
+.notif-badge.hidden { display: none !important; }
+@keyframes slideDown { from { opacity:0; transform:translateX(-50%) translateY(-8px) } to { opacity:1; transform:translateX(-50%) translateY(0) } }
+
+/* ── Live dot pulse ──────────────────────────────────────── */
+.live-dot {
+ display: inline-block; width: 7px; height: 7px;
+ background: var(--green); border-radius: 50%;
+ animation: pulse-live 1.5s infinite;
+ margin-left: .3rem;
+}
+@keyframes pulse-live {
+ 0%, 100% { box-shadow: 0 0 0 0 rgba(46,204,113,.6); }
+ 50% { box-shadow: 0 0 0 5px rgba(46,204,113,0); }
+}
+
+/* ── Responsive improvements ─────────────────────────────── */
+@media (max-width:480px) {
+ .topbar { padding: .35rem .7rem; }
+ .pwa-install-banner { bottom: 4.8rem; left: .4rem; right: .4rem; }
+ .toast { bottom: 5rem; font-size: .82rem; padding: .55rem 1.1rem; }
+ .sk-card .sk-img { width: 64px; height: 60px; }
+}
+
+/* ── Smooth scroll behavior ──────────────────────────────── */
+#mainContent { scroll-behavior: smooth; }
+
+/* ── Custom selection color ──────────────────────────────── */
+::selection { background: rgba(26,188,156,.3); color: var(--text); }
+
+/* ── Focus visible for accessibility ────────────────────── */
+:focus-visible { outline: 2px solid var(--teal); outline-offset: 2px; border-radius: 4px; }
+
+/* ── Card border glow on hover (subtle) ─────────────────── */
+.alert-item:hover,
+.market-card:hover,
+.hospital-card:hover,
+.news-card:hover { border-color: rgba(26,188,156,.25) !important; }
+
+
+/* ============================================================
+ 🎨 NABDH v7.1 — COMPLETE UI SYSTEM
+ تصميم شامل: animations + components + responsive
+ ============================================================ */
+
+/* ── CSS Variables Enhancement ─────────────────────────────── */
+:root {
+ --transition-fast: 0.15s ease;
+ --transition-mid: 0.3s cubic-bezier(0.4,0,0.2,1);
+ --transition-slow: 0.5s cubic-bezier(0.4,0,0.2,1);
+ --spring: 0.4s cubic-bezier(0.34,1.56,0.64,1);
+ --shadow-card: 0 2px 12px rgba(0,0,0,0.4);
+ --shadow-float: 0 8px 32px rgba(0,0,0,0.6);
+ --shadow-glow-red: 0 0 20px rgba(239,68,68,0.4);
+ --shadow-glow-green: 0 0 20px rgba(34,197,94,0.3);
+ --blur-bg: blur(12px);
+ --glass-bg: rgba(255,255,255,0.05);
+ --glass-border: rgba(255,255,255,0.1);
+ --navbar-h: 60px;
+ --topbar-h: 56px;
+}
+
+/* ── Skeleton Loaders ──────────────────────────────────────── */
+@keyframes shimmer {
+ 0% { background-position: -200% 0; }
+ 100% { background-position: 200% 0; }
+}
+.sk-card, .sk-list-item, .sk-stat {
+ background: var(--dark2);
+ border-radius: var(--r2);
+ padding: 14px;
+ margin-bottom: 10px;
+ display: flex;
+ gap: 12px;
+ align-items: flex-start;
+ overflow: hidden;
+}
+.sk-icon, .sk-avatar, .sk-img, .sk-num,
+.sk-line, .sk-label {
+ background: linear-gradient(90deg,
+ rgba(255,255,255,0.04) 25%,
+ rgba(255,255,255,0.10) 50%,
+ rgba(255,255,255,0.04) 75%);
+ background-size: 200% 100%;
+ animation: shimmer 1.6s infinite;
+ border-radius: 6px;
+ flex-shrink: 0;
+}
+.sk-icon { width:40px; height:40px; border-radius:10px; }
+.sk-avatar{ width:44px; height:44px; border-radius:50%; }
+.sk-img { width:80px; height:80px; border-radius:10px; }
+.sk-num { width:60px; height:32px; border-radius:8px; margin-bottom:6px; }
+.sk-label { width:80px; height:14px; border-radius:4px; }
+.sk-body { flex:1; display:flex; flex-direction:column; gap:8px; }
+.sk-line { height:13px; border-radius:4px; }
+.sk-w90 { width:90%; }
+.sk-w80 { width:80%; }
+.sk-w70 { width:70%; }
+.sk-w60 { width:60%; }
+.sk-w50 { width:50%; }
+.sk-w45 { width:45%; }
+.sk-w40 { width:40%; }
+.sk-w30 { width:30%; }
+.sk-stat { flex-direction:column; align-items:center; padding:16px 12px; }
+.sk-person .sk-avatar { width:48px; height:48px; }
+.sk-market { align-items:stretch; }
+.skeleton-loading { pointer-events:none; }
+
+/* ── Pull to Refresh ───────────────────────────────────────── */
+.ptr-indicator {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 8px;
+ height: 0;
+ overflow: hidden;
+ transition: height 0.2s, opacity 0.2s;
+ background: var(--dark2);
+ color: var(--text2);
+ font-size: 0.85rem;
+ border-bottom: 1px solid var(--border1);
+}
+.ptr-icon { font-size: 1.1rem; transition: transform 0.2s; }
+.ptr-spinner {
+ width: 18px; height: 18px;
+ border: 2px solid var(--border1);
+ border-top-color: var(--red1);
+ border-radius: 50%;
+ animation: spin 0.7s linear infinite;
+}
+
+/* ── Enhanced Toast ────────────────────────────────────────── */
+#toastContainer {
+ position: fixed;
+ bottom: calc(var(--navbar-h) + 16px);
+ left: 50%;
+ transform: translateX(-50%);
+ z-index: 9999;
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ align-items: center;
+ pointer-events: none;
+ width: min(90vw, 360px);
+}
+.enhanced-toast {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ padding: 12px 18px;
+ border-radius: 14px;
+ font-size: 0.88rem;
+ font-weight: 500;
+ backdrop-filter: var(--blur-bg);
+ border: 1px solid var(--glass-border);
+ box-shadow: var(--shadow-float);
+ pointer-events: all;
+ cursor: pointer;
+ opacity: 0;
+ transform: translateY(20px) scale(0.95);
+ transition: opacity 0.25s, transform 0.25s var(--spring);
+ max-width: 100%;
+ word-break: break-word;
+}
+.enhanced-toast.show { opacity:1; transform:translateY(0) scale(1); }
+.toast-success { background:rgba(34,197,94,0.18); border-color:rgba(34,197,94,0.35); color:#86efac; }
+.toast-error { background:rgba(239,68,68,0.18); border-color:rgba(239,68,68,0.35); color:#fca5a5; }
+.toast-warning { background:rgba(234,179,8,0.18); border-color:rgba(234,179,8,0.35); color:#fde047; }
+.toast-info { background:rgba(59,130,246,0.18); border-color:rgba(59,130,246,0.35); color:#93c5fd; }
+.toast-icon { font-size:1.1rem; flex-shrink:0; }
+
+/* ── Scroll to Top Button ──────────────────────────────────── */
+#scrollTopBtn {
+ position: fixed;
+ bottom: calc(var(--navbar-h) + 70px);
+ left: 16px;
+ width: 44px; height: 44px;
+ border-radius: 50%;
+ background: var(--red1);
+ color: #fff;
+ border: none;
+ font-size: 1.2rem;
+ cursor: pointer;
+ display: flex; align-items: center; justify-content: center;
+ box-shadow: var(--shadow-glow-red);
+ opacity: 0;
+ transform: scale(0.7) translateY(20px);
+ transition: opacity 0.3s, transform 0.3s var(--spring);
+ pointer-events: none;
+ z-index: 100;
+}
+#scrollTopBtn.show {
+ opacity: 1;
+ transform: scale(1) translateY(0);
+ pointer-events: all;
+}
+#scrollTopBtn:hover { background: var(--red2); transform: scale(1.1); }
+#scrollTopBtn:active { transform: scale(0.95); }
+
+/* ── Connection Badge ──────────────────────────────────────── */
+.conn-badge {
+ width: 8px; height: 8px;
+ border-radius: 50%;
+ display: inline-block;
+ flex-shrink: 0;
+}
+.conn-excellent { background: #22c55e; box-shadow: 0 0 6px #22c55e; }
+.conn-good { background: #22c55e; }
+.conn-fair { background: #eab308; }
+.conn-poor { background: #ef4444; }
+.conn-offline { background: #6b7280; }
+
+/* ── Weather Widget ────────────────────────────────────────── */
+#weatherWidget { display:none; }
+.weather-mini {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ font-size: 0.82rem;
+ color: var(--text2);
+ background: var(--glass-bg);
+ border: 1px solid var(--glass-border);
+ border-radius: 20px;
+ padding: 4px 10px;
+ backdrop-filter: var(--blur-bg);
+}
+.weather-icon { font-size: 1.1rem; }
+.weather-temp { font-weight: 700; color: var(--text1); }
+.weather-cond { color: var(--text3); }
+.weather-city { color: var(--text3); font-size: 0.75rem; }
+
+/* ── Prayer Times Widget ───────────────────────────────────── */
+.prayer-mini {
+ font-size: 0.8rem;
+ color: var(--text2);
+ padding: 4px 10px;
+ background: var(--glass-bg);
+ border: 1px solid var(--glass-border);
+ border-radius: 16px;
+ display: inline-block;
+}
+
+/* ── Trending Items ────────────────────────────────────────── */
+.trending-item {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ padding: 12px 14px;
+ background: var(--dark2);
+ border-radius: var(--r2);
+ margin-bottom: 8px;
+ cursor: pointer;
+ border: 1px solid transparent;
+ transition: all var(--transition-mid);
+}
+.trending-item:hover { border-color: var(--red1); background: var(--dark3); transform: translateX(-3px); }
+.tr-rank { font-size: 0.78rem; color: var(--text3); min-width: 24px; font-weight: 700; }
+.tr-icon { font-size: 1.2rem; flex-shrink: 0; }
+.tr-info { flex: 1; min-width: 0; }
+.tr-title { font-size: 0.88rem; font-weight: 600; color: var(--text1); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
+.tr-meta { font-size: 0.75rem; color: var(--text3); margin-top: 2px; }
+.tr-score { font-size: 0.75rem; color: var(--red1); font-weight: 700; flex-shrink: 0; }
+
+/* ── Leader Board ──────────────────────────────────────────── */
+.leader-item {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ padding: 12px 14px;
+ background: var(--dark2);
+ border-radius: var(--r2);
+ margin-bottom: 8px;
+ border: 1px solid var(--border1);
+ transition: all var(--transition-mid);
+}
+.leader-item:hover { border-color: var(--red1); transform: translateX(-3px); }
+.leader-top { border-color: rgba(234,179,8,0.3); background: rgba(234,179,8,0.05); }
+.leader-rank { font-size: 1.1rem; min-width: 28px; text-align: center; flex-shrink: 0; }
+.leader-avatar { font-size: 1.4rem; flex-shrink: 0; }
+.leader-info { flex: 1; min-width: 0; }
+.leader-name { font-size: 0.9rem; font-weight: 600; color: var(--text1); }
+.leader-area { font-size: 0.75rem; color: var(--text3); margin-top: 2px; }
+.leader-stats { display: flex; flex-direction: column; align-items: flex-end; gap: 3px; flex-shrink: 0; }
+.leader-pts { font-size: 0.8rem; color: var(--red1); font-weight: 700; }
+.leader-badge { font-size: 0.85rem; }
+
+/* ── Blood Bank ────────────────────────────────────────────── */
+.blood-stats-grid {
+ display: grid;
+ grid-template-columns: repeat(4, 1fr);
+ gap: 8px;
+ margin-bottom: 12px;
+}
+.blood-type-card {
+ padding: 10px 6px;
+ border-radius: var(--r2);
+ text-align: center;
+ border: 1px solid var(--border1);
+ transition: all var(--transition-mid);
+}
+.blood-available { border-color: rgba(239,68,68,0.4); background: rgba(239,68,68,0.08); }
+.blood-empty { opacity: 0.4; }
+.bt-type { display: block; font-size: 0.85rem; font-weight: 700; color: var(--red1); }
+.bt-count { display: block; font-size: 1rem; font-weight: 800; color: var(--text1); margin-top: 4px; }
+.blood-total { font-size: 0.82rem; color: var(--text3); text-align: center; }
+
+/* ── Top Areas List ────────────────────────────────────────── */
+#topAreasList { display: flex; flex-direction: column; gap: 6px; }
+.top-area-item {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ padding: 9px 12px;
+ background: var(--dark2);
+ border-radius: 10px;
+ cursor: pointer;
+ border: 1px solid transparent;
+ transition: all var(--transition-fast);
+}
+.top-area-item:hover { border-color: var(--red1); background: var(--dark3); }
+.ta-rank { font-size: 0.75rem; color: var(--text3); min-width: 24px; font-weight: 700; }
+.ta-name { flex: 1; font-size: 0.88rem; color: var(--text1); font-weight: 500; }
+.ta-count { font-size: 0.8rem; color: var(--red1); font-weight: 700; background: rgba(239,68,68,0.1); padding: 2px 8px; border-radius: 10px; }
+
+/* ── Image Viewer ──────────────────────────────────────────── */
+.img-viewer-overlay {
+ position: fixed; inset: 0;
+ background: rgba(0,0,0,0.92);
+ z-index: 10000;
+ display: flex; flex-direction: column;
+ align-items: center; justify-content: center;
+ opacity: 0;
+ transition: opacity 0.3s;
+ padding: 20px;
+}
+.img-viewer-overlay.show { opacity: 1; }
+.img-viewer-close {
+ position: absolute;
+ top: 16px; left: 16px;
+ background: rgba(255,255,255,0.15);
+ border: none; border-radius: 50%;
+ width: 40px; height: 40px;
+ color: #fff; font-size: 1.1rem;
+ cursor: pointer;
+ transition: background 0.2s;
+ display: flex; align-items: center; justify-content: center;
+}
+.img-viewer-close:hover { background: rgba(255,255,255,0.25); }
+.img-viewer-img {
+ max-width: 100%; max-height: 80vh;
+ border-radius: 12px;
+ object-fit: contain;
+ box-shadow: 0 0 40px rgba(0,0,0,0.8);
+}
+.img-viewer-caption {
+ margin-top: 12px;
+ color: rgba(255,255,255,0.7);
+ font-size: 0.85rem;
+ text-align: center;
+ max-width: 90%;
+}
+
+/* ── Poll Mini Card ────────────────────────────────────────── */
+.poll-mini-card {
+ padding: 12px 14px;
+ background: var(--dark2);
+ border-radius: var(--r2);
+ border: 1px solid var(--border1);
+ cursor: pointer;
+ margin-bottom: 8px;
+ transition: all var(--transition-fast);
+}
+.poll-mini-card:hover { border-color: var(--red1); background: var(--dark3); }
+.poll-q { font-size: 0.88rem; font-weight: 600; color: var(--text1); margin-bottom: 6px; }
+.poll-meta { font-size: 0.75rem; color: var(--text3); }
+
+/* ── Urgent Help Banner ────────────────────────────────────── */
+#urgentHelpBanner {
+ display: none;
+ background: linear-gradient(135deg, rgba(239,68,68,0.2), rgba(239,68,68,0.1));
+ border: 1px solid rgba(239,68,68,0.4);
+ border-radius: var(--r2);
+ padding: 10px 14px;
+ font-size: 0.85rem;
+ color: #fca5a5;
+ margin-bottom: 12px;
+ text-align: center;
+ animation: pulse-border 2s infinite;
+}
+@keyframes pulse-border {
+ 0%, 100% { border-color: rgba(239,68,68,0.4); }
+ 50% { border-color: rgba(239,68,68,0.8); }
+}
+
+/* ── VS Viewport ───────────────────────────────────────────── */
+.vs-viewport { position: absolute; top: 0; right: 0; left: 0; }
+
+/* ── Card Animations ───────────────────────────────────────── */
+@keyframes fadeInUp {
+ from { opacity:0; transform:translateY(20px); }
+ to { opacity:1; transform:translateY(0); }
+}
+@keyframes fadeInScale {
+ from { opacity:0; transform:scale(0.94); }
+ to { opacity:1; transform:scale(1); }
+}
+@keyframes slideInRight {
+ from { opacity:0; transform:translateX(30px); }
+ to { opacity:1; transform:translateX(0); }
+}
+@keyframes bounceIn {
+ 0% { opacity:0; transform:scale(0.6); }
+ 70% { transform:scale(1.05); }
+ 100% { opacity:1; transform:scale(1); }
+}
+.anim-fade-up { animation: fadeInUp 0.4s var(--spring) both; }
+.anim-scale { animation: fadeInScale 0.3s ease both; }
+.anim-slide { animation: slideInRight 0.35s ease both; }
+.anim-bounce { animation: bounceIn 0.5s var(--spring) both; }
+.anim-delay-1 { animation-delay: 0.05s; }
+.anim-delay-2 { animation-delay: 0.10s; }
+.anim-delay-3 { animation-delay: 0.15s; }
+.anim-delay-4 { animation-delay: 0.20s; }
+
+/* ── Section Enter Animation ───────────────────────────────── */
+.section.active { animation: fadeInScale 0.3s ease both; }
+
+/* ── Card Hover Effects ────────────────────────────────────── */
+.alert-card, .market-card, .voice-card, .skill-card,
+.news-card, .blood-card, .help-card, .ride-card {
+ transition: transform var(--transition-mid), box-shadow var(--transition-mid), border-color var(--transition-mid);
+ will-change: transform;
+}
+.alert-card:hover, .market-card:hover, .voice-card:hover,
+.skill-card:hover, .news-card:hover {
+ transform: translateY(-2px) translateX(-2px);
+ box-shadow: var(--shadow-card);
+}
+.alert-card:active, .market-card:active { transform: scale(0.98); }
+
+/* ── Glass Cards ───────────────────────────────────────────── */
+.glass-card {
+ background: var(--glass-bg);
+ backdrop-filter: var(--blur-bg);
+ border: 1px solid var(--glass-border);
+ border-radius: var(--r3);
+ padding: 16px;
+}
+
+/* ── Button Improvements ───────────────────────────────────── */
+.btn-primary {
+ background: linear-gradient(135deg, var(--red1), var(--red2));
+ color: #fff;
+ border: none;
+ border-radius: var(--r2);
+ padding: 12px 20px;
+ font-size: 0.9rem;
+ font-weight: 600;
+ cursor: pointer;
+ transition: all var(--transition-fast);
+ font-family: inherit;
+ touch-action: manipulation;
+ -webkit-tap-highlight-color: transparent;
+}
+.btn-primary:hover { filter: brightness(1.1); transform: translateY(-1px); box-shadow: var(--shadow-glow-red); }
+.btn-primary:active { transform: scale(0.97); filter: brightness(0.95); }
+
+.btn-secondary {
+ background: var(--dark2);
+ color: var(--text2);
+ border: 1px solid var(--border1);
+ border-radius: var(--r2);
+ padding: 10px 18px;
+ font-size: 0.88rem;
+ cursor: pointer;
+ transition: all var(--transition-fast);
+ font-family: inherit;
+ touch-action: manipulation;
+}
+.btn-secondary:hover { border-color: var(--red1); color: var(--text1); background: var(--dark3); }
+
+.btn-icon {
+ width: 38px; height: 38px;
+ border-radius: 50%;
+ background: var(--dark2);
+ border: 1px solid var(--border1);
+ display: flex; align-items: center; justify-content: center;
+ cursor: pointer;
+ font-size: 1rem;
+ transition: all var(--transition-fast);
+ touch-action: manipulation;
+}
+.btn-icon:hover { background: var(--dark3); border-color: var(--red1); transform: scale(1.08); }
+.btn-icon:active { transform: scale(0.92); }
+
+/* ── Input Improvements ────────────────────────────────────── */
+.input-field {
+ background: var(--dark2);
+ border: 1.5px solid var(--border1);
+ border-radius: var(--r2);
+ padding: 11px 14px;
+ font-size: 0.9rem;
+ color: var(--text1);
+ font-family: inherit;
+ width: 100%;
+ transition: border-color var(--transition-fast), box-shadow var(--transition-fast);
+ outline: none;
+}
+.input-field:focus {
+ border-color: var(--red1);
+ box-shadow: 0 0 0 3px rgba(239,68,68,0.15);
+}
+.input-field::placeholder { color: var(--text4); }
+
+/* ── Badge / Chip ──────────────────────────────────────────── */
+.badge {
+ display: inline-flex;
+ align-items: center;
+ gap: 4px;
+ padding: 3px 10px;
+ border-radius: 20px;
+ font-size: 0.75rem;
+ font-weight: 600;
+ white-space: nowrap;
+}
+.badge-red { background: rgba(239,68,68,0.15); color: #fca5a5; border: 1px solid rgba(239,68,68,0.3); }
+.badge-green { background: rgba(34,197,94,0.12); color: #86efac; border: 1px solid rgba(34,197,94,0.3); }
+.badge-blue { background: rgba(59,130,246,0.12); color: #93c5fd; border: 1px solid rgba(59,130,246,0.3); }
+.badge-yellow { background: rgba(234,179,8,0.12); color: #fde047; border: 1px solid rgba(234,179,8,0.3); }
+.badge-purple { background: rgba(168,85,247,0.12); color: #d8b4fe; border: 1px solid rgba(168,85,247,0.3); }
+
+/* ── Loading Spinner ───────────────────────────────────────── */
+@keyframes spin { to { transform: rotate(360deg); } }
+.spinner {
+ width: 28px; height: 28px;
+ border: 3px solid var(--border1);
+ border-top-color: var(--red1);
+ border-radius: 50%;
+ animation: spin 0.7s linear infinite;
+ margin: 20px auto;
+}
+.spinner-sm { width: 18px; height: 18px; border-width: 2px; margin: 0; }
+
+/* ── Empty State ───────────────────────────────────────────── */
+.empty-state {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ gap: 12px;
+ padding: 48px 24px;
+ color: var(--text3);
+ text-align: center;
+}
+.empty-state-icon { font-size: 2.8rem; opacity: 0.5; }
+.empty-state-text { font-size: 0.9rem; max-width: 240px; line-height: 1.5; }
+.empty-state-action {
+ background: var(--red1);
+ color: #fff;
+ border: none;
+ border-radius: var(--r2);
+ padding: 10px 20px;
+ font-size: 0.88rem;
+ cursor: pointer;
+ font-family: inherit;
+ margin-top: 8px;
+ transition: filter 0.2s;
+}
+.empty-state-action:hover { filter: brightness(1.1); }
+
+/* ── Section Header ────────────────────────────────────────── */
+.section-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 0 0 12px;
+ border-bottom: 1px solid var(--border1);
+ margin-bottom: 16px;
+}
+.section-title {
+ font-size: 1rem;
+ font-weight: 700;
+ color: var(--text1);
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+.section-action {
+ font-size: 0.8rem;
+ color: var(--red1);
+ cursor: pointer;
+ font-weight: 500;
+ padding: 4px 10px;
+ border-radius: 20px;
+ transition: background 0.2s;
+}
+.section-action:hover { background: rgba(239,68,68,0.1); }
+
+/* ── Live Dot Pulse ────────────────────────────────────────── */
+.live-dot {
+ width: 8px; height: 8px;
+ background: #22c55e;
+ border-radius: 50%;
+ display: inline-block;
+ position: relative;
+ flex-shrink: 0;
+}
+.live-dot::before {
+ content: '';
+ position: absolute;
+ inset: -3px;
+ background: rgba(34,197,94,0.3);
+ border-radius: 50%;
+ animation: ping 1.5s cubic-bezier(0,0,0.2,1) infinite;
+}
+@keyframes ping {
+ 75%, 100% { transform: scale(2); opacity: 0; }
+}
+
+/* ── Map Enhancements ──────────────────────────────────────── */
+.user-map-dot {
+ background: none !important;
+ border: none !important;
+}
+.map-controls-panel {
+ position: absolute;
+ top: 12px;
+ right: 12px;
+ z-index: 1000;
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+}
+.map-ctrl-btn {
+ width: 40px; height: 40px;
+ border-radius: 10px;
+ background: var(--dark1);
+ border: 1px solid var(--border1);
+ color: var(--text1);
+ font-size: 1rem;
+ cursor: pointer;
+ display: flex; align-items: center; justify-content: center;
+ box-shadow: 0 2px 8px rgba(0,0,0,0.4);
+ transition: all var(--transition-fast);
+}
+.map-ctrl-btn:hover { background: var(--dark2); border-color: var(--red1); }
+
+/* ── Online Indicator ──────────────────────────────────────── */
+.online-indicator {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ font-size: 0.78rem;
+ color: #22c55e;
+ font-weight: 600;
+}
+
+/* ── Progress Bar ──────────────────────────────────────────── */
+.progress-bar {
+ height: 6px;
+ background: var(--dark2);
+ border-radius: 3px;
+ overflow: hidden;
+}
+.progress-fill {
+ height: 100%;
+ background: linear-gradient(90deg, var(--red1), var(--red2));
+ border-radius: 3px;
+ transition: width 0.8s var(--spring);
+}
+
+/* ── Swipe Hint ────────────────────────────────────────────── */
+.swipe-hint {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 6px;
+ font-size: 0.75rem;
+ color: var(--text4);
+ padding: 8px;
+ animation: fadeInUp 0.5s ease 2s both;
+}
+
+/* ── Smooth Scrolling ──────────────────────────────────────── */
+#content { scroll-behavior: smooth; }
+
+/* ── Focus Visible Accessibility ──────────────────────────── */
+:focus-visible {
+ outline: 2px solid var(--red1);
+ outline-offset: 3px;
+ border-radius: 4px;
+}
+
+/* ── Responsive Adjustments ────────────────────────────────── */
+@media (max-width: 380px) {
+ .blood-stats-grid { grid-template-columns: repeat(4, 1fr); gap: 5px; }
+ .bt-type { font-size: 0.75rem; }
+ .leader-avatar { display: none; }
+ .tr-score { display: none; }
+}
+@media (min-width: 768px) {
+ :root { --navbar-h: 0px; }
+ #bottomNav { display: none; }
+ .section-pad { padding: 24px; }
+ .blood-stats-grid { grid-template-columns: repeat(8, 1fr); }
+}
+@media (prefers-reduced-motion: reduce) {
+ *, *::before, *::after {
+ animation-duration: 0.01ms !important;
+ animation-iteration-count: 1 !important;
+ transition-duration: 0.01ms !important;
+ }
+}
+
+/* ── Dark Mode Polish ──────────────────────────────────────── */
+::selection { background: rgba(239,68,68,0.3); color: #fff; }
+::-webkit-scrollbar { width: 4px; height: 4px; }
+::-webkit-scrollbar-track { background: var(--dark1); }
+::-webkit-scrollbar-thumb { background: var(--border2); border-radius: 2px; }
+::-webkit-scrollbar-thumb:hover { background: var(--red1); }
+
+/* ── PWA Install Banner ────────────────────────────────────── */
+#pwaInstallBanner {
+ position: fixed;
+ bottom: calc(var(--navbar-h) + 8px);
+ left: 8px; right: 8px;
+ background: linear-gradient(135deg, rgba(15,23,42,0.97), rgba(30,41,59,0.97));
+ border: 1px solid rgba(239,68,68,0.3);
+ border-radius: var(--r3);
+ padding: 14px 16px;
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ box-shadow: var(--shadow-float);
+ backdrop-filter: var(--blur-bg);
+ z-index: 200;
+ animation: slideInUp 0.4s var(--spring) both;
+}
+@keyframes slideInUp {
+ from { transform: translateY(100px); opacity: 0; }
+ to { transform: translateY(0); opacity: 1; }
+}
+.pwa-icon { font-size: 1.8rem; flex-shrink: 0; }
+.pwa-text { flex: 1; }
+.pwa-title { font-size: 0.88rem; font-weight: 700; color: var(--text1); }
+.pwa-sub { font-size: 0.75rem; color: var(--text3); margin-top: 2px; }
+.pwa-close {
+ background: none; border: none;
+ color: var(--text3); font-size: 1.2rem;
+ cursor: pointer; padding: 4px;
+ flex-shrink: 0;
+ transition: color 0.2s;
+}
+.pwa-close:hover { color: var(--text1); }
+
+/* ── Offline Banner ────────────────────────────────────────── */
+#offlineIndicator {
+ position: fixed;
+ top: var(--topbar-h, 56px);
+ left: 0; right: 0;
+ background: rgba(230,126,34,0.95);
+ color: #fff;
+ text-align: center;
+ padding: 6px;
+ font-size: 0.8rem;
+ font-weight: 600;
+ z-index: 500;
+ pointer-events: none;
+ backdrop-filter: blur(4px);
+ transform: translateY(-100%);
+ transition: transform 0.3s;
+}
+#offlineIndicator.show { transform: translateY(0); }
+#offlineIndicator.hidden { display: none !important; }
+
+/* ── Notification Dropdown ─────────────────────────────────── */
+.notif-panel {
+ position: absolute;
+ top: calc(var(--topbar-h) + 4px);
+ left: 8px;
+ right: 8px;
+ max-height: 360px;
+ overflow-y: auto;
+ background: var(--dark1);
+ border: 1px solid var(--border1);
+ border-radius: var(--r3);
+ box-shadow: var(--shadow-float);
+ z-index: 500;
+ animation: fadeInScale 0.2s ease both;
+}
+.notif-item {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ padding: 12px 14px;
+ border-bottom: 1px solid var(--border1);
+ cursor: pointer;
+ transition: background 0.15s;
+}
+.notif-item:hover { background: var(--dark2); }
+.notif-item:last-child { border-bottom: none; }
+.notif-icon { font-size: 1.2rem; flex-shrink: 0; }
+.notif-text { flex: 1; font-size: 0.85rem; color: var(--text1); }
+.notif-time { font-size: 0.72rem; color: var(--text4); flex-shrink: 0; }
+.notif-unread { background: rgba(239,68,68,0.06); }
+.notif-dot {
+ width: 7px; height: 7px;
+ background: var(--red1);
+ border-radius: 50%;
+ flex-shrink: 0;
+}
+
+/* ── Floating Action Button ────────────────────────────────── */
+.fab {
+ position: fixed;
+ bottom: calc(var(--navbar-h) + 80px);
+ left: 16px;
+ width: 52px; height: 52px;
+ border-radius: 50%;
+ background: linear-gradient(135deg, var(--red1), var(--red2));
+ color: #fff;
+ border: none;
+ font-size: 1.4rem;
+ cursor: pointer;
+ box-shadow: var(--shadow-glow-red);
+ display: flex; align-items: center; justify-content: center;
+ transition: all var(--transition-mid);
+ z-index: 99;
+}
+.fab:hover { transform: scale(1.1) rotate(10deg); filter: brightness(1.1); }
+.fab:active { transform: scale(0.92); }
+
+/* ── Category Tabs ─────────────────────────────────────────── */
+.cat-tabs {
+ display: flex;
+ gap: 8px;
+ overflow-x: auto;
+ padding-bottom: 8px;
+ scrollbar-width: none;
+ -webkit-overflow-scrolling: touch;
+}
+.cat-tabs::-webkit-scrollbar { display: none; }
+.cat-tab {
+ flex-shrink: 0;
+ padding: 7px 16px;
+ border-radius: 20px;
+ background: var(--dark2);
+ border: 1px solid var(--border1);
+ color: var(--text2);
+ font-size: 0.82rem;
+ cursor: pointer;
+ transition: all var(--transition-fast);
+ white-space: nowrap;
+ touch-action: manipulation;
+}
+.cat-tab.active, .cat-tab:hover { background: var(--red1); color: #fff; border-color: var(--red1); }
+
+/* ── Lazy Loaded Images ────────────────────────────────────── */
+img[data-src] { opacity: 0; transition: opacity 0.4s ease; }
+img.lazy-loaded { opacity: 1; }
+img { max-width: 100%; height: auto; }
+
+/* ── Page Transition ───────────────────────────────────────── */
+.page-enter { animation: fadeInScale 0.25s ease both; }
+.page-exit { animation: fadeOut 0.2s ease both; pointer-events: none; }
+@keyframes fadeOut { to { opacity: 0; transform: scale(0.97); } }
+
+
+/* ===== v7.6 FIXES — إصلاح شامل ===== */
+
+/* ── الأقسام: إظهار النشط وإخفاء الباقي بشكل صحيح ── */
+.section { display: none; }
+.section.active-sec {
+ display: block !important;
+ opacity: 1 !important;
+ visibility: visible !important;
+ pointer-events: auto !important;
+}
+
+/* ── القائمة الجانبية: إصلاح الظهور الكامل ── */
+#sideMenu.side-menu:not(.hidden) {
+ display: flex !important;
+ flex-direction: column !important;
+ transform: translateX(0) !important;
+ visibility: visible !important;
+ opacity: 1 !important;
+}
+.menu-item {
+ display: flex !important;
+ opacity: 1 !important;
+ visibility: visible !important;
+ pointer-events: auto !important;
+ cursor: pointer !important;
+}
+.menu-item-ico { display: flex !important; }
+.menu-item-body { display: block !important; }
+.menu-item-title { display: block !important; color: #e9edef !important; }
+.menu-item-sub { display: block !important; }
+
+/* ── عنصر الملف الشخصي في القائمة ── */
+.menu-item-profile {
+ border-right: 3px solid rgba(0,168,132,.5) !important;
+ background: rgba(0,168,132,.06) !important;
+}
+.menu-item-profile .menu-item-ico {
+ background: rgba(0,168,132,.2) !important;
+}
+.menu-item-profile .menu-item-title {
+ color: #00a884 !important;
+ font-weight: 600 !important;
+}
+
+/* ── بطاقات سريعة: إصلاح الظهور والتفاعل ── */
+.quick-grid { display: grid !important; }
+.quick-card {
+ display: flex !important;
+ opacity: 1 !important;
+ visibility: visible !important;
+ pointer-events: auto !important;
+ cursor: pointer !important;
+}
+.q-icon, .q-title, .q-desc { display: block !important; }
+
+/* ── الملف الشخصي: إصلاح الظهور ── */
+#sec-profile.section.active-sec {
+ display: block !important;
+ overflow-y: auto !important;
+}
+.pv4-cover {
+ display: block !important;
+ position: relative !important;
+}
+.pv4-id-card { display: block !important; }
+.pv4-actions { display: flex !important; }
+.pv4-act { display: flex !important; flex-direction: column !important; align-items: center !important; }
+.pv4-edit-form { display: block !important; }
+
+/* ── الأزرار: تفعيل كامل ── */
+button:not(:disabled) { pointer-events: auto !important; cursor: pointer !important; }
+.bnav { pointer-events: auto !important; cursor: pointer !important; }
+.menu-btn { pointer-events: auto !important; cursor: pointer !important; }
+
+/* ═══════════════════════════════════════════════
+ ONBOARDING OVERLAY — أول زيارة فقط
+ ═══════════════════════════════════════════════ */
+
+/* الخلفية الكاملة */
+.onb-overlay {
+ position: fixed;
+ inset: 0;
+ z-index: 99999;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 1rem;
+ animation: onbFadeIn .35s ease;
+}
+.onb-overlay.hidden {
+ display: none !important;
+}
+@keyframes onbFadeIn {
+ from { opacity: 0; }
+ to { opacity: 1; }
+}
+
+/* الطبقة الخلفية شبه شفافة */
+.onb-backdrop {
+ position: absolute;
+ inset: 0;
+ background: rgba(5, 8, 18, 0.88);
+ backdrop-filter: blur(6px);
+}
+
+/* الكرت الرئيسي */
+.onb-modal {
+ position: relative;
+ z-index: 1;
+ background: #0f1524;
+ border: 1px solid rgba(26,188,156,.25);
+ border-radius: 22px;
+ width: 100%;
+ max-width: 440px;
+ max-height: 90vh;
+ overflow: hidden;
+ box-shadow: 0 24px 80px rgba(0,0,0,.7), 0 0 0 1px rgba(26,188,156,.1);
+ animation: onbSlideUp .4s cubic-bezier(.22,.68,0,1.2);
+ display: flex;
+ flex-direction: column;
+}
+@keyframes onbSlideUp {
+ from { transform: translateY(40px) scale(.97); opacity: 0; }
+ to { transform: translateY(0) scale(1); opacity: 1; }
+}
+
+/* شريط التقدم العلوي */
+.onb-progress {
+ height: 3px;
+ background: rgba(255,255,255,.07);
+ border-radius: 3px 3px 0 0;
+ overflow: hidden;
+ flex-shrink: 0;
+}
+.onb-progress-fill {
+ height: 100%;
+ background: linear-gradient(90deg, #1abc9c, #16a085);
+ border-radius: 3px;
+ transition: width .4s ease;
+ width: 0%;
+}
+
+/* زر التخطي */
+.onb-skip-btn {
+ position: absolute;
+ top: .9rem;
+ left: 1rem;
+ background: rgba(255,255,255,.06);
+ border: 1px solid rgba(255,255,255,.1);
+ color: #6b7280;
+ font-size: .78rem;
+ padding: .3rem .75rem;
+ border-radius: 20px;
+ cursor: pointer;
+ transition: .2s;
+ z-index: 2;
+ font-family: inherit;
+}
+.onb-skip-btn:hover {
+ color: #e74c3c;
+ border-color: rgba(231,76,60,.4);
+ background: rgba(231,76,60,.08);
+}
+
+/* ── الشرائح ── */
+.onb-slide {
+ display: none;
+ padding: 2.5rem 1.75rem 1.25rem;
+ flex-direction: column;
+ align-items: center;
+ text-align: center;
+ animation: onbSlideFade .3s ease;
+ flex: 1;
+ overflow-y: auto;
+}
+.onb-slide.active {
+ display: flex;
+}
+@keyframes onbSlideFade {
+ from { opacity: 0; transform: translateX(-12px); }
+ to { opacity: 1; transform: translateX(0); }
+}
+
+/* الجزء المرئي (أيقونة كبيرة / صف أيقونات) */
+.onb-slide-visual {
+ position: relative;
+ margin-bottom: 1.25rem;
+}
+.onb-big-icon {
+ font-size: 3.8rem;
+ line-height: 1;
+ filter: drop-shadow(0 0 18px rgba(26,188,156,.35));
+}
+.onb-icons-row {
+ display: flex;
+ gap: .75rem;
+ font-size: 2.2rem;
+ justify-content: center;
+}
+
+/* حلقات نبض حول الأيقونة الترحيبية */
+.onb-pulse-rings {
+ position: absolute;
+ inset: -20px;
+ pointer-events: none;
+}
+.onb-ring {
+ position: absolute;
+ inset: 0;
+ border: 2px solid rgba(26,188,156,.35);
+ border-radius: 50%;
+ animation: onbPulseRing 2.4s ease-out infinite;
+}
+.onb-ring.r2 { animation-delay: .8s; inset: -10px; }
+.onb-ring.r3 { animation-delay: 1.6s; inset: -20px; }
+@keyframes onbPulseRing {
+ 0% { transform: scale(.6); opacity: .8; }
+ 100% { transform: scale(1.4); opacity: 0; }
+}
+
+/* العنوان والوصف */
+.onb-title {
+ font-size: 1.35rem;
+ font-weight: 800;
+ color: #e8eaf0;
+ margin-bottom: .6rem;
+ line-height: 1.4;
+}
+.onb-green { color: #1abc9c; }
+.onb-desc {
+ font-size: .92rem;
+ color: #a0a8bb;
+ line-height: 1.75;
+ margin-bottom: .75rem;
+}
+
+/* وسوم الخصائص (شريحة 0) */
+.onb-tags {
+ display: flex;
+ gap: .5rem;
+ flex-wrap: wrap;
+ justify-content: center;
+ margin-top: .25rem;
+}
+.onb-tag {
+ font-size: .78rem;
+ padding: .25rem .75rem;
+ border-radius: 20px;
+ background: rgba(26,188,156,.09);
+ border: 1px solid rgba(26,188,156,.25);
+ color: #1abc9c;
+ font-weight: 600;
+}
+
+/* قائمة المميزات */
+.onb-list {
+ list-style: none;
+ width: 100%;
+ text-align: right;
+ margin-top: .25rem;
+}
+.onb-list li {
+ padding: .4rem 0;
+ border-bottom: 1px solid rgba(255,255,255,.05);
+ font-size: .87rem;
+ color: #c5cad8;
+ display: flex;
+ gap: .5rem;
+ align-items: flex-start;
+}
+.onb-list li:last-child { border-bottom: none; }
+
+/* مستويات النقاط */
+.onb-levels {
+ display: flex;
+ align-items: center;
+ gap: .4rem;
+ flex-wrap: wrap;
+ justify-content: center;
+ margin-top: .5rem;
+}
+.onb-lv {
+ font-size: .82rem;
+ font-weight: 700;
+ background: rgba(26,188,156,.09);
+ border: 1px solid rgba(26,188,156,.2);
+ color: #e8eaf0;
+ padding: .3rem .7rem;
+ border-radius: 20px;
+}
+.onb-arrow {
+ color: #1abc9c;
+ font-size: .9rem;
+ font-weight: 700;
+}
+
+/* نصائح الشريحة الأخيرة */
+.onb-tips {
+ display: flex;
+ flex-direction: column;
+ gap: .55rem;
+ width: 100%;
+ margin-top: .5rem;
+}
+.onb-tip {
+ background: rgba(26,188,156,.07);
+ border: 1px solid rgba(26,188,156,.18);
+ border-radius: 10px;
+ padding: .6rem 1rem;
+ font-size: .86rem;
+ color: #c5cad8;
+ text-align: right;
+}
+
+/* ── شريط التنقل السفلي ── */
+.onb-nav {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 1rem 1.75rem 1.4rem;
+ border-top: 1px solid rgba(255,255,255,.06);
+ flex-shrink: 0;
+}
+
+/* أزرار السابق/التالي */
+.onb-btn-prev,
+.onb-btn-next {
+ background: transparent;
+ border: 1.5px solid rgba(26,188,156,.35);
+ color: #1abc9c;
+ font-size: .88rem;
+ font-weight: 700;
+ padding: .5rem 1.1rem;
+ border-radius: 22px;
+ cursor: pointer;
+ transition: .2s;
+ font-family: inherit;
+ min-width: 90px;
+}
+.onb-btn-prev:hover,
+.onb-btn-next:hover {
+ background: rgba(26,188,156,.1);
+}
+/* زر "ابدأ الآن" في الشريحة الأخيرة */
+.onb-btn-next.onb-finish {
+ background: linear-gradient(135deg, #1abc9c, #16a085);
+ color: #000;
+ border-color: transparent;
+ box-shadow: 0 4px 16px rgba(26,188,156,.35);
+ padding: .55rem 1.4rem;
+}
+.onb-btn-next.onb-finish:hover {
+ box-shadow: 0 6px 22px rgba(26,188,156,.5);
+ transform: scale(1.04);
+}
+
+/* نقاط المؤشر */
+.onb-dots {
+ display: flex;
+ gap: .45rem;
+ align-items: center;
+}
+.onb-dot {
+ width: 7px;
+ height: 7px;
+ border-radius: 50%;
+ background: rgba(255,255,255,.18);
+ transition: .25s;
+}
+.onb-dot.active {
+ background: #1abc9c;
+ width: 18px;
+ border-radius: 4px;
+}
+
+/* ── جهاز الهاتف الصغير ── */
+@media (max-width: 400px) {
+ .onb-modal { border-radius: 16px; }
+ .onb-slide { padding: 2rem 1.2rem 1rem; }
+ .onb-title { font-size: 1.15rem; }
+ .onb-big-icon { font-size: 3rem; }
+ .onb-nav { padding: .8rem 1.2rem 1.1rem; }
+ .onb-btn-prev, .onb-btn-next { min-width: 75px; font-size: .82rem; padding: .45rem .85rem; }
+}
+
+/* ═══════════════════════════════════════════════
+ ONBOARDING OVERLAY — أول زيارة فقط
+ ═══════════════════════════════════════════════ */
+.onb-overlay{position:fixed;inset:0;z-index:99999;display:flex;align-items:center;justify-content:center;padding:1rem;animation:onbFadeIn .35s ease;}
+.onb-overlay.hidden{display:none !important;}
+@keyframes onbFadeIn{from{opacity:0}to{opacity:1}}
+.onb-backdrop{position:absolute;inset:0;background:rgba(5,8,18,.88);backdrop-filter:blur(6px);}
+.onb-modal{position:relative;z-index:1;background:#0f1524;border:1px solid rgba(26,188,156,.25);border-radius:22px;width:100%;max-width:440px;max-height:90vh;overflow:hidden;box-shadow:0 24px 80px rgba(0,0,0,.7),0 0 0 1px rgba(26,188,156,.1);animation:onbSlideUp .4s cubic-bezier(.22,.68,0,1.2);display:flex;flex-direction:column;}
+@keyframes onbSlideUp{from{transform:translateY(40px) scale(.97);opacity:0}to{transform:translateY(0) scale(1);opacity:1}}
+.onb-progress{height:3px;background:rgba(255,255,255,.07);border-radius:3px 3px 0 0;overflow:hidden;flex-shrink:0;}
+.onb-progress-fill{height:100%;background:linear-gradient(90deg,#1abc9c,#16a085);border-radius:3px;transition:width .4s ease;width:0%;}
+.onb-skip-btn{position:absolute;top:.9rem;left:1rem;background:rgba(255,255,255,.06);border:1px solid rgba(255,255,255,.1);color:#6b7280;font-size:.78rem;padding:.3rem .75rem;border-radius:20px;cursor:pointer;transition:.2s;z-index:2;font-family:inherit;}
+.onb-skip-btn:hover{color:#e74c3c;border-color:rgba(231,76,60,.4);background:rgba(231,76,60,.08);}
+.onb-slide{display:none;padding:2.5rem 1.75rem 1.25rem;flex-direction:column;align-items:center;text-align:center;animation:onbSlideFade .3s ease;flex:1;overflow-y:auto;}
+.onb-slide.active{display:flex;}
+@keyframes onbSlideFade{from{opacity:0;transform:translateX(-12px)}to{opacity:1;transform:translateX(0)}}
+.onb-slide-visual{position:relative;margin-bottom:1.25rem;}
+.onb-big-icon{font-size:3.8rem;line-height:1;filter:drop-shadow(0 0 18px rgba(26,188,156,.35));}
+.onb-icons-row{display:flex;gap:.75rem;font-size:2.2rem;justify-content:center;}
+.onb-pulse-rings{position:absolute;inset:-20px;pointer-events:none;}
+.onb-ring{position:absolute;inset:0;border:2px solid rgba(26,188,156,.35);border-radius:50%;animation:onbPulseRing 2.4s ease-out infinite;}
+.onb-ring.r2{animation-delay:.8s;inset:-10px;}
+.onb-ring.r3{animation-delay:1.6s;inset:-20px;}
+@keyframes onbPulseRing{0%{transform:scale(.6);opacity:.8}100%{transform:scale(1.4);opacity:0}}
+.onb-title{font-size:1.35rem;font-weight:800;color:#e8eaf0;margin-bottom:.6rem;line-height:1.4;}
+.onb-green{color:#1abc9c;}
+.onb-desc{font-size:.92rem;color:#a0a8bb;line-height:1.75;margin-bottom:.75rem;}
+.onb-tags{display:flex;gap:.5rem;flex-wrap:wrap;justify-content:center;margin-top:.25rem;}
+.onb-tag{font-size:.78rem;padding:.25rem .75rem;border-radius:20px;background:rgba(26,188,156,.09);border:1px solid rgba(26,188,156,.25);color:#1abc9c;font-weight:600;}
+.onb-list{list-style:none;width:100%;text-align:right;margin-top:.25rem;}
+.onb-list li{padding:.4rem 0;border-bottom:1px solid rgba(255,255,255,.05);font-size:.87rem;color:#c5cad8;display:flex;gap:.5rem;align-items:flex-start;}
+.onb-list li:last-child{border-bottom:none;}
+.onb-levels{display:flex;align-items:center;gap:.4rem;flex-wrap:wrap;justify-content:center;margin-top:.5rem;}
+.onb-lv{font-size:.82rem;font-weight:700;background:rgba(26,188,156,.09);border:1px solid rgba(26,188,156,.2);color:#e8eaf0;padding:.3rem .7rem;border-radius:20px;}
+.onb-arrow{color:#1abc9c;font-size:.9rem;font-weight:700;}
+.onb-tips{display:flex;flex-direction:column;gap:.55rem;width:100%;margin-top:.5rem;}
+.onb-tip{background:rgba(26,188,156,.07);border:1px solid rgba(26,188,156,.18);border-radius:10px;padding:.6rem 1rem;font-size:.86rem;color:#c5cad8;text-align:right;}
+.onb-nav{display:flex;align-items:center;justify-content:space-between;padding:1rem 1.75rem 1.4rem;border-top:1px solid rgba(255,255,255,.06);flex-shrink:0;}
+.onb-btn-prev,.onb-btn-next{background:transparent;border:1.5px solid rgba(26,188,156,.35);color:#1abc9c;font-size:.88rem;font-weight:700;padding:.5rem 1.1rem;border-radius:22px;cursor:pointer;transition:.2s;font-family:inherit;min-width:90px;}
+.onb-btn-prev:hover,.onb-btn-next:hover{background:rgba(26,188,156,.1);}
+.onb-btn-next.onb-finish{background:linear-gradient(135deg,#1abc9c,#16a085);color:#000;border-color:transparent;box-shadow:0 4px 16px rgba(26,188,156,.35);padding:.55rem 1.4rem;}
+.onb-btn-next.onb-finish:hover{box-shadow:0 6px 22px rgba(26,188,156,.5);transform:scale(1.04);}
+.onb-dots{display:flex;gap:.45rem;align-items:center;}
+.onb-dot{width:7px;height:7px;border-radius:50%;background:rgba(255,255,255,.18);transition:.25s;}
+.onb-dot.active{background:#1abc9c;width:18px;border-radius:4px;}
+@media(max-width:400px){
+ .onb-modal{border-radius:16px;}
+ .onb-slide{padding:2rem 1.2rem 1rem;}
+ .onb-title{font-size:1.15rem;}
+ .onb-big-icon{font-size:3rem;}
+ .onb-nav{padding:.8rem 1.2rem 1.1rem;}
+ .onb-btn-prev,.onb-btn-next{min-width:75px;font-size:.82rem;padding:.45rem .85rem;}
+}
+
+/* ============================================================
+ ⚙️ SETTINGS MODAL
+ ============================================================ */
+.settings-panel{
+ background:var(--card);border-radius:1.2rem 1.2rem 0 0;
+ position:fixed;bottom:0;left:0;right:0;z-index:10500;
+ max-height:88vh;overflow-y:auto;
+ padding-bottom:env(safe-area-inset-bottom,0);
+ animation:slideUpPanel .32s cubic-bezier(.22,1,.36,1);
+}
+@keyframes slideUpPanel{from{transform:translateY(100%)}to{transform:translateY(0)}}
+.settings-header{
+ display:flex;align-items:center;justify-content:space-between;
+ padding:1rem 1.2rem .6rem;border-bottom:1px solid var(--border);
+ position:sticky;top:0;background:var(--card);z-index:2;
+}
+.settings-header h2{margin:0;font-size:1.1rem;color:var(--text1);}
+.settings-body{padding:.4rem 0 1.2rem;}
+.settings-section{padding:.75rem 1.2rem;border-bottom:1px solid var(--border);}
+.settings-section-title{font-size:.78rem;font-weight:700;color:var(--accent);text-transform:uppercase;letter-spacing:.04em;margin-bottom:.65rem;}
+.settings-row{display:flex;align-items:center;justify-content:space-between;gap:1rem;margin-bottom:.5rem;}
+.settings-row:last-child{margin-bottom:0;}
+.settings-row-info{flex:1;}
+.settings-row-label{font-size:.92rem;color:var(--text1);}
+.settings-row-sub{font-size:.78rem;color:var(--text2);margin-top:.1rem;}
+.settings-danger-btn{
+ width:100%;padding:.65rem 1rem;border-radius:.6rem;border:1px solid rgba(231,76,60,.3);
+ background:rgba(231,76,60,.08);color:#e74c3c;font-size:.88rem;cursor:pointer;
+ font-family:inherit;text-align:right;
+}
+.settings-danger-btn:hover{background:rgba(231,76,60,.15);}
+
+/* Toggle Switch */
+.toggle-switch{position:relative;display:inline-block;width:46px;height:26px;flex-shrink:0;}
+.toggle-switch input{opacity:0;width:0;height:0;}
+.toggle-slider{
+ position:absolute;cursor:pointer;inset:0;
+ background:rgba(255,255,255,.12);border-radius:13px;
+ transition:.3s;
+}
+.toggle-slider:before{
+ content:'';position:absolute;height:20px;width:20px;border-radius:50%;
+ left:3px;bottom:3px;background:#fff;
+ transition:.3s;box-shadow:0 1px 4px rgba(0,0,0,.3);
+}
+.toggle-switch input:checked + .toggle-slider{background:var(--accent);}
+.toggle-switch input:checked + .toggle-slider:before{transform:translateX(20px);}
+
+/* Font size buttons */
+.font-size-row{display:flex;gap:.5rem;}
+.fs-btn{
+ flex:1;padding:.55rem;border-radius:.6rem;border:1px solid var(--border);
+ background:var(--bg2);color:var(--text2);cursor:pointer;font-family:inherit;
+ transition:.2s;
+}
+.fs-btn:hover,.active-fs{background:var(--accent);color:#fff;border-color:var(--accent);}
+.fs-btn-mid{font-size:1.05rem;}
+.fs-btn-lg{font-size:1.2rem;}
+
+/* Blocked users list */
+.blocked-list{display:flex;flex-direction:column;gap:.4rem;}
+.blocked-item{
+ display:flex;align-items:center;justify-content:space-between;
+ padding:.5rem .6rem;background:var(--bg2);border-radius:.6rem;
+}
+.blocked-item-name{font-size:.88rem;color:var(--text1);}
+.blocked-unblock-btn{
+ font-size:.78rem;padding:.3rem .65rem;border-radius:.45rem;
+ background:rgba(26,188,156,.12);color:var(--accent);border:1px solid rgba(26,188,156,.25);
+ cursor:pointer;font-family:inherit;
+}
+
+/* ============================================================
+ 🔔 PRAYER ALARM CARD
+ ============================================================ */
+.prayer-alarm-card{padding:.8rem 1rem!important;}
+.prayer-alarm-row{display:flex;align-items:center;justify-content:space-between;gap:1rem;}
+.prayer-alarm-title{font-size:.95rem;color:var(--text1);font-weight:600;}
+.prayer-alarm-sub{font-size:.78rem;color:var(--text2);margin-top:.15rem;}
+
+/* ============================================================
+ 🧮 CURRENCY CALCULATOR
+ ============================================================ */
+.currency-calc{padding:1rem!important;}
+.calc-row{display:flex;gap:.6rem;align-items:stretch;}
+.calc-inp{flex:1;font-size:1.15rem;text-align:center;font-weight:700;}
+.calc-sel{flex:0 0 auto;width:auto;min-width:130px;font-size:.82rem;}
+.calc-swap-row{display:flex;justify-content:center;margin:.4rem 0;}
+.calc-swap-btn{
+ width:36px;height:36px;border-radius:50%;border:1px solid var(--accent);
+ background:rgba(26,188,156,.1);color:var(--accent);font-size:1.1rem;cursor:pointer;
+ display:flex;align-items:center;justify-content:center;
+}
+.calc-result-box{
+ flex:1;background:rgba(26,188,156,.1);border:1px solid rgba(26,188,156,.3);
+ border-radius:.55rem;padding:.6rem 1rem;font-size:1.15rem;font-weight:700;
+ color:var(--accent);display:flex;align-items:center;justify-content:center;
+ min-height:42px;
+}
+.calc-note{font-size:.75rem;color:var(--text2);margin-top:.6rem;text-align:center;}
+
+/* ============================================================
+ 🚫 BLOCK BUTTON IN DM
+ ============================================================ */
+#dmBlockBtn.blocked-active{color:#e74c3c!important;}
+
+/* ============================================================
+ 📡 NEARBY BANNER — إشعار الإضافات الجديدة من المنطقة
+ ============================================================ */
+.nearby-banner {
+ position: fixed;
+ top: 70px;
+ left: 50%;
+ transform: translateX(-50%);
+ z-index: 9999;
+ min-width: 280px;
+ max-width: calc(100vw - 2rem);
+ animation: nb-slidein 0.4s cubic-bezier(.22,1,.36,1) both;
+}
+.nearby-banner.nb-fadeout {
+ animation: nb-fadeout 0.5s ease forwards;
+}
+@keyframes nb-slidein {
+ from { opacity:0; transform:translateX(-50%) translateY(-20px); }
+ to { opacity:1; transform:translateX(-50%) translateY(0); }
+}
+@keyframes nb-fadeout {
+ from { opacity:1; transform:translateX(-50%) translateY(0); }
+ to { opacity:0; transform:translateX(-50%) translateY(-12px); }
+}
+.nearby-banner-inner {
+ display: flex;
+ align-items: center;
+ gap: .8rem;
+ background: linear-gradient(135deg, rgba(26,188,156,.95), rgba(22,160,133,.98));
+ color: #fff;
+ border-radius: 1rem;
+ padding: .75rem 1rem;
+ box-shadow: 0 8px 30px rgba(0,0,0,.35);
+ backdrop-filter: blur(10px);
+ border: 1px solid rgba(255,255,255,.2);
+ direction: rtl;
+}
+.nearby-banner-icon {
+ font-size: 1.8rem;
+ flex-shrink: 0;
+ animation: nb-pulse 1s ease-in-out 2;
+}
+@keyframes nb-pulse {
+ 0%,100% { transform: scale(1); }
+ 50% { transform: scale(1.2); }
+}
+.nearby-banner-text {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ gap: .2rem;
+ font-size: .88rem;
+ line-height: 1.4;
+}
+.nearby-banner-text strong {
+ font-weight: 700;
+ font-size: .9rem;
+}
+.nearby-banner-close {
+ background: rgba(255,255,255,.2);
+ border: none;
+ color: #fff;
+ width: 26px;
+ height: 26px;
+ border-radius: 50%;
+ cursor: pointer;
+ font-size: .85rem;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex-shrink: 0;
+ transition: background .2s;
+}
+.nearby-banner-close:hover { background: rgba(255,255,255,.35); }
+
+/* ============================================================
+ 🔊 SOUND SETTINGS — إعدادات الصوت
+ ============================================================ */
+.sound-test-btn {
+ display: inline-flex;
+ align-items: center;
+ gap: .4rem;
+ background: rgba(26,188,156,.15);
+ border: 1px solid var(--accent);
+ color: var(--accent);
+ border-radius: 2rem;
+ padding: .35rem .9rem;
+ font-size: .82rem;
+ cursor: pointer;
+ transition: all .2s;
+ font-family: inherit;
+}
+.sound-test-btn:hover {
+ background: rgba(26,188,156,.3);
+}
+
+
+/* ============================================================
+ 🏘️ HOOD GROUPS - مجموعات الأحياء CSS
+ ============================================================ */
+
+/* Section header */
+.hood-top { background: linear-gradient(135deg,#1a2a1a 0%,#0f2415 100%); }
+.hood-top h2 { color: #4caf50; }
+
+/* Groups list */
+.hood-list { display: flex; flex-direction: column; gap: .75rem; }
+
+/* Group card */
+.hood-card {
+ background: var(--dark3);
+ border: 1px solid var(--border);
+ border-radius: var(--r);
+ padding: .9rem;
+ cursor: pointer;
+ transition: border-color .2s, transform .15s;
+}
+.hood-card:hover { border-color: #4caf50; transform: translateY(-1px); }
+
+.hood-card-top { display: flex; align-items: flex-start; gap: .7rem; }
+.hood-card-ico { font-size: 1.8rem; line-height: 1; min-width: 2.2rem; text-align: center; }
+.hood-card-info { flex: 1; min-width: 0; }
+.hood-card-name { font-weight: 700; color: var(--text); font-size: 1rem; }
+.hood-card-area { font-size: .8rem; color: var(--text2); margin-top: .15rem; }
+.hood-card-type-badge {
+ display: inline-block; margin-top: .3rem;
+ background: rgba(76,175,80,.15); color: #4caf50;
+ font-size: .72rem; padding: .15rem .55rem; border-radius: 99px;
+}
+.hood-card-desc {
+ font-size: .83rem; color: var(--text2); margin-top: .55rem;
+ display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
+}
+.hood-card-footer {
+ display: flex; gap: .9rem; margin-top: .55rem;
+ font-size: .78rem; color: var(--text2);
+}
+.hood-card-time { margin-right: auto; }
+
+/* Join button on card */
+.hood-join-btn {
+ background: #4caf50; color: #fff; border: none;
+ border-radius: 99px; padding: .3rem .85rem; font-size: .8rem;
+ cursor: pointer; white-space: nowrap; transition: background .2s;
+ align-self: center;
+}
+.hood-join-btn:hover { background: #388e3c; }
+.hood-join-btn.joined { background: var(--dark4); color: #4caf50; border: 1px solid #4caf50; }
+
+/* Group page tabs */
+.hood-tabs {
+ display: flex; background: var(--dark2);
+ border-bottom: 1px solid var(--border);
+}
+.hood-tab {
+ flex: 1; background: none; border: none; color: var(--text2);
+ padding: .7rem .5rem; font-size: .85rem; cursor: pointer;
+ border-bottom: 2px solid transparent; transition: color .2s, border-color .2s;
+}
+.hood-tab.active { color: #4caf50; border-bottom-color: #4caf50; }
+
+/* Tab content */
+.hood-tab-content { overflow-y: auto; max-height: calc(100vh - 180px); }
+
+/* Compose area */
+.gp-compose {
+ padding: .8rem;
+ background: var(--dark2);
+ border-bottom: 1px solid var(--border);
+}
+
+/* Post item */
+.hood-post-item {
+ padding: .85rem 1rem;
+ border-bottom: 1px solid var(--border);
+}
+.hood-post-meta {
+ display: flex; align-items: center; gap: .5rem;
+ font-size: .8rem; color: var(--text2); margin-bottom: .4rem;
+}
+.hood-post-type-ico { font-size: 1rem; }
+.hood-post-author { font-weight: 600; color: var(--text); }
+.hood-post-time { margin-right: auto; }
+.hood-post-text { font-size: .9rem; color: var(--text); line-height: 1.55; }
+.hood-like-btn {
+ margin-top: .4rem; background: none; border: 1px solid var(--border);
+ color: var(--text2); border-radius: 99px; padding: .2rem .7rem;
+ font-size: .78rem; cursor: pointer; transition: border-color .2s, color .2s;
+}
+.hood-like-btn:hover { border-color: #e74c3c; color: #e74c3c; }
+
+/* Nomination item */
+.hood-nom-item {
+ background: var(--dark3); border: 1px solid var(--border);
+ border-radius: var(--rs); padding: .8rem; margin-bottom: .6rem;
+}
+.hood-nom-title { font-weight: 700; color: #f1c40f; margin-bottom: .3rem; }
+.hood-nom-desc { font-size: .85rem; color: var(--text2); margin-bottom: .5rem; }
+.hood-nom-footer { display: flex; align-items: center; justify-content: space-between; }
+.hood-nom-author { font-size: .78rem; color: var(--text2); }
+.hood-vote-btn {
+ background: rgba(241,196,15,.12); color: #f1c40f;
+ border: 1px solid rgba(241,196,15,.3); border-radius: 99px;
+ padding: .25rem .8rem; font-size: .8rem; cursor: pointer;
+ transition: background .2s;
+}
+.hood-vote-btn:hover { background: rgba(241,196,15,.25); }
+
+/* Members */
+.hood-member-row {
+ display: flex; align-items: center; gap: .7rem;
+ padding: .6rem 0; border-bottom: 1px solid var(--border);
+}
+.hood-member-ava { font-size: 1.5rem; }
+.hood-member-name { font-size: .9rem; color: var(--text); flex: 1; }
+.hood-owner-badge {
+ background: rgba(76,175,80,.15); color: #4caf50;
+ font-size: .72rem; padding: .15rem .5rem; border-radius: 99px;
+}
+
+/* gp-action-btn (join in header) */
+.gp-action-btn {
+ background: #4caf50; color: #fff; border: none;
+ border-radius: 99px; padding: .35rem 1rem; font-size: .82rem;
+ cursor: pointer; white-space: nowrap; transition: background .2s;
+}
+.gp-action-btn:hover { background: #388e3c; }
+.gp-action-btn.joined { background: var(--dark4); color: #4caf50; border: 1px solid #4caf50; }
+
+/* Create modal card — hood modal uses .hood-modal-overlay instead */
+.hood-modal-overlay {
+ position: fixed; inset: 0; background: rgba(0,0,0,.75);
+ z-index: 5500; display: flex; align-items: center; justify-content: center;
+ padding: 1rem;
+}
+.hood-modal-overlay.hidden { display: none !important; }
+.modal-card {
+ background: var(--dark2); border: 1px solid var(--border);
+ border-radius: var(--r); width: 100%; max-width: 420px;
+ max-height: 90vh; overflow-y: auto;
+}
+.modal-header {
+ display: flex; align-items: center; justify-content: space-between;
+ padding: .9rem 1rem; border-bottom: 1px solid var(--border);
+ font-weight: 700; color: var(--text); font-size: 1rem;
+}
+.modal-close {
+ background: none; border: none; color: var(--text2);
+ font-size: 1.2rem; cursor: pointer; padding: .2rem .5rem;
+}
+.modal-close:hover { color: var(--accent); }
+
+
+/* ── Hood Empty State ─────────────────────────────────────── */
+.hood-empty {
+ display: flex; flex-direction: column; align-items: center;
+ justify-content: center; padding: 3rem 1.5rem; text-align: center;
+}
+.hood-empty-ico { font-size: 3rem; margin-bottom: .8rem; }
+.hood-empty-title{ font-size: 1.1rem; font-weight: 700; color: var(--text); margin-bottom: .4rem; }
+.hood-empty-sub { font-size: .85rem; color: var(--text2); line-height: 1.6; }
+
+/* ══════════════════════════════════════════════════════
+ 🏘️ HOOD GROUPS — ENHANCED STYLES v7.9
+ ══════════════════════════════════════════════════════ */
+
+/* Card badges */
+.hood-card-badges { display:flex; gap:.3rem; flex-wrap:wrap; margin-top:.25rem; }
+.hood-card-type-badge {
+ display:inline-block; font-size:.7rem; padding:.15rem .5rem;
+ background:rgba(76,175,80,.15); color:#4caf50;
+ border:1px solid rgba(76,175,80,.3); border-radius:99px;
+}
+.hood-meeting-badge {
+ display:inline-block; font-size:.7rem; padding:.15rem .5rem;
+ background:rgba(52,152,219,.15); color:#3498db;
+ border:1px solid rgba(52,152,219,.3); border-radius:99px;
+}
+.hood-joined-badge {
+ display:inline-block; font-size:.7rem; padding:.15rem .5rem;
+ background:rgba(76,175,80,.2); color:#4caf50;
+ border:1px solid #4caf50; border-radius:99px;
+}
+
+/* ── Hood Group Page header ───────────────────────────── */
+.hgp-header {
+ display:flex; align-items:center; gap:.6rem;
+ padding:.8rem 1rem; background:var(--dark2);
+ border-bottom:1px solid var(--border); position:sticky; top:0; z-index:10;
+}
+.hgp-avatar {
+ font-size:2.2rem; width:2.6rem; height:2.6rem;
+ display:flex; align-items:center; justify-content:center;
+ background:rgba(76,175,80,.12); border-radius:50%; flex-shrink:0;
+}
+.hgp-actions { display:flex; align-items:center; gap:.4rem; flex-shrink:0; }
+.hgp-share-btn {
+ background:rgba(76,175,80,.1); border:1px solid rgba(76,175,80,.3);
+ color:#4caf50; border-radius:99px; width:2.1rem; height:2.1rem;
+ display:flex; align-items:center; justify-content:center;
+ cursor:pointer; font-size:1rem; transition:background .2s;
+}
+.hgp-share-btn:hover { background:rgba(76,175,80,.25); }
+
+/* Stats bar */
+.hgp-stats {
+ display:flex; gap:.5rem; flex-wrap:wrap; align-items:center;
+ padding:.45rem 1rem; background:rgba(76,175,80,.05);
+ border-bottom:1px solid var(--border); font-size:.78rem; color:var(--text2);
+}
+.hgp-contact { color:#4caf50; }
+
+/* Compose area */
+.hgp-compose { padding:.75rem; border-bottom:1px solid var(--border); background:var(--dark3); }
+.hgp-type-sel { margin-bottom:.45rem; width:100%; }
+.hgp-input-row { display:flex; gap:.5rem; align-items:flex-end; }
+.hgp-ta { flex:1; resize:none; min-height:44px; }
+.hgp-send-btn {
+ background:#4caf50; color:#fff; border:none;
+ border-radius:50%; width:2.4rem; height:2.4rem; flex-shrink:0;
+ font-size:1.1rem; cursor:pointer; transition:background .2s;
+ display:flex; align-items:center; justify-content:center;
+}
+.hgp-send-btn:hover { background:#388e3c; }
+.hgp-send-btn:disabled { background:var(--dark4); color:var(--text2); }
+
+/* Posts list */
+.hgp-posts-list { padding:.6rem; }
+.hood-post-item {
+ background:var(--dark3); border:1px solid var(--border);
+ border-radius:var(--rs); padding:.75rem; margin-bottom:.55rem;
+ transition:border-color .2s;
+}
+.hood-post-item:hover { border-color:rgba(76,175,80,.3); }
+.hood-post-item.hood-post-meeting { border-left:3px solid #3498db; }
+.hood-post-item.hood-post-initiative{ border-left:3px solid #f1c40f; }
+.hood-post-item.hood-post-nomination{ border-left:3px solid #e67e22; }
+.hood-post-meta {
+ display:flex; align-items:center; gap:.45rem; margin-bottom:.4rem;
+ font-size:.8rem;
+}
+.hood-post-type-ico { font-size:.95rem; }
+.hood-post-author { font-weight:600; color:var(--text); }
+.hood-post-time { margin-right:auto; color:var(--text2); font-size:.74rem; }
+.hood-post-text { font-size:.9rem; color:var(--text); line-height:1.6; }
+.hood-like-btn {
+ margin-top:.5rem; background:transparent;
+ border:1px solid var(--border); color:var(--text2);
+ border-radius:99px; padding:.2rem .7rem; font-size:.8rem;
+ cursor:pointer; transition:all .2s;
+}
+.hood-like-btn:hover { border-color:#e74c3c; color:#e74c3c; }
+.hood-like-btn.liked { border-color:#e74c3c; color:#e74c3c; background:rgba(231,76,60,.08); }
+
+/* Meeting info block inside post */
+.hood-meeting-info {
+ display:flex; flex-wrap:wrap; gap:.4rem; align-items:center;
+ margin:.45rem 0 .3rem; padding:.4rem .6rem;
+ background:rgba(52,152,219,.08); border:1px solid rgba(52,152,219,.2);
+ border-radius:var(--rs); font-size:.8rem; color:#3498db;
+}
+.hood-cal-btn {
+ background:rgba(52,152,219,.15); border:1px solid rgba(52,152,219,.35);
+ color:#3498db; border-radius:99px; padding:.15rem .55rem;
+ font-size:.75rem; cursor:pointer; transition:background .2s;
+}
+.hood-cal-btn:hover { background:rgba(52,152,219,.3); }
+
+/* ── Meetings tab ──────────────────────────────────────── */
+.hgp-meetings-list { display:flex; flex-direction:column; gap:.6rem; }
+.hood-meeting-card {
+ display:flex; gap:.75rem; align-items:flex-start;
+ background:var(--dark3); border:1px solid var(--border);
+ border-radius:var(--rs); padding:.8rem; transition:border-color .2s;
+}
+.hood-meeting-card.upcoming { border-left:3px solid #3498db; }
+.hood-meeting-card.past { border-left:3px solid var(--border); opacity:.65; }
+.hmc-date {
+ display:flex; flex-direction:column; align-items:center;
+ justify-content:center; min-width:2.6rem; text-align:center;
+ background:rgba(52,152,219,.1); border-radius:var(--rs); padding:.35rem .5rem;
+}
+.hmc-day { font-size:1.3rem; font-weight:800; color:#3498db; line-height:1; }
+.hmc-mon { font-size:.7rem; color:var(--text2); }
+.hmc-info { flex:1; }
+.hmc-title { font-weight:600; color:var(--text); font-size:.9rem; margin-bottom:.2rem; }
+.hmc-time, .hmc-place { font-size:.8rem; color:var(--text2); }
+.hmc-author { font-size:.75rem; color:var(--text2); margin-top:.25rem; }
+.hmc-actions { display:flex; flex-direction:column; align-items:flex-end; gap:.3rem; }
+.hmc-status {
+ font-size:.72rem; padding:.12rem .45rem; border-radius:99px;
+}
+.hmc-status.upcoming { background:rgba(52,152,219,.15); color:#3498db; }
+.hmc-status.past { background:rgba(127,140,141,.12); color:var(--text2); }
+
+/* ── Nominations improved ─────────────────────────────── */
+.hood-nom-item {
+ background:var(--dark3); border:1px solid var(--border);
+ border-radius:var(--rs); padding:.8rem; margin-bottom:.6rem;
+ transition:border-color .2s;
+}
+.hood-nom-item:hover { border-color:rgba(241,196,15,.3); }
+.hood-nom-top { display:flex; align-items:center; gap:.4rem; flex-wrap:wrap; margin-bottom:.3rem; }
+.hood-nom-rank { font-size:1.1rem; }
+.hood-nom-title { font-weight:700; color:#f1c40f; flex:1; }
+.hood-nom-cat {
+ font-size:.72rem; padding:.12rem .45rem;
+ background:rgba(241,196,15,.1); color:#f1c40f;
+ border:1px solid rgba(241,196,15,.25); border-radius:99px;
+}
+.hood-nom-desc { font-size:.85rem; color:var(--text2); margin-bottom:.5rem; }
+.hood-nom-footer { display:flex; align-items:center; justify-content:space-between; }
+.hood-nom-author { font-size:.78rem; color:var(--text2); }
+.hood-vote-btn {
+ background:rgba(241,196,15,.12); color:#f1c40f;
+ border:1px solid rgba(241,196,15,.3); border-radius:99px;
+ padding:.25rem .8rem; font-size:.8rem; cursor:pointer; transition:all .2s;
+}
+.hood-vote-btn:hover { background:rgba(241,196,15,.25); }
+.hood-vote-btn.voted { background:rgba(39,174,96,.15); color:#27ae60; border-color:rgba(39,174,96,.4); }
+
+/* ── Members improved ─────────────────────────────────── */
+.hood-members-header {
+ font-size:.85rem; color:var(--text2); margin-bottom:.6rem; padding-bottom:.5rem;
+ border-bottom:1px solid var(--border);
+}
+.hood-member-row {
+ display:flex; align-items:center; gap:.7rem;
+ padding:.55rem 0; border-bottom:1px solid rgba(255,255,255,.04);
+}
+.hood-member-ava { font-size:1.5rem; width:2rem; text-align:center; }
+.hood-member-name { font-size:.9rem; color:var(--text); flex:1; }
+.hood-owner-badge {
+ background:rgba(241,196,15,.12); color:#f1c40f;
+ font-size:.72rem; padding:.12rem .45rem; border-radius:99px;
+ border:1px solid rgba(241,196,15,.3);
+}
+
+/* ── City filter in list ──────────────────────────────── */
+#hoodCityFilter {
+ background:var(--dark3); border:1px solid var(--border);
+ color:var(--text); border-radius:var(--rs);
+}
+#hoodCityFilter option { background:var(--dark2); }
+
+/* ── Geo drop for hood area input ────────────────────── */
+#hoodAreaDrop { top:100%; z-index:100; }
+
+/* ── q-hood quick card ───────────────────────────────── */
+.q-hood {
+ border-color: rgba(76,175,80,.35);
+ background: rgba(76,175,80,.07);
+}
+.q-hood:hover {
+ border-color: rgba(76,175,80,.65);
+ box-shadow: 0 6px 20px rgba(76,175,80,.2);
+}
+.q-hood::before { background: linear-gradient(135deg, rgba(76,175,80,.06), rgba(46,125,50,.04)); }
+
diff --git a/public/favicon.svg b/public/favicon.svg
new file mode 100644
index 0000000..fabfb36
--- /dev/null
+++ b/public/favicon.svg
@@ -0,0 +1,3 @@
+
+ 💓
+
diff --git a/public/guide.html b/public/guide.html
new file mode 100644
index 0000000..4197a56
--- /dev/null
+++ b/public/guide.html
@@ -0,0 +1,1265 @@
+
+
+
+
+
+ نبض — دليل المستخدم الشامل
+
+
+
+
+
+
+
+ 💚 نبض
+ 🚀 افتح التطبيق
+
+
+
+
+
+
+
+
+
+
+ ✅ مجاني 100%
+ 📱 يعمل على جميع الأجهزة
+ 🔴 بيانات لحظية مباشرة
+ ⚡ سريع وخفيف
+ 🔒 آمن وخصوصيتك محفوظة
+ 🌐 يدعم العربية بالكامل
+ 📴 يعمل بدون إنترنت جزئياً
+ 🆘 نداء الاستغاثة الفوري
+
+
+
+
+
+
+
+
+
+
+
1
+
+
افتح التطبيق في المتصفح
+
افتح الرابط sudan-1.onrender.com من أي متصفح (Chrome, Safari, Firefox). لا تحتاج لتسجيل حساب لتصفح معظم الأقسام.
+
+
+
+
2
+
+
ثبّته كتطبيق على هاتفك
+
اضغط على زر "⬇️ ثبّت التطبيق" من القائمة الجانبية أو اختر "إضافة إلى الشاشة الرئيسية" من متصفحك. سيعمل كتطبيق عادي بعد ذلك.
+
+
+
+
3
+
+
فعّل خاصية الموقع الجغرافي
+
اسمح للتطبيق بالوصول لموقعك لتحصل على بيانات مخصصة لمنطقتك: أسعار الصرف، الأدوية القريبة، التنبيهات المحلية، وأوقات الصلاة الدقيقة.
+
+
+
+
4
+
+
أكمل ملفك الشخصي واربح نقاطاً
+
أدخل اسمك ومنطقتك من قسم "الملف الشخصي". ابدأ بالمشاركة لكسب النقاط والشارات والارتقاء في لوحة المتصدرين.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
🗺️
+
الخريطة الحية
+
خريطة تفاعلية تُظهر جميع البلاغات والأشخاص والأحداث في لحظتها الفعلية. تدعم أربعة أنماط عرض وطبقات متعددة.
+
+ 🔴 خطر 🟡 تحذير 🔵 معلومة 🌡️ كثافة حرارية
+
+
+
+
+
📢
+
بلّغ الآن
+
أبلغ عن أي حادث أو أزمة فوراً مع الموقع الجغرافي والصور. تنتشر التنبيهات للمستخدمين في المنطقة خلال ثوانٍ.
+
+ 📍 موقع تلقائي 📸 صور ⚡ فوري
+
+
+
+
+
🆘
+
نداء الاستغاثة (SOS)
+
في حالات الطوارئ القصوى — يُرسل نداء فوري لجميع المستخدمين ضمن نطاق 100 كم ويضع موقعك على الخريطة على الفور.
+
+ 🚨 طوارئ 📡 100 كم ⚡ فوري
+
+
+
+
+
💱
+
صرّاف الشعب
+
أسعار الصرف اللحظية بالجنيه السوداني (SDG) مقابل الدولار واليورو والريال. مُبلَّغ عنها من المجتمع مباشرةً مع مخطط تاريخي.
+
+ 💵 USD 💶 EUR 💸 SDG 📊 مخطط
+
+
+
+
+
💊
+
دواء موجود
+
ابحث عن أي دواء في أقرب صيدلية لموقعك. يمكنك أيضاً إضافة معلومة توافر دواء لمساعدة المجتمع.
+
+ 📍 قريب مني ✅ متوفر ❌ غير متوفر
+
+
+
+
+
🩸
+
بنك الدم
+
ابحث عن متبرع بالدم حسب فصيلتك وموقعك، أو سجّل نفسك كمتبرع، أو أرسل طلب دم عاجل.
+
+ A+ B+ O+ AB+ +4 فصائل
+
+
+
+
+
⚡
+
جدول الكهرباء
+
تقارير انقطاع وجداول الكهرباء مُبلَّغ عنها من المجتمع لكل منطقة. صوّت للتأكيد أو أضف جدولاً جديداً.
+
+ 🗳️ تصويت 📅 جداول 📍 بالمنطقة
+
+
+
+
+
🕌
+
أوقات الصلاة
+
أوقات الصلاة الدقيقة حسب موقعك الجغرافي مع عداد تنازلي للصلاة القادمة ودعم لجميع المناطق.
+
+ ⏰ عداد 📍 جغرافي 🌙 هجري
+
+
+
+
+
🏥
+
دليل المستشفيات
+
دليل شامل للمستشفيات والعيادات والمختبرات والصيدليات مع خاصية البحث وتقييم الخدمة.
+
+ 🏥 مستشفى 🏨 عيادة 🔬 مختبر ⭐ تقييم
+
+
+
+
+
🛒
+
سوق P2P المباشر
+
بيع واشترِ وتبادل مباشرةً بين الأشخاص. إلكترونيات، ملابس، أغذية، سيارات، عقارات، خدمات وأكثر.
+
+ بيع شراء تبادل 📸 صور
+
+
+
+
+
🤝
+
بورصة المهارات
+
قدّم مهارتك واطلب مهارة أخرى بالمقايضة أو الأجر. منصة تبادل خدمات بين أبناء المجتمع.
+
+ 💡 تقديم 🔍 طلب 🔄 تبادل
+
+
+
+
+
🚗
+
مشاركة التنقل
+
وفّر تنقلك مع جيرانك — ابحث عن رحلة مشتركة أو أعلن عن رحلتك لتشارك التكلفة.
+
+ 🗓️ موعد 💺 مقاعد 💰 سعر
+
+
+
+
+
🎓
+
مجموعات التعلم
+
غرف دراسية بمستويات مختلفة (ابتدائي حتى مهني). دردشة جماعية ومكالمات صوتية/مرئية WebRTC مدمجة.
+
+ 💬 دردشة 🎙️ صوتي 📹 مرئي
+
+
+
+
+
📦
+
طلبات المساعدة
+
اطلب مساعدة في الغذاء أو الدواء أو المواصلات أو المأوى أو المال. علّم طلبك "عاجل" للأولوية.
+
+ 🍞 غذاء 💊 دواء 🚌 نقل 🏠 مأوى
+
+
+
+
+
🗳️
+
استطلاعات الرأي
+
شارك برأيك في القضايا المجتمعية وتابع نتائج الاستطلاعات بشكل مباشر ولحظي.
+
+ 📊 نتائج حية 🗳️ تصويت
+
+
+
+
+
📰
+
الأخبار المحلية
+
أخبار من المجتمع ومصنّفة (سياسة، اقتصاد، أمن، صحة، رياضة). التحقق بالتصويت الجماعي.
+
+ سياسة اقتصاد أمن صحة
+
+
+
+
+
💬
+
الرسائل المباشرة
+
تراسل مباشر بين المستخدمين مع دعم النصوص والصور والرسائل الصوتية ومؤشر الكتابة.
+
+ 📸 صور 🎙️ صوت ⌨️ مؤشر كتابة
+
+
+
+
+
🔍
+
البحث عن الأشخاص
+
ابحث عن أي شخص بالاسم أو الرقم أو البريد الإلكتروني أو الشركة. يمكنك تحديد موقعه على الخريطة.
+
+ 👤 اسم 📞 هاتف ✉️ بريد
+
+
+
+
+
💧
+
خريطة المياه
+
صوّت على حالة المياه في منطقتك (موجودة/مقطوعة/ضعيفة) وتابع تقارير المنطقة.
+
+ ✅ موجودة ❌ مقطوعة ⚠️ ضعيفة
+
+
+
+
+
🌤️
+
الطقس
+
حالة الطقس الآنية حسب موقعك مع إمكانية البحث عن أي مدينة. نصائح تكيّف مع الأحوال الجوية.
+
+ 🌡️ درجة حرارة 💨 رياح 💧 رطوبة
+
+
+
+
+
🎤
+
صوت الحي
+
ارفع مشكلة في حيّك أو منطقتك (كهرباء، ماء، طرق، صحة، أمن) وصوّت المجتمع عليها.
+
+ ⚡ كهرباء 💧 ماء 🛣️ طرق
+
+
+
+
+
📊
+
لوحة الإحصاءات
+
إحصاءات شاملة: المستخدمون الآن، التقارير الإجمالية، الأرواح المنقذة، أكثر المدن نشاطاً.
+
+ 👥 مباشر 📈 تقارير 🏙️ مدن
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 🗺️ الخريطة
+ 💱 الصراف
+ 🩸 الدم
+ 🛒 السوق
+ 🎓 التعلم
+ 👤 الملف
+
+
+
+
+
+
+
+
+
🗺️ الخريطة الحية
+
قلب تطبيق نبض — خريطة تفاعلية تُظهر كل ما يحدث في السودان والعالم لحظةً بلحظة.
+
+ أربعة أنماط عرض: داكن، فاتح، أقمار اصطناعية، طبوغرافي
+ طبقات: تنبيهات، أشخاص، كثافة حرارية، حدود الولايات
+ فلاتر: خطر، تحذير، معلومة، أشخاص، SOS، قريب مني
+ أدوات عائمة: بلّغ الآن، نداء الاستغاثة، موقعي الحالي
+ بحث بالمنطقة أو الحي
+ عداد التنبيهات والأشخاص المتصلين
+
+
+
+
+ 🔴
+ حريق في منطقة الرياض، الخرطوم
+ خطر
+
+
+ 🟡
+ تدهور طريق شارع النيل
+ تحذير
+
+
+ 🔵
+ فعالية ثقافية في المركز
+ معلومة
+
+
+ 🆘
+ نداء استغاثة — حي العمارات
+ عاجل
+
+
+
+
+
+
+
+
+
+
💱 صرّاف الشعب
+
أسعار الصرف الشعبية اللحظية — يُشاركها المجتمع من كل مكان في السودان.
+
+ سعر الدولار، اليورو، الريال السعودي مقابل الجنيه
+ مخطط تاريخي يُظهر التذبذبات
+ أعلى سعر، أدنى سعر، المتوسط اليومي
+ أضف سعرك الآن مع تحديد موقعك
+ تحديث تلقائي لحظي عبر Socket.IO
+ مقارنة بين مناطق مختلفة
+
+
+
+
+ 💵
+ دولار أمريكي (USD)
+ SDG 1,850
+
+
+ 💶
+ يورو أوروبي (EUR)
+ SDG 2,010
+
+
+ 💸
+ ريال سعودي (SAR)
+ SDG 493
+
+
+ 📊
+ مخطط آخر 7 أيام
+ ↗ صاعد
+
+
+
+
+
+
+
+
+
+
🩸 بنك الدم
+
منصة تبرع الدم — تربط المحتاجين بالمتبرعين في أسرع وقت.
+
+ البحث عن متبرع بالفصيلة والمدينة
+ إرسال طلب دم عاجل مع إشعار فوري
+ تسجيل كمتبرع دائم
+ دعم جميع الفصائل الثماني
+ إحصاءات التبرع بالمنطقة
+ تنبيهات الطوارئ للمتبرعين القريبين
+
+
+
+
+ 🩸
+ طلب عاجل — فصيلة O+
+ عاجل
+
+
+ ✅
+ متبرع متاح — A+ — الخرطوم
+ متاح
+
+
+ 📍
+ 3 متبرعين في نطاق 5 كم
+ قريب
+
+
+ 🏆
+ +25 نقطة مكافأة التبرع
+ مكافأة
+
+
+
+
+
+
+
+
+
+
🛒 سوق P2P المباشر
+
سوق شعبي رقمي — بيع واشترِ مباشرةً بدون وسيط.
+
+ نشر إعلان مع صور متعددة
+ 9 فئات: إلكترونيات، ملابس، أغذية، أدوية، سيارات، عقارات، خدمات...
+ دعم عدة عملات: SDG، دولار، يورو، ريال، تبادل
+ فلتر "قريب مني" للعثور على الأقرب
+ تعديل وحذف إعلانك في أي وقت
+ نظام الإعجاب والمشاهدات لمتابعة الاهتمام
+
+
+
+
+ 📱
+ iPhone 13 — حالة ممتازة
+ SDG 450K
+
+
+ 🚗
+ تويوتا 2018 — خرطوم
+ للبيع
+
+
+ 🔄
+ تبادل أدوات كهربائية
+ تبادل
+
+
+ 📍
+ إعلانات على بعد 2 كم
+ قريب
+
+
+
+
+
+
+
+
+
+
🎓 مجموعات التعلم
+
غرف دراسية افتراضية تجمع الطلاب والمعلمين في بيئة تعليمية تفاعلية.
+
+ 7 مستويات: ابتدائي، متوسط، ثانوي، جامعي، مهني، عام، لغات
+ دردشة نصية جماعية لحظية
+ مكالمات صوتية ومرئية مدمجة (WebRTC)
+ مؤشر الكتابة الحي
+ مشاركة الملفات والصور
+ 20 نقطة مكافأة عن كل 5 رسائل دراسية
+
+
+
+
+ 🎒
+ مجموعة ثانوي — الرياضيات
+ 15 عضو
+
+
+ 🎓
+ مجموعة جامعي — طب
+ نشط
+
+
+ 🔧
+ مجموعة مهني — برمجة
+ 8 عضو
+
+
+ 🎙️
+ مكالمة صوتية جارية...
+ مباشر
+
+
+
+
+
+
+
+
+
+
👤 الملف الشخصي
+
هويتك الرقمية في نبض — أكمله لتحصل على أقصى استفادة من المنصة.
+
+ صورة شخصية وصورة غلاف مخصصة
+ اسم مستعار وإيموجي شخصي
+ إخفاء رقم الهاتف (اختياري)
+ شارة التوثيق للحسابات الموثوقة
+ عرض نقاطك ومستواك وشاراتك
+ QR Code شخصي للمشاركة السريعة
+
+
+
+
+ ⭐
+ المستوى 4 — متقدم
+ 250 نقطة
+
+
+ 🏆
+ شارة بطل المجتمع
+ مكتسب
+
+
+ 🔥
+ سلسلة 7 أيام متواصلة
+ نشط
+
+
+ 📊
+ المركز 12 في لوحة المتصدرين
+ أسبوعي
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
المستويات الستة
+
+
🌱 جديد 0 – 20 نقطة المستوى 1
+
📗 مبتدئ 20 – 80 نقطة المستوى 2
+
⚡ نشيط 80 – 200 نقطة المستوى 3
+
🔥 متقدم 200 – 500 نقطة المستوى 4
+
🌟 نجم 500 – 1000 نقطة المستوى 5
+
👑 أسطورة 1000+ نقطة المستوى 6
+
+
+
شارات الإنجاز
+
+
🚨 أول بلاغ +10 نقطة أرسل أول بلاغ حقيقي
+
🤝 مساعد +45 نقطة ردّ على طلبات المساعدة
+
🦸 بطل المجتمع +100 نقطة قدّم مساعدة كبرى
+
📰 مراسل ميداني +120 نقطة غطاء إخباري مستمر
+
🩸 واهب الحياة +50 نقطة التسجيل أو التبرع بالدم
+
🔗 الرابط +50 نقطة دعوة أصدقاء للتطبيق
+
👁️ اليقظ +40 نقطة التحقق من التنبيهات
+
🎓 العالم +30 نقطة المشاركة في التعلم
+
🔥 المتواصل +70 نقطة 7 أيام نشاط متواصل
+
⭐ أسطورة نبض +500 نقطة إنجاز المستوى الأعلى
+
+
+
التحديات اليومية
+
+
🚨 بلاغ عاجل
أرسل بلاغاً حقيقياً واحداً
⭐ +10 نقطة + شارة 🚨
+
🩸 واهب الدم
سجّل أو اطلب تبرعاً بالدم
⭐ +25 نقطة + شارة 🩸
+
📲 ناشر الخير
شارك التطبيق مع 3 أشخاص
⭐ +15 نقطة
+
📚 الطالب النشيط
أرسل 5 رسائل في مجموعة دراسية
⭐ +20 نقطة + شارة 🎓
+
🤝 يد العون
ردّ على طلب مساعدة
⭐ +30 نقطة + شارة 🤝
+
🗳️ الناخب النشيط
صوّت على 5 تنبيهات مختلفة
⭐ +10 نقطة
+
🛒 السوق النشط
انشر إعلاناً في السوق
⭐ +15 نقطة + شارة 🛒
+
+
+
+
+
+
+
+
+
+
+
🇸🇩
+
18 ولاية سودانية
+
تغطية كاملة لجميع ولايات السودان مع بيانات المدن والأحياء: الخرطوم، الجزيرة، نهر النيل، البحر الأحمر، كسلا، القضارف، نيالا، والمزيد.
+
+
+
🌍
+
تغطية عالمية
+
يدعم التطبيق أيضاً المدن العالمية للمغتربين السودانيين في مصر، السعودية، الإمارات، قطر، وجميع أنحاء العالم.
+
+
+
📍
+
البحث بالحي
+
ابحث بدقة حتى مستوى الحي والشارع. الخرطوم وحدها تضم أكثر من 22 حياً في قاعدة البيانات الجغرافية.
+
+
+
+
+
+
+
+
+
+
+
⚙️ Node.js
+
🔌 Socket.IO
+
🗺️ Leaflet
+
📱 PWA
+
🎙️ WebRTC
+
🚀 Express 5
+
🌐 REST API
+
📦 Service Worker
+
+
+ 145+ واجهة API
+ 388+ وظيفة JavaScript
+ 23,500+ سطر كود
+ SW v9 · Cache فوري
+ RTL عربي كامل
+ متوافق مع كل الهواتف
+
+
+
+
+
+
+
+
+
+
+
+
هل التطبيق مجاني تماماً؟ ▼
+
نعم، نبض مجاني 100% بدون أي رسوم أو اشتراكات. جميع الخدمات متاحة للجميع بلا استثناء.
+
+
+
+
هل يحتاج التطبيق لتسجيل حساب؟ ▼
+
يمكنك تصفح معظم الأقسام بدون تسجيل. لكن لنشر البلاغات والمشاركة في المجتمع وكسب النقاط، يُنصح بإنشاء ملف شخصي بسيط (اسم + موقع).
+
+
+
+
كيف أثبّت التطبيق على هاتفي؟ ▼
+
من Chrome أو Safari، افتح الرابط ثم اضغط على "إضافة إلى الشاشة الرئيسية" من قائمة المتصفح. أو اضغط على زر "ثبّت التطبيق" داخل القائمة الجانبية في نبض نفسه.
+
+
+
+
هل يعمل التطبيق بدون إنترنت؟ ▼
+
نعم جزئياً! بفضل Service Worker، يتم تخزين أساسيات التطبيق محلياً. عند انقطاع الإنترنت تظهر صفحة Offline مع البيانات المخزنة مسبقاً.
+
+
+
+
هل بياناتي الشخصية آمنة؟ ▼
+
نعم. يمكنك إخفاء رقم هاتفك من إعدادات الملف الشخصي. لا يُشارك أي معلومات شخصية مع طرف ثالث. التطبيق يستخدم HTTPS مشفراً.
+
+
+
+
ما هو نداء الاستغاثة SOS وكيف يعمل؟ ▼
+
في حالات الطوارئ القصوى فقط — اضغط زر SOS، سيُرسل نداء فوري لجميع المستخدمين في نطاق 100 كيلومتر مع إظهار موقعك على الخريطة الحية للجميع.
+
+
+
+
كيف تعمل نقاط المكافآت؟ ▼
+
كل مساهمة تمنحك نقاطاً: بلاغ (+10)، تبرع دم (+25)، رد مساعدة (+30)... تتراكم النقاط لترفع مستواك من "جديد" إلى "أسطورة" مع شارات حصرية ومركز في لوحة المتصدرين.
+
+
+
+
هل التطبيق متاح خارج السودان؟ ▼
+
نعم! يمكن استخدامه من أي مكان في العالم. يدعم البحث الجغرافي العالمي ويتيح للمغتربين السودانيين متابعة أخبار الوطن والتواصل مع المجتمع.
+
+
+
+
+
+
+
+
+
+
+
جاهز للانضمام لمجتمع نبض؟
+
أكثر من مجرد تطبيق — نبض هو صوت كل مواطن سوداني يريد مجتمعاً أفضل.
+
+
+
+
+
+
+
+ 💚 نبض — مجتمع للجميع · السودان والعالم
+
+ sudan-1.onrender.com
+ · الإصدار v7.6 · Service Worker v9
+
+
+ مبني بـ ❤️ لخدمة المجتمع السوداني
+
+
+
+
+
+
diff --git a/public/images/icon-192.png b/public/images/icon-192.png
new file mode 100644
index 0000000..e9a8bab
Binary files /dev/null and b/public/images/icon-192.png differ
diff --git a/public/images/icon-512.png b/public/images/icon-512.png
new file mode 100644
index 0000000..8304063
Binary files /dev/null and b/public/images/icon-512.png differ
diff --git a/public/index.html b/public/index.html
new file mode 100644
index 0000000..f8ae38b
--- /dev/null
+++ b/public/index.html
@@ -0,0 +1,3487 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ نبض | Nabdh - نبض المدينة الحي v7.0
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
نبض
+
Nabdh · نبض المدينة الحي
+
+
جاري تحديد موقعك تلقائياً...
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
نبض المدينة الحي
+
اعرف ما يحدث حولك أينما كنت في العالم
+
+
0 مستخدم نشط
+
0 تقرير موثق
+
0 حياة أُنقذت
+
0 منطقة
+
+
+
+
+
+ 📍
+ جاري التحديد التلقائي...
+ 🔄 تحديث
+
+
+
+
+
🔔
+
مرحباً بك في نبض - الموقع يتحدد تلقائياً 💓
+
+
+
+
+
+ 🆘 نداء استغاثة
+
+
+
+
+
+
⚡ الوصول السريع
+
+ 🗺️ الخريطة الحية أحداث منطقتك
+ 🔍 البحث عن أشخاص مثل Truecaller
+ 💵 صرّاف الشعب سعر الصرف
+ 📢 صوت الحي مشاكل مجتمعك
+ 🛒 سوق P2P بيع وتبادل مباشر
+ 🚨 بلّغ الآن شارك خبراً عاجلاً
+ 🔗 شارك التطبيق أخبر أصدقاءك
+ 🩸 بنك الدم تبرع أو ابحث
+ ⚡ جدول الكهرباء انقطاع حيّك
+ 🕌 أوقات الصلاة حسب موقعك
+ 🏥 دليل المستشفيات أقرب مرفق صحي
+ 📰 الأخبار المحلية أخبار مجتمعية
+ 🚗 مشاركة التنقل شارك مشوارك
+ 🌦️ الطقس حالة مدينتك
+ 💧 خريطة المياه انقطاع وتوزيع
+ 🎓 مجموعات التعلم تعلّم مع الآخرين
+ 🏘️ مجموعات الأحياء نظافة · مبادرات · ترشيح
+ 📦 طلبات المساعدة أطلب أو قدّم
+ 🗳️ استطلاعات الرأي شارك رأيك
+ 📊 لوحة الإحصاءات إحصاءات حية
+
+
+
+
+
+
+
+
+
+
+
--- ج.س / دولار
+
لم يُبلَّغ عن أي سعر بعد
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
🎯 تحدي اليوم
+
جاري التحميل...
+
+
+
🏅 المكافأة: نقاط + شارة
+
+
+
+
+
+
+
+
+
+ 💬 الدردشة العامة
+
+
+ 🔍 ابحث عن شخص
+
+
+
+
+
+
+
🔗
+
+
ادعُ صديقاً واكسب نقاطاً!
+
كل صديق تدعوه = 20 نقطة مجانية لك
+
+
+20 🏆
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 🌐 الكل
+ 🔴 خطر
+ 🟡 تحذير
+ 🟢 معلومة
+ 👥 أشخاص
+ 📡 قريب مني
+ 🆘 SOS
+
+
+
+
+
+
+
+
+ 0 خطر
+
+
+ 0 تحذير
+
+
+ 0 معلومة
+
+
+ 0 شخص
+
+
0 إجمالي
+
+ — °م
+
+
+
+
+
+
+
🌐 مستخدمون قريبون منك
+
+
+
+
+
+
+
+
+
+
+
+
🔍 البحث عن الأشخاص
+
بحث شامل - أشخاص، شركات، أرقام، مهارات وإعلانات
+
+
+
+
+
+
+ 🌐 الكل
+ 👤 اسم
+ 📱 رقم
+ ✉️ بريد
+ 🏢 شركة
+
+
+ 🔍
+
+ بحث
+
+
+
+ 0 نتيجة
+ •
+ بحث في الأشخاص والإعلانات والمهارات
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 🌱
+ جديد
+
+
+
+
+
+
+
+
+
+
+
+
+
+
👤
+
+
📷
+
+
+
+
+
+
+
+
+
+
مستخدم نبض
+ ✅
+
+
+
+
+ 💓 نبض
+ 🟢 نشط
+ 🌱 جديد
+
+
+
+
+
+
+
+ رسائلي
+
+
+
+ موقعي
+
+
+
+
+
+ مشاركة
+
+
+
+
+
+ QR
+
+
+
+ طوارئ
+
+
+
+ بلاغ
+
+
+
+
+
+
+ التقدم نحو المستوى التالي
+ 0 / 100 XP
+
+
+
+ 🔥
+ سلسلة 1 يوم
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
✕
+
📲 QR كود ملفك
+
+
+
+
+ 📋 نسخ الرابط
+
+
+
+
+
+
+
+
+
+
+
+
+
🆘 نداء استغاثة
+
اختر نوع الاستغاثة
+
📞 اتصال بالطوارئ 999
+
📡 أرسل نداء للمحيطين بك
+
📤 شارك موقعك الآن
+
إلغاء
+
+
+
+
+
💬 الرسائل المباشرة
+
تواصل خاص ومباشر مع أي شخص في نبض
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 💬 انضم للدردشة العامة
+
+
+
+
+
+
+
+
+
💵 صرّاف الشعب
+
سعر الصرف الحقيقي من السوق مباشرة - يتحدث تلقائياً بموقعك
+
+
+
+
سعر الدولار الآن
+
---
+
جنيه سوداني
+
لم يُبلَّغ بعد
+
+
+
- أعلى اليوم
+
- أدنى اليوم
+
- المتوسط
+
+
+
+
📈 منحنى السعر
+
+
+
+
+
🧮 حاسبة الصرف السريعة
+
+
+
+
+
📤 شارك السعر من مكانك
+
+
+
+
🕐 آخر الأسعار المُبلَّغ عنها
+
+
+
+
+
+
+
+
💊 دواء موجود
+
ابحث عن دوائك في أقرب صيدلية - يعمل بموقعك تلقائياً
+
+
+
+
+
+
+ الكل
+ ✅ متوفر
+ ❌ غير متوفر
+ 📡 قريب مني
+
+
+
+
+
+
+
+
+
+
📢 صوت الحي
+
صوتك يصنع التغيير - يُرسل بموقعك الفعلي
+
+
+
+
+
+
+
+
🤝 بورصة المهارات
+
تبادل الخدمات بدون نقود - P2P حقيقي
+
+
+
💡 عرض مهارتك واحصل على ما تحتاجه من مجتمعك - مجاناً تماماً
+
+
➕ أضف مهارتك
+
+
+
+
+
+
+
+
🛒 سوق P2P المباشر
+
بيع • شراء • تبادل مباشر بين الناس بناءً على موقعك الفعلي
+
+
+
+ 🌐 الكل
+ 💰 بيع
+ 🛍️ شراء
+ 🔄 تبادل
+ 📡 قريب مني
+
+
+
+ الكل
+ 📱 إلكترونيات
+ 👕 ملابس
+ 🥦 أغذية
+ 💊 أدوية
+ 🚗 سيارات
+ 🏠 عقارات
+ 🛠️ خدمات
+ 📦 أخرى
+
+
+
+
+
+
+
+
+
+
+
🚨 بلّغ الآن
+
شارك معلومة عاجلة وأنقذ حياة - الموقع يُرفق تلقائياً
+
+
+
+ 🔴 خطر
+ 🟡 تحذير
+ 🟢 معلومة
+
+
+
+
+
+ 🆘 نداء استغاثة طارئ
+
+
+
+
+
💡 نصائح للتبليغ الفعّال
+
+ ✅ كن دقيقاً في وصف المكان والحدث
+ ✅ اذكر الوقت التقريبي للحدث
+ ✅ الموقع يُرفق تلقائياً بدون أي ضغط
+ ✅ يمكنك النقر على الخريطة لتحديد الموقع يدوياً
+ ✅ لا تشارك معلومات شخصية حساسة
+
+
+
+
+
+
+
+
+
🩸 بنك الدم
+
تبرع بالدم أو ابحث عن متبرع - أنقذ حياة الآن
+
+
+
+
+ 🔍 ابحث عن متبرع
+ 🆘 طلب عاجل
+ 🩸 سجّل كمتبرع
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
⚡ جدول الكهرباء
+
شارك جدول الانقطاع في حيّك - ساعد جيرانك
+
+
+
+
+
⚡
+
+ حالة الكهرباء في منطقتك
+ —
+
+
+ ✅ موجودة
+ ❌ مقطوعة
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
🕌 أوقات الصلاة
+
أوقات دقيقة حسب موقعك الجغرافي
+
+
+
+
+
الصلاة القادمة
+
—
+
--:--:--
+
+
+
+
+
+
+
+
+ 📍 يتحدد الموقع تلقائياً...
+ 🔄 تحديث
+
+
+
+
+ طريقة الحساب
+
+ مسلم العالم (الإخوان - الجامعة الإسلامية)
+ أم القرى
+ المجلس الإسلامي للأمريكا الشمالية
+ رابطة العالم الإسلامي
+
+
+
+
+
+
+
+
+
+
+
🏥 دليل المستشفيات
+
ابحث عن أقرب مستشفى أو عيادة وقيّمها
+
+
+
+
+
+ 🔍
+
+
+ كل الأنواع
+ 🏥 مستشفى
+ 🩺 عيادة
+ 🔬 مختبر
+ 💊 صيدلية
+ 🚨 طوارئ
+
+
+
+ 📡 قريب مني (20كم)
+ 🔄 تحديث
+ + أضف
+
+
+
+
+
+
+
+
+
+
+
+
+
📰 الأخبار المحلية
+
أخبار مجتمعية موثوقة من السكان أنفسهم
+
+
+
+
+
+
+ 🌐 الكل
+ 🏛️ سياسة
+ 💰 اقتصاد
+ 🛡️ أمن
+ 🏥 صحة
+ ⚽ رياضة
+ 📋 عام
+
+
+
+
+
+
+
+
+
+
+
+
🚗 مشاركة التنقل
+
شارك رحلتك أو ابحث عن مشوار
+
+
+
+
+
+
+
+
🌦️ الطقس
+
حالة الطقس حسب موقعك الجغرافي
+
+
+
+
+
⏳ جاري تحديد الطقس...
+
+
+
+ 📍 يتحدد الموقع تلقائياً...
+ 🔄 تحديث
+
+
+
+
+
+
+
+
+
+
+
+
💧 خريطة المياه
+
تقارير انقطاع المياه وأماكن التوزيع
+
+
+
+
+
💧
+
+ حالة المياه في منطقتك
+ —
+
+
+ ✅ موجودة
+ ❌ مقطوعة
+
+
+
+
+
+
+
+
+
+
+
+
+
🏘️ مجموعات الأحياء
+
تواصل مع جيرانك — نظافة، اجتماعات، مبادرات، ترشيحات
+
+
+
+
+
+
+ 🔍
+
+
+
+ 🌐 الكل
+ 🧹 نظافة
+ 📅 اجتماعات
+ 💡 مبادرة
+ ⭐ ترشيح
+ 💬 عام
+
+
+
+ 📍
+
+ جميع المدن والأحياء
+
+
+
+
+
+
+ ➕ إنشاء مجموعة حي جديدة
+
+
+
+
+
+
+
+
+
+
+
+
🎓 مجموعات التعلم
+
تعلّم مع الآخرين وشارك معرفتك
+
+
+
+
+
+ 🔍
+
+
+
+ 🌐 الكل
+ 🌱 ابتدائي
+ 📗 متوسط
+ 📘 ثانوي
+ 🎓 جامعي
+ ⚙️ مهني
+
+
+
+
+
+
+
+
+
+
+ 🔴
+ جاري التسجيل...
+ 0:00
+ ✕ إلغاء
+
+
+
+
+
+
+
+
+
+
+
📦 طلبات المساعدة
+
اطلب مساعدة أو قدّمها لمن يحتاج
+
+
+
+
+ 🌐 الكل
+ 🍞 غذاء
+ 💊 دواء
+ 🚗 مواصلات
+ 🏠 مأوى
+ 💵 مالي
+ 📋 أخرى
+ 🚨 عاجل
+
+
+
+
+
+
+
+
+
+
+
+
🗳️ استطلاعات الرأي
+
شارك برأيك في قضايا مجتمعك
+
+
+
+
+
+
+
+
📊 لوحة الإحصاءات
+
إحصاءات التطبيق والمجتمع بشكل مباشر
+
+
+
+
+ 🔄 تحديث
+
+
+
+
+
+
+
+
+
+
🔄 تحديث الإحصاءات
+
+
+
+
+
+
+
+
+
+ 📅 هذا الأسبوع
+ 🌟 كل الوقت
+ 🏙️ مدينتي
+
+
+
+
مركزك الآن
+
#---
+
0 نقطة
+
+
+
+
+
+
+
+
+ 🆘 أحتاج مساعدة
+ 🤝 أنا أساعد
+ 📢 أبلّغ الآن
+ 🗺️ الخريطة الحية
+
+
✕ إغلاق
+
+
+
+
+
+
+
+
+ 💬 واتساب
+ ✈️ تيليغرام
+ 🐦 تويتر/X
+ 👥 فيسبوك
+ 📋 نسخ الرابط
+ ↗️ مشاركة
+
+
+ 👁️ 0 مشاهدة
+ 🔁 0 مشاركة
+ 🔥 1 نقطة حرارة
+
+
+
+
+
+
+
+
+
+
🏅
+
+
إنجاز جديد!
+
حصلت على شارة
+
+
+
+
+
+
+
+
+
+ 👥 0 عضو
+ 💬 0 منشور
+ ⭐ 0 ترشيح
+
+
+
+
+ 💬 المنشورات
+ 📅 الاجتماعات
+ ⭐ الترشيحات
+ 👥 الأعضاء
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 💬 الدردشة
+ 👥 الأعضاء
+ 📎 الوسائط
+ 📋 المعلومات
+
+
+
+
+
+
+
+
+ 🎙️ مكالمة جارية
+ 0:00
+
+
+ 🎙️
+ 📹
+ 📵
+
+
+
+
+
+
+
+
+
+
+
+
+ يكتب...
+
+
+
+
+
+
+
+
+
+
+
+ 😊 😂 ❤️ 👍 👎 🔥 💯 🎉 😢 😡 🤔 👏 🙏 💪 🎓 📚 ✅ ❌ ⭐ 🏆
+ 😀 😃 😄 😁 😆 😅 🤣 😇 😍 🥰 😘 🤩 😎 🤓 😏 😒 😞 😔 😟 😕
+ 🙂 🙃 😉 😌 😛 😜 😝 🤑 🤗 🤭 🤫 🤥 😶 😐 😑 😬 🙄 😯 😦 😧
+
+
+
+
+
+ 🔴
+ جاري التسجيل...
+ 0:00
+ ✕ إلغاء
+
+
+
+
+
+
+
+
+
+ 📷 صور
+ 🎬 فيديو
+ 🎵 صوت
+ 📁 ملفات
+
+
+
+
+
+
+
+
+
+
+
🔗 دعوة للمجموعة
+
جاري التوليد...
+
+ 📋 نسخ الرابط
+ ↗️ مشاركة
+
+
+
✕ إغلاق
+
+
+
+
+
+
+
📞
+
مكالمة واردة
+
مكالمة صوتية
+
+ ✅ قبول
+ ❌ رفض
+
+
+
+
+
+
+
+
+
+
+
+
+
+ مكالمة جارية
+ 0:00
+
+
+ 🎙️
+ 📹
+ 📵
+
+
+
+
+
+
+
+
+
+
+ يكتب...
+
+
+
+
+
+ 😊 😂 ❤️ 👍 👎 🔥 💯 🎉 😢 😡 🤔 👏 🙏 💪 🎓 📚 ✅ ❌ ⭐ 🏆
+ 😀 😃 😄 😁 😆 😅 🤣 😇 😍 🥰 😘 🤩 😎 🤓 😏 😒 😞 😔 😟 😕
+
+
+
+ 🔴
+ جاري التسجيل...
+ 0:00
+ ✕ إلغاء
+
+
+
+
+
+ 🏠 الرئيسية
+ 🗺️ الخريطة
+ 🚨
+ 🔍 بحث
+ 💬 رسائل 0
+
+
+
+
+
+
+
+ 📵 غير متصل — تعمل في وضع offline
+
+
+
+
+
+
💓
+
+ ثبّت نبض على جهازك
+ يعمل بدون إنترنت · أسرع · إشعارات فورية
+
+
+
+ 📲 ثبّت
+ ✕
+
+
+
+
+
+
+
+
+
+
💓
+
مرحباً في نبض!
+
كيف تريد أن يناديك المجتمع؟
+
+
+ ✅ انضم الآن
+ تخطّ
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
✕
+
+
+
+
+
+
امسح الكود للوصول المباشر
+
+
+
+
+
+ 📋 نسخ
+
+
+
+
+
+
+
+
+ —
+ مستخدم نشط
+
+
+
+ —
+ تقرير
+
+
+
+ —
+ مدينة
+
+
+
+
+
+
+
+ 🆘
+
+
+
+
⬆️
+
+
+
+
+
+
+
+
+
+
🔤 حجم الخط
+
+ ص
+ م
+ ك
+
+
+
+
+
+
🔕 الوضع الهادئ
+
+
+
إيقاف جميع الإشعارات مؤقتاً
+
يمكنك إعادة تفعيلها في أي وقت
+
+
+
+
+
+
+
+
+
+
+
🕌 إشعارات الصلاة
+
+
+
منبّه قبل الصلاة بـ 5 دقائق
+
يعمل على متصفحك المحلي
+
+
+
+
+
+
+
+
+
+
+
🔊 التنبيهات الصوتية
+
+
+
تفعيل الأصوات للأحداث الجديدة
+
تنبيه صوتي عند كل حدث أو إضافة قريبة
+
+
+
+
+
+
+
+ ▶ اختبار الصوت
+
+
+
+
+
+
🔔 الإشعارات
+
+
+
إشعارات النقاط والإنجازات
+
+
+
+
+
+
+
+
+
إشعارات التقارير الجديدة القريبة
+
صوت + banner عند حدث في نطاق 50 كم
+
+
+
+
+
+
+
+
+
إشعارات الرسائل المباشرة
+
+
+
+
+
+
+
+
+
+
+
🚫 المستخدمون المحظورون
+
+
+
+
+
+
🗑️ البيانات
+
🗑️ مسح ذاكرة التخزين المؤقت
+
📥 تصدير بياناتي
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
تخطي ✕
+
+
+
+
+
+
أهلاً بك في نبض
+
المنصة المجتمعية الأولى للسودان — أخبار حية، أسعار الصرف، خدمات طوارئ، وتواصل مجتمعي كل ذلك في مكان واحد.
+
+ 🆓 مجاني
+ 📱 بلا تحميل
+ 🔴 مباشر
+
+
+
+
+
+
+
الخريطة الحية والبلاغات
+
شاهد كل ما يحدث حولك لحظةً بلحظة على الخريطة التفاعلية — بلاغات الخطر، التحذيرات، ومواقع الأشخاص.
+
+ 🔴 أبلغ عن حادث أو أزمة فوراً
+ 🆘 نداء استغاثة يصل لـ 100 كم
+ 🌡️ خريطة كثافة حرارية للمناطق
+ 📍 موقعك يُحدَّد تلقائياً
+
+
+
+
+
+
+
خدمات يومية لا غنى عنها
+
كل ما تحتاجه يومياً في متناول يدك:
+
+ 💱 صرّاف الشعب — أسعار صرف لحظية
+ 💊 دواء موجود — أقرب صيدلية لك
+ 🩸 بنك الدم — تبرع أو ابحث عن متبرع
+ ⚡ جدول الكهرباء — بتقارير مجتمعية
+ 🕌 أوقات الصلاة — دقيقة حسب موقعك
+
+
+
+
+
+
+
مجتمع نشط وسوق حر
+
تواصل مع جيرانك وشارك في بناء المجتمع:
+
+ 🛒 سوق P2P — بيع واشترِ بلا وسيط
+ 🎓 مجموعات التعلم — دراسة جماعية مباشرة
+ 🤝 بورصة المهارات — تبادل الخدمات
+ 📰 الأخبار المحلية — بتحقق جماعي
+ 📦 طلبات المساعدة — اطلب أو قدّم يد العون
+
+
+
+
+
+
+
اربح نقاطاً وارتقِ في المستويات
+
كل مساهمة تمنحك نقاطاً لترتقي من جديد إلى أسطورة نبض :
+
+ 🌱 جديد
+ →
+ ⚡ نشيط
+ →
+ 🔥 متقدم
+ →
+ 👑 أسطورة
+
+
+ 🚨 بلاغ عاجل ← +10 نقطة
+ 🩸 تبرع بالدم ← +25 نقطة
+ 🤝 رد على مساعدة ← +30 نقطة
+
+
+
+
+
+
+
أنت جاهز الآن!
+
استكشف نبض وشارك مجتمعك. يمكنك دائماً قراءة الدليل الكامل من القائمة الجانبية.
+
+
📍 فعّل خاصية الموقع لأفضل تجربة
+
📲 ثبّت التطبيق على شاشتك الرئيسية
+
👤 أكمل ملفك الشخصي لكسب نقاط
+
+
+
+
+
+
+ ← السابق
+
+
+
+ التالي →
+
+
+
+
+
+
+
+
+
diff --git a/public/js/app.js b/public/js/app.js
new file mode 100644
index 0000000..1fb745e
--- /dev/null
+++ b/public/js/app.js
@@ -0,0 +1,10650 @@
+/* ===== NABDH نبض - app.js v5 - Profiles + People Search + DM + Live Map ===== */
+
+const API = '';
+let socket = null;
+let map = null;
+let mapMarkers = {};
+let peopleMarkers = {}; // userId/socketId → marker
+let userMarker = null, userCircle = null;
+let allAlerts = [], allRates = [], allMedicines = [], allVoice = [], allSkills = [], allMarket = [];
+let nearbyUsers = [];
+let currentSection = 'home';
+let selectedReportType = 'danger';
+let selectedMarketType = 'sell';
+let medFilter = 'all', marketFilter = 'all', marketCatFilter = 'all';
+let rateChart = null;
+let userLat = null, userLng = null;
+let userLocationName = 'غير محدد';
+let geoSearchTimers = {};
+let chatOpen = false, chatRoom = null, chatUser = null;
+let myName = localStorage.getItem('nabdh_name') || '';
+let myUserId = localStorage.getItem('nabdh_uid') || generateUID();
+let myProfile = JSON.parse(localStorage.getItem('nabdh_profile') || 'null');
+let locationWatcher = null;
+let locationUpdateTimer = null;
+let heatLayer = null;
+let mapHeatVisible = false;
+let peopleLayerVisible = true;
+let peopleSearchType = 'all';
+let activeDMConversation = null;
+let dmUnreadCount = 0;
+let _sectionHistory = []; // تاريخ التنقل بين الأقسام
+let _historyPushing = false; // منع التكرار
+let userCity = ''; // اسم مدينة المستخدم
+let userState = ''; // اسم ولاية المستخدم
+let userAreaName = ''; // اسم حي/منطقة المستخدم
+
+function generateUID() {
+ const uid = 'u_' + Math.random().toString(36).substring(2, 11) + Date.now().toString(36);
+ localStorage.setItem('nabdh_uid', uid);
+ return uid;
+}
+
+/* ============================================================
+ INIT
+============================================================ */
+window.addEventListener('DOMContentLoaded', () => {
+ // ===== إجبار تحديث كاش SW القديم =====
+ if ('serviceWorker' in navigator && 'caches' in window) {
+ caches.keys().then(keys => {
+ keys.filter(k => k !== 'nabdh-v9' && k !== 'nabdh-static-v9')
+ .forEach(k => { caches.delete(k); console.log('[Cache] حُذف كاش قديم:', k); });
+ });
+ navigator.serviceWorker.getRegistrations().then(regs => {
+ regs.forEach(r => r.update());
+ });
+ }
+ // ===== إجبار إظهار التطبيق فوراً =====
+ const _appEl = document.getElementById('app');
+ if (_appEl) { _appEl.classList.remove('hidden'); _appEl.style.display = 'flex'; }
+ updateSplash('جاري التحميل...');
+ if (!myName) setTimeout(showNameModal, 2000);
+
+ // FIX: Show skeleton for homeAlerts immediately so page never looks empty
+ const _haEl = document.getElementById('homeAlerts');
+ if (_haEl) _haEl.innerHTML =
+ Array(4).fill('').join('');
+
+ // Pre-fetch alerts during splash so they show instantly on app reveal
+ fetch('/api/alerts').then(r => r.json()).then(data => {
+ allAlerts = data;
+ // Render into homeAlerts even while still in splash (hidden) so it's ready
+ renderHomeAlerts();
+ updateTicker();
+ }).catch(() => {});
+
+ // تقليل وقت الـ splash من 2.8 ثانية إلى 1.2 ثانية
+ setTimeout(() => {
+ document.getElementById('splash').classList.add('fade-out');
+ setTimeout(() => {
+ document.getElementById('splash').style.display = 'none';
+ const appEl = document.getElementById('app');
+ if (appEl) appEl.classList.remove('hidden');
+ initApp();
+ }, 400);
+ }, 1200);
+ startAutoLocation();
+ addHeroParticles();
+ loadMyProfile();
+});
+
+async function initApp() {
+ // مسح كاش localStorage القديم عند كل تشغيل جديد للتطبيق
+ try {
+ const cacheVersion = '7.4';
+ const storedVer = localStorage.getItem('_nabdh_cache_ver');
+ if (storedVer !== cacheVersion) {
+ // مسح جميع مفاتيح الكاش القديمة
+ const keysToRemove = [];
+ for (let i = 0; i < localStorage.length; i++) {
+ const k = localStorage.key(i);
+ if (k && (k.startsWith('_cache_') || k.startsWith('_api_') || k.startsWith('nabdh_'))) {
+ keysToRemove.push(k);
+ }
+ }
+ keysToRemove.forEach(k => localStorage.removeItem(k));
+ localStorage.setItem('_nabdh_cache_ver', cacheVersion);
+ }
+ } catch(e) {}
+ connectSocket();
+ // تحميل البيانات بشكل متوازٍ - لا يتوقف التطبيق إذا فشلت أي منها
+ await Promise.allSettled([loadStats(), loadAlerts(), loadExchange(), loadMedicines(), loadVoice(), loadSkills(), loadMarket()]);
+ initMap();
+ loadConversations();
+ // افتح القسم المحدد في الـ hash إن وجد
+ const hash = window.location.hash.replace('#', '');
+ const validSections = ['home','map','report','people','messages','profile','blood','power','prayer','medicine','voice','skills','exchange','market','hospitals','news','rides','weather','water','study','hood','help','polls','dashboard'];
+ if (hash && validSections.includes(hash) && hash !== 'home') {
+ goSection(hash, false);
+ }
+ setInterval(() => {
+ loadStats();
+ if (currentSection === 'exchange') loadExchange();
+ if (currentSection === 'map' || currentSection === 'home') loadNearbyAlerts();
+ if (currentSection === 'messages') loadConversations();
+ }, 30000);
+}
+
+function updateSplash(msg) {
+ const el = document.getElementById('splashMsg');
+ if (el) el.textContent = msg;
+}
+
+function addHeroParticles() {
+ const hero = document.querySelector('.hero-particles');
+ if (!hero) return;
+ for (let i = 0; i < 12; i++) {
+ const p = document.createElement('div');
+ p.className = 'hero-particle';
+ p.style.cssText = `left:${Math.random()*100}%;bottom:0;animation-duration:${4+Math.random()*6}s;animation-delay:${Math.random()*5}s;width:${1+Math.random()*3}px;height:${1+Math.random()*3}px;opacity:${.3+Math.random()*.7};`;
+ hero.appendChild(p);
+ }
+}
+
+/* ============================================================
+ GPS / LOCATION
+============================================================ */
+function startAutoLocation() {
+ if (!navigator.geolocation) { setLocationDisplay('الجهاز لا يدعم GPS'); return; }
+ setLocationDisplay('⏳ جاري التحديد...');
+ navigator.geolocation.getCurrentPosition(
+ pos => handleLocationUpdate(pos, true),
+ () => {
+ setLocationDisplay('📍 تعذّر التحديد التلقائي');
+ setTimeout(() => navigator.geolocation.getCurrentPosition(
+ pos => handleLocationUpdate(pos, true),
+ () => {},
+ { enableHighAccuracy: false, timeout: 12000, maximumAge: 300000 }
+ ), 5000);
+ },
+ { enableHighAccuracy: true, timeout: 12000, maximumAge: 60000 }
+ );
+ if (locationWatcher !== null) navigator.geolocation.clearWatch(locationWatcher);
+ locationWatcher = navigator.geolocation.watchPosition(
+ pos => handleLocationUpdate(pos, false),
+ () => {},
+ { enableHighAccuracy: true, timeout: 20000, maximumAge: 30000 }
+ );
+ if (locationUpdateTimer) clearInterval(locationUpdateTimer);
+ locationUpdateTimer = setInterval(() => {
+ navigator.geolocation.getCurrentPosition(
+ pos => handleLocationUpdate(pos, false),
+ () => {},
+ { enableHighAccuracy: true, timeout: 12000, maximumAge: 30000 }
+ );
+ }, 60000);
+}
+
+async function handleLocationUpdate(pos, showMsg) {
+ const newLat = pos.coords.latitude;
+ const newLng = pos.coords.longitude;
+ if (userLat && userLng && !showMsg) {
+ const moved = haversine(userLat, userLng, newLat, newLng) * 1000;
+ if (moved < 50) return;
+ }
+ userLat = newLat;
+ userLng = newLng;
+ const name = await reverseGeocode(userLat, userLng);
+ userLocationName = name;
+ setLocationDisplay('📍 ' + name);
+ updateSplash('تم تحديد موقعك ✓');
+ if (map) updateUserMapMarker();
+ if (socket) {
+ const showOnMap = myProfile ? (myProfile.showOnMap !== false) : true;
+ socket.emit('user_location', {
+ lat: userLat, lng: userLng,
+ name: myName || 'مستخدم',
+ area: name,
+ userId: myUserId,
+ showOnMap,
+ avatar: myProfile?.avatar || '',
+ phone: myProfile?.phone || '',
+ });
+ }
+ autoFillAllLocationFields(name, userLat, userLng);
+ loadNearbyAlerts();
+ loadNearbyUsers();
+ loadNearbyPeople();
+ if (showMsg) showToast('✅ موقعك: ' + name, 'success');
+}
+
+function autoFillAllLocationFields(name, lat, lng) {
+ const fields = [
+ { inp:'exSourceInput', lat:'exSourceLat', lng:'exSourceLng' },
+ { inp:'medAreaInput', lat:'medLat', lng:'medLng' },
+ { inp:'voiceAreaInput', lat:'voiceLat', lng:'voiceLng' },
+ { inp:'skillAreaInput', lat:'skillLat', lng:'skillLng' },
+ { inp:'mAreaInput', lat:'mLat', lng:'mLng' },
+ { inp:'reportAreaInput', lat:'reportLat', lng:'reportLng' },
+ ];
+ for (const f of fields) {
+ const i = document.getElementById(f.inp);
+ const la = document.getElementById(f.lat);
+ const lo = document.getElementById(f.lng);
+ if (i && !i.value) i.value = name;
+ if (la && !la.value) la.value = lat;
+ if (lo && !lo.value) lo.value = lng;
+ }
+ ['exLocStatus','medLocStatus','voiceLocStatus','skillLocStatus','mLocStatus'].forEach(id => {
+ const el = document.getElementById(id);
+ if (el && (el.textContent.includes('اضغط') || el.textContent.includes('أو') || el.textContent.includes('تلقائياً'))) el.textContent = '✅ ' + name;
+ });
+ const rt = document.getElementById('reportLocText');
+ if (rt) rt.textContent = '✅ ' + name;
+ const rd = document.getElementById('reportLocDot');
+ if (rd) rd.className = 'loc-dot loc-ok';
+}
+
+function requestLocation(silent = false) {
+ if (!navigator.geolocation) { setLocationDisplay('الجهاز لا يدعم GPS'); return; }
+ setLocationDisplay('⏳ جاري التحديد...');
+ navigator.geolocation.getCurrentPosition(
+ pos => handleLocationUpdate(pos, !silent),
+ () => {
+ setLocationDisplay('📍 الموقع غير متاح');
+ if (!silent) showToast('⚠️ يمكنك إدخال موقعك يدوياً', 'error');
+ },
+ { enableHighAccuracy: true, timeout: 12000, maximumAge: 30000 }
+ );
+}
+
+async function reverseGeocode(lat, lng) {
+ try {
+ const r = await fetch(`https://nominatim.openstreetmap.org/reverse?format=json&lat=${lat}&lon=${lng}&accept-language=ar`);
+ const d = await r.json();
+ const a = d.address || {};
+ return a.suburb || a.quarter || a.city_district || a.neighbourhood ||
+ a.city || a.town || a.village || a.county || a.state || a.country ||
+ lat.toFixed(3) + ',' + lng.toFixed(3);
+ } catch {
+ return lat.toFixed(3) + ',' + lng.toFixed(3);
+ }
+}
+
+function setLocationDisplay(txt) {
+ const t = txt.replace('📍 ', '');
+ const el1 = document.getElementById('locationBarText');
+ const el2 = document.getElementById('menuLocText');
+ const el3 = document.getElementById('heroBadge');
+ if (el1) el1.textContent = txt;
+ if (el2) el2.textContent = t;
+ if (el3) el3.textContent = '📍 ' + t + ' • مباشر';
+}
+
+async function attachGPS(inputId, latId, lngId, statusId, dotId) {
+ const statusEl = document.getElementById(statusId);
+ const dotEl = dotId ? document.getElementById(dotId) : null;
+ if (statusEl) statusEl.textContent = '⏳ جاري التحديد...';
+ if (dotEl) dotEl.className = 'loc-dot loc-loading';
+ if (userLat && userLng) {
+ fillGPSFields(inputId, latId, lngId, statusId, dotId, userLat, userLng, userLocationName);
+ return;
+ }
+ if (!navigator.geolocation) { if (statusEl) statusEl.textContent = '❌ GPS غير متاح'; return; }
+ navigator.geolocation.getCurrentPosition(
+ async pos => {
+ userLat = pos.coords.latitude; userLng = pos.coords.longitude;
+ const name = await reverseGeocode(userLat, userLng);
+ userLocationName = name;
+ fillGPSFields(inputId, latId, lngId, statusId, dotId, userLat, userLng, name);
+ setLocationDisplay('📍 ' + name);
+ },
+ () => {
+ if (statusEl) statusEl.textContent = '❌ تعذّر تحديد الموقع';
+ if (dotEl) dotEl.className = 'loc-dot loc-err';
+ },
+ { enableHighAccuracy: true, timeout: 8000 }
+ );
+}
+
+function fillGPSFields(inputId, latId, lngId, statusId, dotId, lat, lng, name) {
+ const inp = document.getElementById(inputId);
+ const la = document.getElementById(latId);
+ const lo = document.getElementById(lngId);
+ const st = document.getElementById(statusId);
+ const dt = dotId ? document.getElementById(dotId) : null;
+ if (inp) inp.value = name;
+ if (la) la.value = lat;
+ if (lo) lo.value = lng;
+ if (st) st.textContent = '✅ ' + name;
+ if (dt) dt.className = 'loc-dot loc-ok';
+}
+
+async function loadNearbyAlerts() {
+ if (!userLat) return;
+ try {
+ const data = await fetch('/api/alerts/nearby?lat=' + userLat + '&lng=' + userLng + '&km=100').then(r => r.json());
+ allAlerts = data;
+ renderHomeAlerts(); renderMapAlerts(); updateMapCounts(); updateTicker();
+ } catch {}
+}
+
+async function loadNearbyUsers() {
+ if (!userLat) return;
+ try {
+ nearbyUsers = await fetch('/api/users/nearby?lat=' + userLat + '&lng=' + userLng + '&km=100').then(r => r.json());
+ renderNearbyUsers();
+ } catch {}
+}
+
+async function loadNearbyPeople() {
+ if (!userLat) return;
+ try {
+ const people = await fetch('/api/people/map').then(r => r.json());
+ renderNearbyPeopleList(people);
+ if (map && peopleLayerVisible) refreshPeopleMarkers(people);
+ const el = document.getElementById('cnt-people');
+ if (el) el.textContent = people.length;
+ } catch {}
+}
+
+/* ============================================================
+ NAME MODAL
+============================================================ */
+function showNameModal() {
+ const el = document.getElementById('nameModal');
+ if (el && !myName) el.classList.remove('hidden');
+}
+function saveName() {
+ const inp = document.getElementById('nameInput');
+ const n = (inp ? inp.value : '').trim();
+ if (!n) return showToast('❌ أدخل اسمك أو لقباً', 'error');
+ myName = n;
+ localStorage.setItem('nabdh_name', n);
+ document.getElementById('nameModal').classList.add('hidden');
+ showToast('✅ مرحباً ' + n + '! أنت الآن جزء من نبض 💓', 'success');
+ if (socket && userLat) socket.emit('user_location', { lat: userLat, lng: userLng, name: myName, area: userLocationName, userId: myUserId });
+ // Auto-init profile with name
+ syncProfileWithServer({ name: n });
+ updateProfileUI();
+}
+function skipName() {
+ myName = 'مستخدم';
+ const el = document.getElementById('nameModal');
+ if (el) el.classList.add('hidden');
+}
+
+/* ============================================================
+ GEO SEARCH
+============================================================ */
+function searchGeoInline(inputId, dropId) {
+ const q = document.getElementById(inputId).value.trim();
+ const drop = document.getElementById(dropId);
+ clearTimeout(geoSearchTimers[inputId]);
+ if (!q || q.length < 1) { drop.classList.add('hidden'); return; }
+ geoSearchTimers[inputId] = setTimeout(async () => {
+ try {
+ const res = await fetch('/api/geo/search?q=' + encodeURIComponent(q)).then(r => r.json());
+ if (!res.length) { drop.classList.add('hidden'); return; }
+ drop.innerHTML = res.map(r =>
+ '' + (r.label || r.name) + '
'
+ ).join('');
+ drop.classList.remove('hidden');
+ } catch {}
+ }, 280);
+}
+
+function selectGeoResult(inputId, dropId, lat, lng, name) {
+ const inp = document.getElementById(inputId);
+ if (inp) inp.value = name.replace(/^[🇸🇩🏙️🏘️🌍]+\s?/u, '');
+ document.getElementById(dropId) && document.getElementById(dropId).classList.add('hidden');
+ const base = inputId.replace('Input', '');
+ const la = document.getElementById(base + 'Lat');
+ const lo = document.getElementById(base + 'Lng');
+ if (la) la.value = lat;
+ if (lo) lo.value = lng;
+ if (map && inputId === 'mapSearchInput') {
+ map.setView([lat, lng], 14, { animate: true });
+ const msr = document.getElementById('mapSearchResults');
+ if (msr) msr.classList.add('hidden');
+ }
+}
+
+function searchGeo() { searchGeoInline('mapSearchInput', 'mapSearchResults'); }
+
+document.addEventListener('click', e => {
+ if (!e.target.closest('.geo-picker-wrap') && !e.target.closest('.map-search-wrap')) {
+ document.querySelectorAll('.geo-dropdown, .map-search-results').forEach(d => d.classList.add('hidden'));
+ }
+});
+
+/* ============================================================
+ SOCKET.IO
+============================================================ */
+function connectSocket() {
+ try {
+ socket = io({ timeout: 8000, reconnectionAttempts: 5, transports: ['websocket', 'polling'] });
+ } catch(e) { return; }
+ socket.on('connect', () => {
+ const offEl = document.getElementById('offlineIndicator');
+ if (offEl) offEl.classList.add('hidden');
+ });
+ socket.on('connect_error', () => { /* الاتصال اختياري - لا يوقف التطبيق */ });
+ socket.on('connect_timeout', () => { /* لا يوقف التطبيق */ });
+ socket.on('stats_update', s => updateStats(s));
+ socket.on('new_alert', alert => onNewAlert(alert));
+ socket.on('new_rate', rate => { allRates.unshift(rate); renderExchange(); });
+ socket.on('new_medicine', med => { allMedicines.unshift(med); renderMedicines(); });
+ socket.on('new_voice', item => { allVoice.unshift(item); renderVoice(); });
+ socket.on('new_skill', skill => { allSkills.unshift(skill); renderSkills(); });
+ socket.on('new_market_item', item => {
+ allMarket.unshift(item); renderMarket(); renderHomeMarket();
+ showNotif('🛒 إعلان جديد: ' + item.title);
+ });
+ socket.on('vote_update', ({ id, votes }) => {
+ const a = allAlerts.find(x => x.id === id); if (a) a.votes = votes;
+ document.querySelectorAll('[data-vote-id="' + id + '"]').forEach(el => el.textContent = '👍 ' + votes);
+ });
+ socket.on('market_like', ({ id, likes }) => {
+ const m = allMarket.find(x => x.id === id); if (m) m.likes = likes;
+ document.querySelectorAll('[data-like-id="' + id + '"]').forEach(el => el.textContent = '❤️ ' + likes);
+ });
+ socket.on('nearby_users', users => { nearbyUsers = users; renderNearbyUsers(); });
+ socket.on('chat_msg', ({ room, msg }) => { if (chatOpen && chatRoom === room) appendChatMsg(msg, false); });
+ socket.on('p2p_msg', msg => showNotif('💬 رسالة من ' + (msg.senderName || 'مستخدم') + ': ' + msg.text));
+ socket.on('sos_alert', sos => {
+ showNotif('🆘 نداء استغاثة من ' + sos.area);
+ if (map && sos.lat && sos.lng) addSOSPin(sos);
+ });
+ socket.on('people_map_update', () => { loadNearbyPeople(); });
+ // Direct Messages
+ socket.on('dm_msg', ({ conversationId, msg, from }) => {
+ dmUnreadCount++;
+ updateDMBadge();
+ var preview = msg.mediaType ? ('📎 ' + { image:'صورة', video:'فيديو', audio:'صوت', file:'ملف' }[msg.mediaType] || 'مرفق') : (msg.text || '').substring(0, 40);
+ showNotif('💬 رسالة من ' + (msg.senderName || 'مستخدم') + ': ' + preview);
+ if (activeDMConversation === conversationId) {
+ appendDMMessage(msg, false);
+ }
+ // Also update dmChatMessages page if open
+ var dmPage = document.getElementById('dmChatPage');
+ if (dmPage && !dmPage.classList.contains('hidden') && dmCurrentUser && dmCurrentUser.id === from) {
+ var container = document.getElementById('dmChatMessages');
+ if (container) {
+ var emptyEl = container.querySelector('.gp-empty-chat');
+ if (emptyEl) container.innerHTML = '';
+ var msgEl = document.createElement('div');
+ msgEl.className = 'gp-msg gpm-other';
+ var mediaHtml = '';
+ if (msg.mediaType === 'image' && msg.mediaData) {
+ mediaHtml = '';
+ } else if (msg.mediaType === 'video' && msg.mediaData) {
+ mediaHtml = '
';
+ } else if (msg.mediaType === 'audio' && msg.mediaData) {
+ mediaHtml = '';
+ } else if (msg.mediaType === 'file' && msg.mediaData) {
+ mediaHtml = '';
+ }
+ msgEl.innerHTML = '' + escHtml(msg.senderName || 'عضو') + '
' + mediaHtml + (msg.text ? '' + escHtml(msg.text) + '
' : '') + '';
+ container.appendChild(msgEl);
+ container.scrollTop = container.scrollHeight;
+ }
+ }
+ if (currentSection === 'messages') loadConversations();
+ });
+ socket.on('dm_sent', ({ conversationId, msg }) => {
+ if (activeDMConversation === conversationId) appendDMMessage(msg, true);
+ // Also update dmChatMessages page for the sender
+ var dmPage = document.getElementById('dmChatPage');
+ if (dmPage && !dmPage.classList.contains('hidden')) {
+ var container = document.getElementById('dmChatMessages');
+ if (container) {
+ var emptyEl = container.querySelector('.gp-empty-chat');
+ if (emptyEl) container.innerHTML = '';
+ var msgEl = document.createElement('div');
+ msgEl.className = 'gp-msg gpm-mine';
+ var mediaHtml = '';
+ if (msg.mediaType === 'image' && msg.mediaData) {
+ mediaHtml = '';
+ } else if (msg.mediaType === 'video' && msg.mediaData) {
+ mediaHtml = '
';
+ } else if (msg.mediaType === 'audio' && msg.mediaData) {
+ mediaHtml = '';
+ } else if (msg.mediaType === 'file' && msg.mediaData) {
+ mediaHtml = '';
+ }
+ if (mediaHtml) {
+ msgEl.innerHTML = mediaHtml + (msg.text ? '' + escHtml(msg.text) + '
' : '') + '';
+ container.appendChild(msgEl);
+ container.scrollTop = container.scrollHeight;
+ }
+ }
+ }
+ });
+}
+
+function onNewAlert(alert) {
+ allAlerts.unshift(alert);
+ renderHomeAlerts(); renderMapAlerts(); addMapPin(alert); updateTicker(); updateMapCounts();
+ showNotif(alert.icon + ' تنبيه جديد: ' + alert.area);
+ sendBrowserNotif('نبض: ' + alert.icon + ' ' + alert.msg, alert.area);
+}
+
+/* ============================================================
+ BROWSER NOTIFICATIONS
+============================================================ */
+function requestNotifPermission() {
+ if ('Notification' in window && Notification.permission === 'default') Notification.requestPermission();
+}
+function sendBrowserNotif(title, body) {
+ if ('Notification' in window && Notification.permission === 'granted') {
+ try { new Notification(title, { body, icon: '/favicon.svg' }); } catch {}
+ }
+}
+
+/* ============================================================
+ STATS
+============================================================ */
+async function loadStats() {
+ try { updateStats(await fetch('/api/stats').then(r => r.json())); } catch {}
+}
+function updateStats(s) {
+ // top bar
+ animateCount('liveUsers', s.online || s.users || 0);
+ animateCount('liveReports', s.reports || s.total_alerts || 0);
+ // hero block
+ animateCount('hUsers', s.online || s.users || 0);
+ animateCount('hReports',s.reports || s.total_alerts || 0);
+ animateCount('hLives', s.lives_saved || 0);
+ animateCount('hCities', s.cities || 0);
+ // dashboard extras if available
+ if (s.market_items !== undefined) animateCount('vmb-market', s.market_items);
+ if (s.blood_donors !== undefined) animateCount('vmb-blood', s.blood_donors);
+}
+
+/* ============================================================
+ ALERTS
+============================================================ */
+async function loadAlerts() {
+ try { allAlerts = await fetch('/api/alerts').then(r => r.json()); } catch { allAlerts = []; }
+ renderHomeAlerts(); updateTicker();
+}
+function renderHomeAlerts() {
+ const el = document.getElementById('homeAlerts');
+ if (!el) return;
+ const list = userLat ? [...allAlerts].sort((a, b) => (dist(a) || 9999) - (dist(b) || 9999)).slice(0, 6) : allAlerts.slice(0, 6);
+ el.innerHTML = list.length ? list.map(a => alertCard(a)).join('') : emptyState('🔕', 'لا توجد تنبيهات بعد', 'كن أول من يُبلّغ عن حدث في منطقتك', 'report');
+}
+function alertCard(a) {
+ const d = dist(a);
+ const distTxt = d !== null && d < 500 ? '📡 ' + (d < 1 ? '<1 كم' : Math.round(d) + ' كم') + ' ' : '';
+ const imgHtml = a.imageId ? ' ' : '';
+ return '' +
+ '
' + a.icon + '
' +
+ '
' +
+ '
' + escHtml(a.msg) + '
' +
+ imgHtml +
+ '
' +
+ '📍 ' + escHtml(a.area) + ' ' + distTxt +
+ '🕐 ' + timeAgo(a.time) + ' ' +
+ '👍 ' + a.votes + ' ' +
+ '🔗 ' +
+ '
';
+}
+function updateTicker() {
+ const el = document.getElementById('alertTicker');
+ if (!el) return;
+ const txt = allAlerts.length
+ ? allAlerts.slice(0, 10).map(a => a.icon + ' ' + a.msg + ' • ').join('') + allAlerts.slice(0, 10).map(a => a.icon + ' ' + a.msg + ' • ').join('')
+ : 'مرحباً بك في نبض - ابدأ بمشاركة أول تقرير من منطقتك! 💓 ';
+ el.textContent = txt;
+}
+async function vote(id) {
+ try { await fetch('/api/alerts/' + id + '/vote', { method: 'POST' }); } catch {}
+}
+function shareItem(msg, area) {
+ const text = '🚨 من تطبيق نبض:\n' + msg + '\n📍 ' + area + '\n\n#نبض_المدينة';
+ if (navigator.share) navigator.share({ title: 'تنبيه نبض', text }).catch(() => {});
+ else navigator.clipboard && navigator.clipboard.writeText(text).then(() => showToast('✅ تم النسخ', 'success'));
+}
+
+/* ============================================================
+ SOS
+============================================================ */
+let sosTimeout = null;
+function triggerSOS() {
+ if (!userLat) { showToast('❌ الموقع غير محدد', 'error'); return; }
+ ['sosBtn','sosHomeBtn','sosBtn2'].forEach(id => {
+ const b = document.getElementById(id);
+ if (b) b.classList.add('sos-pressing');
+ });
+ sosTimeout = setTimeout(async () => {
+ try {
+ await fetch('/api/sos', { method: 'POST', headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ lat: userLat, lng: userLng, name: myName || 'مستخدم', area: userLocationName }) });
+ showToast('🆘 تم إرسال نداء الاستغاثة لمن حولك!', 'success');
+ } catch { showToast('❌ خطأ في الإرسال', 'error'); }
+ cancelSOS();
+ }, 2000);
+}
+function cancelSOS() {
+ if (sosTimeout) { clearTimeout(sosTimeout); sosTimeout = null; }
+ ['sosBtn','sosHomeBtn','sosBtn2'].forEach(id => {
+ const b = document.getElementById(id);
+ if (b) b.classList.remove('sos-pressing');
+ });
+}
+function addSOSPin(sos) {
+ if (!map) return;
+ const icon = L.divIcon({ className: 'custom-marker', html: '🆘
', iconSize: [40, 40], iconAnchor: [20, 40] });
+ L.marker([sos.lat, sos.lng], { icon }).addTo(map)
+ .bindPopup('', { className: 'custom-popup' });
+}
+function openSosModal() {
+ const m = document.getElementById('sosModal');
+ if (m) {
+ m.classList.remove('hidden');
+ history.pushState({ section: currentSection, modal: 'sos' }, '', '#' + currentSection);
+ }
+}
+function closeSosModal() {
+ const m = document.getElementById('sosModal');
+ if (m) m.classList.add('hidden');
+}
+function sendSOSAlert() {
+ triggerSOS();
+}
+function shareSOSLocation() {
+ const lat = userLat || 15.5007, lng = userLng || 32.5599;
+ const name = myName || 'مستخدم نبض';
+ const area = userLocationName || 'غير محدد';
+ const text = '🆘 أحتاج مساعدة!\n👤 ' + name + '\n📍 ' + area + '\n🗺️ https://maps.google.com/?q=' + lat + ',' + lng + '\n💓 تطبيق نبض';
+ if (navigator.share) {
+ navigator.share({ title: '🆘 نداء استغاثة', text }).catch(() => {});
+ } else if (navigator.clipboard) {
+ navigator.clipboard.writeText(text).then(() => showToast('✅ تم نسخ معلومات الموقع', 'success'));
+ }
+}
+
+/* ============================================================
+ MAP
+============================================================ */
+function initMap() {
+ if (map) return;
+ const center = userLat ? [userLat, userLng] : [15.5007, 32.5599];
+ const zoom = userLat ? 13 : 6;
+ map = L.map('map', { zoomControl: false, attributionControl: false }).setView(center, zoom);
+ L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png', { maxZoom: 19, subdomains: 'abcd' }).addTo(map);
+ L.control.zoom({ position: 'bottomleft' }).addTo(map);
+ fetch('/api/map').then(r => r.json()).then(pins => {
+ pins.forEach(p => addMapPin(p)); renderMapAlerts(); updateMapCounts();
+ }).catch(() => {});
+ addSudanStatesLayer();
+ if (userLat) updateUserMapMarker();
+ map.on('click', e => {
+ if (currentSection === 'report') {
+ document.getElementById('reportLat').value = e.latlng.lat.toFixed(6);
+ document.getElementById('reportLng').value = e.latlng.lng.toFixed(6);
+ reverseGeocode(e.latlng.lat, e.latlng.lng).then(name => {
+ const inp = document.getElementById('reportAreaInput');
+ if (inp) inp.value = name;
+ showToast('📍 تم تحديد الموقع: ' + name, 'success');
+ });
+ }
+ });
+ loadHeatmapData();
+ // Load people on map
+ loadNearbyPeople();
+}
+
+async function loadHeatmapData() {
+ try {
+ const pts = await fetch('/api/heatmap').then(r => r.json());
+ if (!pts.length) return;
+ heatLayer = L.layerGroup();
+ pts.forEach(p => {
+ const c = L.circleMarker([p.lat, p.lng], {
+ radius: Math.min(4 + (p.weight || 1), 12),
+ color: p.type === 'danger' ? 'rgba(231,76,60,.5)' : p.type === 'warning' ? 'rgba(241,196,15,.4)' : 'rgba(26,188,156,.3)',
+ fillColor: p.type === 'danger' ? 'rgba(231,76,60,.2)' : p.type === 'warning' ? 'rgba(241,196,15,.15)' : 'rgba(26,188,156,.1)',
+ fillOpacity: 1, weight: 1
+ });
+ heatLayer.addLayer(c);
+ });
+ } catch {}
+}
+
+function toggleHeatmap() {
+ if (!map) return;
+ if (!heatLayer) return showToast('ℹ️ لا توجد بيانات كافية للخريطة الحرارية', 'success');
+ mapHeatVisible = !mapHeatVisible;
+ if (mapHeatVisible) { heatLayer.addTo(map); showToast('🌡️ خريطة الكثافة مفعّلة', 'success'); }
+ else { heatLayer.remove(); showToast('خريطة الكثافة مُخفاة', 'success'); }
+}
+
+function addSudanStatesLayer() {
+ fetch('/api/geo/sudan').then(r => r.json()).then(states => {
+ states.forEach(st => {
+ const circle = L.circleMarker([st.lat, st.lng], {
+ radius: 8, color: 'rgba(26,188,156,0.6)', fillColor: 'rgba(26,188,156,0.12)', fillOpacity: 1, weight: 1.5
+ }).addTo(map);
+ circle.bindTooltip('🇸🇩 ' + st.state, { permanent: false, direction: 'top', className: 'custom-popup', opacity: .9 });
+ circle.on('click', () => {
+ map.setView([st.lat, st.lng], 11, { animate: true });
+ const filtered = allAlerts.filter(a => a.area && a.area.includes(st.state));
+ const el = document.getElementById('mapAlertsList');
+ const title = document.getElementById('mapListTitle');
+ if (title) title.textContent = '🇸🇩 تنبيهات ' + st.state + ' (' + filtered.length + ')';
+ if (el) el.innerHTML = filtered.length ? filtered.map(a => alertCard(a)).join('') : emptyState('🗺️', 'لا توجد تنبيهات في ' + st.state, 'كن أول من يُبلّغ!', 'report');
+ });
+ });
+ }).catch(() => {});
+}
+
+function addMapPin(pin) {
+ if (!map || !pin.lat || !pin.lng) return;
+ if (mapMarkers[pin.id]) mapMarkers[pin.id].remove();
+ const icons = { danger: '🔴', warning: '🟡', info: '🟢' };
+ const sizes = { danger: 36, warning: 30, info: 28 };
+ const sz = sizes[pin.type] || 30;
+ const icon = L.divIcon({
+ className: 'custom-marker',
+ html: '' + (icons[pin.type] || '🟡') + '
',
+ iconSize: [sz, sz], iconAnchor: [sz / 2, sz / 2]
+ });
+ const d = dist(pin);
+ const marker = L.marker([pin.lat, pin.lng], { icon }).addTo(map).bindPopup(
+ '' +
+ '' +
+ '' +
+ (d !== null ? '' : '') +
+ '🔗 شارك ',
+ { className: 'custom-popup', maxWidth: 220 }
+ );
+ mapMarkers[pin.id] = marker;
+}
+
+// رسم أشخاص على الخريطة
+function refreshPeopleMarkers(people) {
+ if (!map) return;
+ // Remove stale markers
+ const newIds = new Set(people.map(p => p.socketId || p.userId));
+ Object.entries(peopleMarkers).forEach(([id, m]) => {
+ if (!newIds.has(id) || id === myUserId) { m.remove(); delete peopleMarkers[id]; }
+ });
+ people.forEach(p => {
+ if (!p.lat || !p.lng) return;
+ const pid = p.socketId || p.userId || p.name;
+ if (pid === myUserId) return; // don't double-render self
+ if (peopleMarkers[pid]) { peopleMarkers[pid].setLatLng([p.lat, p.lng]); return; }
+ const avatarText = p.avatar ? p.avatar : (p.name ? p.name.substring(0, 1).toUpperCase() : '👤');
+ const icon = L.divIcon({
+ className: 'custom-marker',
+ html: '' + avatarText + '
',
+ iconSize: [36, 36], iconAnchor: [18, 36]
+ });
+ const d = p.lat && userLat ? haversine(userLat, userLng, p.lat, p.lng) : null;
+ const marker = L.marker([p.lat, p.lng], { icon }).addTo(map).bindPopup(
+ '',
+ { className: 'custom-popup', maxWidth: 200 }
+ );
+ peopleMarkers[pid] = marker;
+ });
+}
+
+function updateUserMapMarker() {
+ if (!map || !userLat) return;
+ if (userMarker) { userMarker.remove(); userMarker = null; }
+ if (userCircle) { userCircle.remove(); userCircle = null; }
+ const icon = L.divIcon({
+ className: 'custom-marker',
+ html: '📍
',
+ iconSize: [38, 38], iconAnchor: [19, 38]
+ });
+ userMarker = L.marker([userLat, userLng], { icon, zIndexOffset: 1000 }).addTo(map)
+ .bindPopup('✅ يتجدد تلقائياً
', { className: 'custom-popup' });
+ userCircle = L.circle([userLat, userLng], { radius: 5000, color: 'rgba(26,188,156,.7)', fillColor: 'rgba(26,188,156,.05)', fillOpacity: 1, weight: 1.5, dashArray: '6,4' }).addTo(map);
+}
+
+function locateOnMap() {
+ const btn = document.getElementById('locateBtnIcon');
+ if (btn) { btn.textContent = '⏳'; btn.classList.add('spin'); }
+ navigator.geolocation.getCurrentPosition(
+ async pos => {
+ await handleLocationUpdate(pos, false);
+ if (map && userLat) map.setView([userLat, userLng], 14, { animate: true });
+ if (btn) { btn.textContent = '🎯'; btn.classList.remove('spin'); }
+ },
+ () => { if (btn) { btn.textContent = '🎯'; btn.classList.remove('spin'); } },
+ { enableHighAccuracy: true, timeout: 10000 }
+ );
+}
+
+function filterMap(type, btn) {
+ document.querySelectorAll('.map-filters .filt').forEach(b => b.classList.remove('active-filt'));
+ if (btn) btn.classList.add('active-filt');
+
+ if (type === 'people') {
+ // Show only people markers, hide alert markers
+ Object.values(mapMarkers).forEach(m => m.remove ? m.remove() : null);
+ loadNearbyPeople();
+ document.getElementById('mapListTitle').textContent = '👥 الأشخاص على الخريطة';
+ document.getElementById('mapAlertsList').innerHTML = emptyState('👥', 'يظهر الأشخاص النشطون على الخريطة', 'اضغط على أي مستخدم للتواصل معه');
+ return;
+ }
+
+ // Re-show alert markers
+ Object.values(mapMarkers).forEach(m => m.addTo && m.addTo(map));
+
+ if (type !== 'all' && type !== 'nearby') allAlerts.forEach(a => { if (mapMarkers[a.id] && a.type !== type) mapMarkers[a.id].remove(); });
+ if (type === 'nearby') {
+ if (!userLat) { showToast('⚠️ الموقع غير محدد', 'error'); return; }
+ allAlerts.forEach(a => { if (mapMarkers[a.id] && (!a.lat || !a.lng || haversine(userLat, userLng, a.lat, a.lng) > 50)) mapMarkers[a.id].remove(); });
+ map.setView([userLat, userLng], 13, { animate: true });
+ }
+ renderMapAlerts(type);
+}
+
+function renderMapAlerts(filter) {
+ filter = filter || 'all';
+ const el = document.getElementById('mapAlertsList');
+ const title = document.getElementById('mapListTitle');
+ if (!el) return;
+ let list = allAlerts;
+ if (filter === 'nearby') {
+ if (!userLat) return;
+ list = allAlerts.filter(a => a.lat && a.lng && haversine(userLat, userLng, a.lat, a.lng) <= 50);
+ if (title) title.textContent = '📡 تنبيهات قريبة (' + list.length + ')';
+ } else if (filter !== 'all') {
+ list = allAlerts.filter(a => a.type === filter);
+ const labels = { danger: '🔴 خطر', warning: '🟡 تحذير', info: '🟢 معلومة' };
+ if (title) title.textContent = '📋 ' + (labels[filter] || filter) + ' (' + list.length + ')';
+ } else {
+ if (title) title.textContent = '📋 جميع التنبيهات (' + list.length + ')';
+ }
+ el.innerHTML = list.length ? list.map(a => alertCard(a)).join('') : emptyState('🗺️', 'لا توجد تنبيهات', 'أرسل تقريرك الأول وسيظهر على الخريطة فوراً', 'report');
+}
+
+function updateMapCounts() {
+ const g = id => document.getElementById(id);
+ if (g('cnt-danger')) g('cnt-danger').textContent = allAlerts.filter(a => a.type === 'danger').length;
+ if (g('cnt-warning')) g('cnt-warning').textContent = allAlerts.filter(a => a.type === 'warning').length;
+ if (g('cnt-info')) g('cnt-info').textContent = allAlerts.filter(a => a.type === 'info').length;
+ if (g('cnt-total')) g('cnt-total').textContent = allAlerts.length;
+}
+
+function sortAlerts(by) {
+ if (by === 'votes') allAlerts.sort((a, b) => b.votes - a.votes);
+ if (by === 'time') allAlerts.sort((a, b) => b.time - a.time);
+ if (by === 'nearby' && userLat) allAlerts.sort((a, b) => (dist(a) || 9999) - (dist(b) || 9999));
+ renderMapAlerts();
+}
+
+function showOnMap(lat, lng, title) {
+ goSection('map');
+ setTimeout(() => {
+ if (map) {
+ map.setView([lat, lng], 16, { animate: true });
+ L.popup({ className: 'custom-popup' }).setLatLng([lat, lng]).setContent('').openOn(map);
+ }
+ }, 300);
+}
+
+/* ============================================================
+ EXCHANGE
+============================================================ */
+async function loadExchange() {
+ try { allRates = await fetch('/api/exchange').then(r => r.json()); } catch { allRates = []; }
+ renderExchange();
+}
+function renderExchange() {
+ const liveRateEl = document.getElementById('liveRate');
+ if (liveRateEl) liveRateEl.textContent = allRates.length ? allRates[0].rate : '---';
+ if (!allRates.length) {
+ setEl('exRateMain', '---'); setEl('exUpdated', 'لم يُبلَّغ بعد');
+ setEl('homeRateNum', '---'); setEl('homeRateChange', 'لم يُبلَّغ عن أي سعر بعد');
+ ['exHigh', 'exLow', 'exAvg'].forEach(id => setEl(id, '-'));
+ const rl = document.getElementById('ratesList');
+ if (rl) rl.innerHTML = emptyState('💵', 'لا توجد أسعار بعد', 'شارك سعر الصرف من منطقتك');
+ renderRateChart([]); return;
+ }
+ const latest = allRates[0];
+ const today = allRates.filter(r => Date.now() - r.time < 86400000);
+ const nums = today.map(r => r.rate);
+ const hi = Math.max(...nums), lo = Math.min(...nums), avg = Math.round(nums.reduce((a, b) => a + b, 0) / nums.length);
+ setEl('exRateMain', latest.rate); setEl('exUpdated', 'آخر تحديث: ' + timeAgo(latest.time) + ' • ' + latest.source);
+ setEl('homeRateNum', latest.rate); setEl('homeRateChange', '📍 ' + latest.source + ' • ' + timeAgo(latest.time));
+ setEl('exHigh', hi); setEl('exLow', lo); setEl('exAvg', avg);
+ const rl = document.getElementById('ratesList');
+ if (rl) rl.innerHTML = allRates.slice(0, 25).map(r =>
+ '' + r.rate + ' ج.س / $
' +
+ '
📍 ' + escHtml(r.source) + ' • 🕐 ' + timeAgo(r.time) + '
' +
+ '
' + (r.verified ? '✅ موثق ' : '⏳ قيد التحقق ') +
+ ' 🔗
'
+ ).join('');
+ renderRateChart(today.slice().reverse());
+}
+function renderRateChart(rates) {
+ const cv = document.getElementById('rateChart');
+ if (!cv) return;
+ if (rateChart) { rateChart.destroy(); rateChart = null; }
+ const noMsg = document.getElementById('noChartMsg');
+ if (!rates.length) {
+ cv.style.display = 'none';
+ if (!noMsg) { const p = document.createElement('p'); p.id = 'noChartMsg'; p.style.cssText = 'text-align:center;color:var(--text2);padding:2rem;font-size:.85rem'; p.textContent = '📈 الرسم سيظهر بعد أول سعر مُبلَّغ عنه'; cv.parentNode.appendChild(p); }
+ return;
+ }
+ cv.style.display = '';
+ if (noMsg) noMsg.remove();
+ const recent = rates.slice(-30);
+ const labels = recent.map(r => { const d = new Date(r.time); return d.getHours() + ':' + String(d.getMinutes()).padStart(2, '0'); });
+ rateChart = new Chart(cv.getContext('2d'), {
+ type: 'line',
+ data: { labels, datasets: [{ data: recent.map(r => r.rate), borderColor: '#f1c40f', backgroundColor: 'rgba(241,196,15,.08)', fill: true, tension: .4, pointBackgroundColor: '#f1c40f', pointRadius: 3, borderWidth: 2 }] },
+ options: { responsive: true, animation: { duration: 600 }, plugins: { legend: { display: false } },
+ scales: { x: { ticks: { color: '#8892a4', font: { family: 'Tajawal', size: 10 } }, grid: { color: 'rgba(255,255,255,.04)' } }, y: { ticks: { color: '#8892a4', font: { family: 'Tajawal', size: 10 } }, grid: { color: 'rgba(255,255,255,.04)' } } }
+ }
+ });
+}
+async function submitRate() {
+ const rate = document.getElementById('newRate').value;
+ const source = document.getElementById('exSourceInput').value || userLocationName;
+ const lat = document.getElementById('exSourceLat').value || userLat;
+ const lng = document.getElementById('exSourceLng').value || userLng;
+ if (!rate || Number(rate) < 1) return showToast('❌ أدخل سعراً صحيحاً', 'error');
+ const btn = document.querySelector('#sec-exchange .btn-submit');
+ if (btn) { btn.disabled = true; btn.textContent = '⏳ جاري الإرسال...'; }
+ try {
+ await fetch('/api/exchange', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ rate, source, lat, lng }) });
+ document.getElementById('newRate').value = '';
+ showToast('✅ تم مشاركة السعر! شكراً', 'success');
+ loadExchange();
+ } catch { showToast('❌ حدث خطأ', 'error'); }
+ finally { if (btn) { btn.disabled = false; btn.textContent = '📤 شارك السعر'; } }
+}
+
+/* ============================================================
+ MEDICINES
+============================================================ */
+async function loadMedicines() {
+ try { allMedicines = await fetch('/api/medicines').then(r => r.json()); } catch { allMedicines = []; }
+ renderMedicines();
+}
+function renderMedicines() {
+ const list = filterMedicines();
+ const el = document.getElementById('medList');
+ if (!el) return;
+ if (!list.length) {
+ const q = document.getElementById('medSearch') ? document.getElementById('medSearch').value : '';
+ el.innerHTML = q ? emptyState('🔍', 'لا نتائج لـ "' + q + '"', 'جرب اسماً آخر') : emptyState('💊', 'لا توجد أدوية مُسجَّلة', 'أضف معلومة دواء وساعد مجتمعك');
+ return;
+ }
+ el.innerHTML = list.map(m => {
+ const d = m.lat && userLat ? haversine(userLat, userLng, m.lat, m.lng) : null;
+ return '' +
+ '
' + escHtml(m.name) + '
' +
+ (m.nameEn ? '
' + escHtml(m.nameEn) + '
' : '') +
+ '
🏥 ' + escHtml(m.pharmacy) + '
' +
+ '
📍 ' + escHtml(m.area) + ' • 🕐 ' + timeAgo(m.time) + '
' +
+ (d !== null ? '
📡 ' + (d < 1 ? '<1' : Math.round(d)) + ' كم
' : '') + '
' +
+ '
' +
+ '
' + (m.available && m.price ? m.price + ' ج.س' : m.available ? 'متوفر' : 'غير متوفر') + '
' +
+ '
' + (m.available ? '✅ متوفر' : '❌ نفد') + '
' +
+ (m.lat && userLat ? '
🗺️ ' : '') +
+ '
';
+ }).join('');
+}
+function filterMedicines() {
+ const q = (document.getElementById('medSearch') ? document.getElementById('medSearch').value : '').toLowerCase().trim();
+ return allMedicines.filter(m => {
+ const qOk = !q || m.name.includes(q) || (m.nameEn || '').toLowerCase().includes(q) || (m.pharmacy || '').includes(q) || (m.area || '').includes(q);
+ const fOk = medFilter === 'all' || (medFilter === 'available' && m.available) || (medFilter === 'unavailable' && !m.available) || (medFilter === 'nearby' && m.lat && userLat && haversine(userLat, userLng, m.lat, m.lng) <= 20);
+ return qOk && fOk;
+ });
+}
+function searchMedicine() { renderMedicines(); }
+function filterMed(f, btn) {
+ if (f === 'nearby' && !userLat) { showToast('⚠️ الموقع غير محدد', 'error'); return; }
+ medFilter = f;
+ document.querySelectorAll('.med-filter-row .filt').forEach(b => b.classList.remove('active-filt'));
+ btn.classList.add('active-filt');
+ renderMedicines();
+}
+async function submitMedicine() {
+ const name = document.getElementById('medName').value.trim();
+ const pharmacy = document.getElementById('medPharmacy').value.trim();
+ const area = document.getElementById('medAreaInput').value.trim() || userLocationName;
+ const lat = document.getElementById('medLat').value || userLat;
+ const lng = document.getElementById('medLng').value || userLng;
+ const price = document.getElementById('medPrice').value;
+ const avail = document.querySelector('[name="medAvail"]:checked') ? document.querySelector('[name="medAvail"]:checked').value === 'true' : true;
+ if (!name) return showToast('❌ أدخل اسم الدواء', 'error');
+ if (!pharmacy) return showToast('❌ أدخل اسم الصيدلية', 'error');
+ try {
+ await fetch('/api/medicines', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name, pharmacy, area, price: Number(price) || 0, available: avail, lat, lng }) });
+ ['medName', 'medPharmacy', 'medAreaInput', 'medPrice'].forEach(id => { const e = document.getElementById(id); if (e) e.value = ''; });
+ showToast('✅ تم إضافة معلومة الدواء!', 'success');
+ loadMedicines();
+ } catch { showToast('❌ حدث خطأ', 'error'); }
+}
+
+/* ============================================================
+ VOICE
+============================================================ */
+async function loadVoice() {
+ try { allVoice = await fetch('/api/voice').then(r => r.json()); } catch { allVoice = []; }
+ renderVoice();
+}
+function renderVoice() {
+ const el = document.getElementById('voiceList');
+ if (!el) return;
+ const catIcons = { كهرباء: '⚡', ماء: '💧', طرق: '🛣️', صحة: '🏥', أمن: '🔒', أخرى: '📌' };
+ el.innerHTML = allVoice.length ? allVoice.map(v =>
+ '' +
+ '' +
+ (v.desc ? '
' + escHtml(v.desc) + '
' : '') +
+ '
'
+ ).join('') : emptyState('📢', 'لا توجد بلاغات بعد', 'كن أول من يرفع مشكلة - صوتك يصنع التغيير!');
+}
+async function submitVoice() {
+ const title = document.getElementById('voiceTitle').value.trim();
+ const desc = document.getElementById('voiceDesc').value.trim();
+ const area = document.getElementById('voiceAreaInput').value.trim() || userLocationName;
+ const lat = document.getElementById('voiceLat').value || userLat;
+ const lng = document.getElementById('voiceLng').value || userLng;
+ const category = document.getElementById('voiceCat').value;
+ if (!title) return showToast('❌ أدخل عنوان المشكلة', 'error');
+ try {
+ await fetch('/api/voice', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ title, desc, area, category, lat, lng }) });
+ document.getElementById('voiceTitle').value = ''; document.getElementById('voiceDesc').value = '';
+ showToast('📢 تم إرسال صوتك!', 'success'); loadVoice();
+ } catch { showToast('❌ حدث خطأ', 'error'); }
+}
+async function voteVoice(id) {
+ try { await fetch('/api/voice/' + id + '/vote', { method: 'POST' }); const v = allVoice.find(x => x.id === id); if (v) { v.votes++; renderVoice(); } } catch {}
+}
+
+/* ============================================================
+ SKILLS
+============================================================ */
+async function loadSkills() {
+ try { allSkills = await fetch('/api/skills').then(r => r.json()); } catch { allSkills = []; }
+ renderSkills();
+}
+function renderSkills() {
+ const el = document.getElementById('skillsList');
+ if (!el) return;
+ el.innerHTML = allSkills.length ? allSkills.map(s =>
+ '' +
+ '
' + escHtml(s.avatar || s.name.substring(0, 2).toUpperCase()) + '
' +
+ '
' + escHtml(s.name) + '
' +
+ '
💼 ' + escHtml(s.skill) + '
' +
+ '
✅ ' + escHtml(s.offer) + ' 🔄 ' + escHtml(s.want) + '
' +
+ '' +
+ (s.contact ? '
' : '') +
+ '
💬 دردشة ' +
+ '
'
+ ).join('') : emptyState('🤝', 'لا توجد مهارات بعد', 'أضف مهارتك وتواصل مع مجتمعك مجاناً!');
+}
+async function submitSkill() {
+ const name = document.getElementById('skillName').value.trim();
+ const offer = document.getElementById('skillOffer').value.trim();
+ const want = document.getElementById('skillWant').value.trim();
+ const contact = document.getElementById('skillContact').value.trim();
+ const area = document.getElementById('skillAreaInput').value.trim() || userLocationName;
+ const lat = document.getElementById('skillLat').value || userLat;
+ const lng = document.getElementById('skillLng').value || userLng;
+ if (!name) return showToast('❌ أدخل اسمك', 'error');
+ if (!offer) return showToast('❌ أدخل ما تعرضه', 'error');
+ if (!want) return showToast('❌ أدخل ما تريده', 'error');
+ try {
+ await fetch('/api/skills', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name, skill: offer, offer, want, area, contact, lat, lng }) });
+ ['skillName', 'skillOffer', 'skillWant', 'skillContact', 'skillAreaInput'].forEach(id => { const e = document.getElementById(id); if (e) e.value = ''; });
+ showToast('🤝 تم إضافة مهارتك!', 'success'); loadSkills();
+ } catch { showToast('❌ حدث خطأ', 'error'); }
+}
+
+/* ============================================================
+ MARKET P2P
+============================================================ */
+async function loadMarket() {
+ try { allMarket = await fetch('/api/market').then(r => r.json()); } catch { allMarket = []; }
+ renderMarket(); renderHomeMarket();
+}
+function getFilteredMarket() {
+ return allMarket.filter(m => {
+ const typeOk = marketFilter === 'all' || m.type === marketFilter || (marketFilter === 'nearby' && m.lat && userLat && haversine(userLat, userLng, m.lat, m.lng) <= 30);
+ const catOk = marketCatFilter === 'all' || m.category === marketCatFilter;
+ return typeOk && catOk;
+ });
+}
+function renderMarket() {
+ const el = document.getElementById('marketList');
+ if (!el) return;
+ const list = getFilteredMarket();
+ if (!list.length) { el.innerHTML = '' + emptyState('🛒', 'لا توجد إعلانات بعد', 'كن أول من ينشر إعلانه!') + '
'; return; }
+ const typeLabel = { sell: '💰 بيع', buy: '🛍️ شراء', trade: '🔄 تبادل' };
+ const typeClass = { sell: 'mc-sell', buy: 'mc-buy', trade: 'mc-trade' };
+ el.innerHTML = list.map(m => {
+ const d = m.lat && userLat ? haversine(userLat, userLng, m.lat, m.lng) : null;
+ return '' +
+ '
' + (typeLabel[m.type] || 'بيع') + ' ' +
+ '
' + escHtml(m.title) + '
' +
+ (m.price ? '
' + m.price + ' ' + (m.currency || 'ج.س') + '
' : '
تبادل
') +
+ '
📍 ' + escHtml(m.area) + ' • 🕐 ' + timeAgo(m.time) + '
' +
+ (d !== null ? '
📡 ' + (d < 1 ? '<1' : Math.round(d)) + ' كم
' : '') +
+ '
';
+ }).join('');
+}
+function renderHomeMarket() {
+ const el = document.getElementById('homeMarket');
+ if (!el) return;
+ if (!allMarket.length) { el.innerHTML = emptyState('🛒', 'لا توجد إعلانات بعد', 'كن أول من ينشر!', 'market'); return; }
+ el.innerHTML = allMarket.slice(0, 4).map(m =>
+ '' +
+ '
' + escHtml(m.title) + '
📍 ' + escHtml(m.area) + ' • ' + m.category + '
' +
+ (m.price ? '
' + m.price + ' ' + (m.currency || 'ج.س') + '
' : '
تبادل
') +
+ '
'
+ ).join('');
+}
+function filterMarket(f, btn) {
+ if (f === 'nearby' && !userLat) { showToast('⚠️ الموقع غير محدد', 'error'); return; }
+ marketFilter = f;
+ document.querySelectorAll('.market-filters .mfilt').forEach(b => b.classList.remove('active-mfilt'));
+ btn.classList.add('active-mfilt'); renderMarket();
+}
+function filterMarketCat(cat, btn) {
+ marketCatFilter = cat;
+ document.querySelectorAll('.market-cats .mcat').forEach(b => b.classList.remove('active-mcat'));
+ btn.classList.add('active-mcat'); renderMarket();
+}
+function selectMType(type, btn) {
+ selectedMarketType = type;
+ document.querySelectorAll('.mtype').forEach(b => b.classList.remove('active-mtype'));
+ btn.classList.add('active-mtype');
+}
+async function submitMarket() {
+ const title = document.getElementById('mTitle').value.trim();
+ const desc = document.getElementById('mDesc').value.trim();
+ const category = document.getElementById('mCategory').value;
+ const price = document.getElementById('mPrice').value;
+ const currency = document.getElementById('mCurrency').value;
+ const contact = document.getElementById('mContact').value.trim();
+ const area = document.getElementById('mAreaInput').value.trim() || userLocationName;
+ const lat = document.getElementById('mLat').value || userLat;
+ const lng = document.getElementById('mLng').value || userLng;
+ if (!title) return showToast('❌ أدخل عنوان الإعلان', 'error');
+ if (!contact) return showToast('❌ أدخل طريقة التواصل', 'error');
+ try {
+ // Upload market photo if present
+ let imageId = null;
+ const mPhoto = document.getElementById('marketPhoto');
+ if (mPhoto && mPhoto.files && mPhoto.files[0]) {
+ await new Promise(function(resolve) { uploadPhoto('marketPhoto', function(id) { imageId = id; resolve(); }); });
+ }
+ await fetch('/api/market', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ title, desc, type: selectedMarketType, price: Number(price) || 0, currency, category, contact, area, lat, lng, imageId }) });
+ ['mTitle', 'mDesc', 'mPrice', 'mContact', 'mAreaInput'].forEach(id => { const e = document.getElementById(id); if (e) e.value = ''; });
+ if (mPhoto) mPhoto.value = '';
+ const mpv = document.getElementById('marketPhotoPreview'); if (mpv) { mpv.classList.add('hidden'); mpv.innerHTML = ''; }
+ const mpn = document.getElementById('marketPhotoName'); if (mpn) mpn.textContent = '';
+ showToast('🛒 تم نشر إعلانك!', 'success'); loadMarket();
+ } catch { showToast('❌ حدث خطأ', 'error'); }
+}
+async function likeMarket(id) { try { await fetch('/api/market/' + id + '/like', { method: 'POST' }); } catch {} }
+function openMarketModal(id) {
+ const m = allMarket.find(x => x.id === id);
+ if (!m) return;
+ fetch('/api/market/' + id + '/view', { method: 'POST' }).catch(() => {});
+ const typeLabel = { sell: '💰 للبيع', buy: '🛍️ مطلوب', trade: '🔄 للتبادل' };
+ const typeClass = { sell: 'mc-sell', buy: 'mc-buy', trade: 'mc-trade' };
+ const d = m.lat && userLat ? haversine(userLat, userLng, m.lat, m.lng) : null;
+ document.getElementById('marketModalContent').innerHTML =
+ '' + (typeLabel[m.type] || 'بيع') + ' ' +
+ (m.imageId ? ' ' : '') +
+ '' + escHtml(m.title) + '
' +
+ (m.price ? '' + m.price + ' ' + (m.currency || 'ج.س') + '
' : 'تبادل مباشر
') +
+ (m.desc ? '' + escHtml(m.desc) + '
' : '') +
+ '' +
+ '
📍 الموقع ' + escHtml(m.area) + '
' +
+ '
📦 الفئة ' + escHtml(m.category) + '
' +
+ '
🕐 التاريخ ' + timeAgo(m.time) + '
' +
+ '
👁️ المشاهدات ' + (m.views + 1) + '
' +
+ (d !== null ? '
📡 المسافة ' + (d < 1 ? '<1' : Math.round(d)) + ' كم
' : '') + '
' +
+ '' +
+ '📞 تواصل الآن ' +
+ '💬 دردشة
' +
+ '🔗 مشاركة الإعلان ';
+ document.getElementById('marketModal').classList.remove('hidden');
+ history.pushState({ section: currentSection, modal: 'market' }, '', '#' + currentSection);
+}
+function closeMarketModal(e) {
+ if (!e || e.target === document.getElementById('marketModal')) document.getElementById('marketModal').classList.add('hidden');
+}
+function contactSeller(contact) {
+ if (!contact) return showToast('❌ لا توجد طريقة تواصل', 'error');
+ if (contact.match(/^[\d+]+$/)) window.open('tel:' + contact);
+ else if (contact.match(/^https?:\/\//)) window.open(contact);
+ else showToast('📞 تواصل عبر: ' + contact, 'success');
+}
+
+/* ============================================================
+ NEARBY USERS PANEL
+============================================================ */
+function renderNearbyUsers() {
+ const el = document.getElementById('nearbyUsersPanel');
+ if (!el) return;
+ const countEl = document.getElementById('nearbyCount');
+ if (!nearbyUsers.length) { el.classList.add('hidden'); return; }
+ el.classList.remove('hidden');
+ if (countEl) countEl.textContent = '(' + nearbyUsers.length + ' قريب منك)';
+ const list = document.getElementById('nearbyUsersList');
+ if (!list) return;
+ list.innerHTML = nearbyUsers.slice(0, 10).map(u =>
+ '' +
+ '
' + (u.name ? u.name.substring(0, 2).toUpperCase() : 'مج') + '
' +
+ '
' + escHtml(u.name || 'مستخدم') + '
' +
+ '
📡 ' + (u.dist || '?') + ' كم
'
+ ).join('');
+}
+
+function renderNearbyPeopleList(people) {
+ const el = document.getElementById('nearbyPeopleList');
+ if (!el) return;
+ if (!people || !people.length) {
+ el.innerHTML = emptyState('👥', 'لا يوجد أشخاص نشطون قريبون الآن', 'سيظهرون هنا عند فتح التطبيق');
+ return;
+ }
+ el.innerHTML = people.slice(0, 20).map(p => {
+ const d = p.lat && userLat ? haversine(userLat, userLng, p.lat, p.lng) : null;
+ const avatarText = p.avatar || (p.name ? p.name.substring(0, 2).toUpperCase() : '👤');
+ return '' +
+ '
' + avatarText + '
' +
+ '
' +
+ '
' + escHtml(p.name || 'مستخدم') + '
' +
+ '
📍 ' + escHtml(p.area || 'غير محدد') + '
' +
+ (d !== null ? '
📡 ' + (d < 1 ? '<1' : Math.round(d)) + ' كم
' : '') +
+ '
' +
+ '
' +
+ '💬 ' +
+ '🗺️ ' +
+ '
';
+ }).join('');
+}
+
+/* ============================================================
+ PEOPLE SEARCH (Truecaller-style) - Enhanced v2
+============================================================ */
+let peopleSearchTimer = null;
+function setPeopleSearchType(type, btn) {
+ peopleSearchType = type;
+ document.querySelectorAll('.psb-tab').forEach(b => b.classList.remove('active-psb-tab'));
+ btn.classList.add('active-psb-tab');
+ const inp = document.getElementById('peopleSearchInp');
+ const icon = document.getElementById('psb-icon');
+ if (inp) {
+ if (type === 'phone') { inp.placeholder = '📱 أدخل رقم الهاتف المُعلن أو الشخصي...'; if (icon) icon.textContent = '📱'; inp.type = 'tel'; }
+ else if (type === 'email') { inp.placeholder = '✉️ أدخل البريد الإلكتروني...'; if (icon) icon.textContent = '✉️'; inp.type = 'email'; }
+ else if (type === 'name') { inp.placeholder = '👤 ابحث بالاسم الكامل أو الجزئي...'; if (icon) icon.textContent = '👤'; inp.type = 'text'; }
+ else if (type === 'company') { inp.placeholder = '🏢 ابحث باسم الشركة أو المسمى الوظيفي...'; if (icon) icon.textContent = '🏢'; inp.type = 'text'; }
+ else { inp.placeholder = 'ابحث في جميع المواقع والشركات...'; if (icon) icon.textContent = '🔍'; inp.type = 'text'; }
+ inp.focus();
+ }
+ searchPeople();
+}
+
+function searchPeople(instant = false) {
+ clearTimeout(peopleSearchTimer);
+ const delay = instant ? 0 : 400;
+ peopleSearchTimer = setTimeout(async () => {
+ const q = (document.getElementById('peopleSearchInp')?.value || '').trim();
+ const el = document.getElementById('peopleResults');
+ const statsEl = document.getElementById('psb-stats');
+ if (!el) return;
+ if (!q || q.length < 2) {
+ el.innerHTML = '';
+ if (statsEl) statsEl.style.display = 'none';
+ return;
+ }
+ el.innerHTML = '
جاري البحث في جميع البيانات...
';
+ try {
+ const results = await fetch('/api/search/people?q=' + encodeURIComponent(q) + '&type=' + peopleSearchType + '&limit=30').then(r => r.json());
+
+ // Update stats
+ if (statsEl) {
+ statsEl.style.display = 'flex';
+ const countEl = document.getElementById('psb-count');
+ const scopeEl = document.getElementById('psb-scope');
+ if (countEl) countEl.textContent = results.length + ' نتيجة';
+ if (scopeEl) {
+ const types = [...new Set(results.map(r => r.type))];
+ const labels = { person: 'أشخاص', listing: 'إعلانات', skill: 'مهارات' };
+ scopeEl.textContent = types.map(t => labels[t] || t).join(' + ');
+ }
+ }
+
+ if (!results.length) {
+ el.innerHTML = emptyState('🔍', 'لا نتائج لـ "' + q + '"', 'جرب اسماً أو رقماً أو بريداً مختلفاً');
+ return;
+ }
+ el.innerHTML = results.map(p => renderPersonCard(p, q)).join('');
+ } catch { el.innerHTML = emptyState('⚠️', 'خطأ في البحث', 'تحقق من اتصالك وحاول مجدداً'); }
+ }, delay);
+}
+
+function renderPersonCard(p, query = '') {
+ const isOnline = nearbyUsers.some(u => u.userId === p.userId);
+ const d = p.lat && userLat ? haversine(userLat, userLng, p.lat, p.lng) : null;
+ const avatarText = p.avatar || (p.name ? p.name.substring(0, 2).toUpperCase() : '👤');
+ const isPerson = p.type === 'person' || !p.type;
+ const isListing = p.type === 'listing';
+ const isSkill = p.type === 'skill';
+ const typeTag = isListing ? '🛒 إعلان ' :
+ isSkill ? '🤝 مهارة ' : '';
+
+ // Public phone (always shown if exists)
+ const pubPhone = p.publicPhone || '';
+
+ return '' +
+ '' +
+
+ // الرقم المعلن - يُعرض دائماً إذا وُجد
+ (pubPhone ? '
📢 مُعلن ' + escHtml(pubPhone) + ' 📞 ' : '') +
+
+ // تفاصيل التواصل
+ '
' +
+ (p.phone && !pubPhone ? '
📱 ' + escHtml(p.phone) + ' ' +
+ '📞
' : '') +
+ (p.email ? '
✉️ ' + escHtml(p.email) + '
' : '') +
+ (p.website ? '
' : '') +
+ '
' +
+
+ // أزرار الإجراءات
+ '
' +
+ (p.userId && isPerson ? '💬 راسله ' : '') +
+ (pubPhone || p.phone ? '📞 اتصال ' : '') +
+ (p.lat ? '📍 موقعه ' : '') +
+ '
';
+}
+
+/* ============================================================
+ QUICK MESSAGE SEARCH (in messages section)
+============================================================ */
+let quickSearchTimer = null;
+function quickSearchForMsg(query) {
+ clearTimeout(quickSearchTimer);
+ const resEl = document.getElementById('msgQuickResults');
+ if (!query || query.length < 2) {
+ if (resEl) resEl.classList.add('hidden');
+ return;
+ }
+ quickSearchTimer = setTimeout(async () => {
+ try {
+ const results = await fetch('/api/search/people?q=' + encodeURIComponent(query) + '&type=name&limit=8').then(r => r.json());
+ if (!results.length || !resEl) { if (resEl) resEl.classList.add('hidden'); return; }
+ resEl.innerHTML = results.map(p => {
+ const avatarText = p.avatar || (p.name ? p.name.substring(0, 2).toUpperCase() : '👤');
+ return '' +
+ '
' + avatarText + '
' +
+ '
' + escHtml(p.name || 'مستخدم') + '
' +
+ '
📍 ' + escHtml(p.area || '') + '
' +
+ '
';
+ }).join('');
+ resEl.classList.remove('hidden');
+ } catch {}
+ }, 350);
+}
+
+document.addEventListener('click', e => {
+ if (!e.target.closest('#msgQuickSearch') && !e.target.closest('#msgQuickResults')) {
+ const r = document.getElementById('msgQuickResults');
+ if (r) r.classList.add('hidden');
+ }
+});
+
+/* ============================================================
+ USER PROFILE
+============================================================ */
+function loadMyProfile() {
+ const stored = localStorage.getItem('nabdh_profile');
+ if (stored) {
+ try { myProfile = JSON.parse(stored); } catch { myProfile = null; }
+ }
+ if (myProfile) {
+ myName = myProfile.name || myName;
+ updateProfileUI();
+ // تحديث رابط الملف الشخصي في القائمة
+ const pLinkName = document.getElementById('menuProfileLinkName');
+ if (pLinkName && myProfile.name) pLinkName.textContent = myProfile.name;
+ const pIco = document.getElementById('menuProfileIco');
+ if (pIco && myProfile.emoji) pIco.textContent = myProfile.emoji;
+ }
+}
+
+/* ─────────────────────────────────────────────────────────────
+ updateProfileUI — v4.0 Full Rebuild
+───────────────────────────────────────────────────────────── */
+
+/* ─────────────────────────────────────────────────────────────
+ updateProfileUI — v5.0 (2026 Ultra Modern)
+───────────────────────────────────────────────────────────── */
+function updateProfileUI() {
+ const name = myProfile?.name || myName || 'مستخدم نبض';
+ const avatar = myProfile?.avatar || (name ? name.substring(0, 2).toUpperCase() : '👤');
+ const area = myProfile?.area || userLocationName || 'غير محدد';
+
+ // ── Name / menu ───────────────────────────────────────────
+ setEl('profileHeroName', name);
+ setEl('menuProfileName', name);
+
+ // ── Location row ──────────────────────────────────────────
+ const heroAreaEl = document.getElementById('profileHeroArea');
+ if (heroAreaEl) {
+ const span = heroAreaEl.querySelector('span');
+ if (span) span.textContent = area;
+ else heroAreaEl.textContent = area;
+ }
+
+ // ── Job title ─────────────────────────────────────────────
+ const heroJob = document.getElementById('profileHeroJob');
+ if (heroJob) {
+ const jobStr = [myProfile?.jobTitle, myProfile?.company].filter(Boolean).join(' • ');
+ heroJob.textContent = jobStr;
+ heroJob.classList.toggle('hidden', !jobStr);
+ }
+
+ // ── Avatar display ────────────────────────────────────────
+ const avatarDisplay = avatar.length <= 3 ? avatar : avatar.substring(0, 2);
+ ['profileAvatarBig', 'menuAvatar', 'peAvatarPreview'].forEach(id => {
+ const el = document.getElementById(id);
+ if (el) el.textContent = avatarDisplay;
+ });
+
+ // ── Restore profile photo if saved ───────────────────────
+ if (myProfile?.profileImage) {
+ ['profilePhotoDisplay','pePhotoImg'].forEach(id => {
+ const el = document.getElementById(id);
+ if (el) { el.src = myProfile.profileImage; el.classList.remove('hidden'); el.style.display = 'block'; }
+ });
+ const bigAv = document.getElementById('profileAvatarBig');
+ if (bigAv) bigAv.style.display = 'none';
+ }
+
+ // ── Cover gradient (v5) ───────────────────────────────────
+ const cover = document.getElementById('profileCover');
+ if (cover) {
+ const covers = [
+ 'linear-gradient(135deg,#0d2233 0%,#1a3a4a 60%,#0a1a2a 100%)',
+ 'linear-gradient(135deg,#1a0d33 0%,#2d1b4e 60%,#0d0a2a 100%)',
+ 'linear-gradient(135deg,#0d2a1a 0%,#1a4a2e 60%,#0a1a0d 100%)',
+ 'linear-gradient(135deg,#2a0d0d 0%,#4a1a1a 60%,#1a0a0a 100%)',
+ 'linear-gradient(135deg,#0d1a33 0%,#1a2a4a 60%,#0a0d1a 100%)',
+ 'linear-gradient(135deg,#1a1a0d 0%,#3a2a1a 60%,#0d0d0a 100%)',
+ 'linear-gradient(135deg,#061a1a 0%,#0d3a3a 60%,#030d0d 100%)',
+ 'linear-gradient(135deg,#1a0d1a 0%,#330d33 60%,#0d060d 100%)',
+ ];
+ const idx = myProfile?.coverIndex ?? (name.charCodeAt(0) % covers.length);
+ cover.style.background = covers[idx % covers.length];
+ cover.style.transition = 'background .6s ease';
+ }
+
+ // ── Verified badge ────────────────────────────────────────
+ const vBadge = document.getElementById('profileVerifiedBadge');
+ if (vBadge) vBadge.style.display = myProfile?.verified ? 'inline-flex' : 'none';
+
+ // ── Public phone bar ──────────────────────────────────────
+ const pubPhone = myProfile?.publicPhone;
+ const pubPhoneBar = document.getElementById('publicPhoneBar');
+ const ppbNumber = document.getElementById('ppb-number');
+ const pvPubRow = document.getElementById('pv-public-phone-row');
+ const pvPubPhone = document.getElementById('pv-public-phone');
+ if (pubPhoneBar) { pubPhoneBar.style.display = pubPhone ? 'flex' : 'none'; pubPhoneBar.classList.toggle('hidden', !pubPhone); }
+ if (ppbNumber) ppbNumber.textContent = pubPhone || '—';
+ if (pvPubRow) pvPubRow.style.display = pubPhone ? 'flex' : 'none';
+ if (pvPubPhone) pvPubPhone.textContent = pubPhone || '—';
+
+ // ── Contact ───────────────────────────────────────────────
+ setEl('pv-phone', myProfile?.phone || 'غير مُضاف');
+ setEl('pv-email', myProfile?.email || 'غير مُضاف');
+ setEl('pv-whatsapp', myProfile?.whatsapp || '—');
+ setEl('pv-telegram', myProfile?.telegram || '—');
+
+ const phoneCallBtn = document.getElementById('pv-phone-call-btn');
+ if (phoneCallBtn) phoneCallBtn.style.display = myProfile?.phone ? 'inline-flex' : 'none';
+
+ const emailLink = document.getElementById('pv-email-link');
+ if (emailLink) {
+ if (myProfile?.email) { emailLink.href = 'mailto:' + myProfile.email; emailLink.classList.remove('hidden'); emailLink.style.display = 'inline-flex'; }
+ else { emailLink.classList.add('hidden'); emailLink.style.display = 'none'; }
+ }
+ const waLink = document.getElementById('pv-whatsapp-link');
+ if (waLink) {
+ if (myProfile?.whatsapp) { waLink.href = 'https://wa.me/' + myProfile.whatsapp.replace(/\D/g,''); waLink.classList.remove('hidden'); }
+ else waLink.classList.add('hidden');
+ }
+ const tgLink = document.getElementById('pv-telegram-link');
+ if (tgLink) {
+ if (myProfile?.telegram) { tgLink.href = 'https://t.me/' + myProfile.telegram.replace('@',''); tgLink.classList.remove('hidden'); }
+ else tgLink.classList.add('hidden');
+ }
+
+ // ── Professional ──────────────────────────────────────────
+ setEl('pv-jobtitle', myProfile?.jobTitle || '—');
+ setEl('pv-company', myProfile?.company || '—');
+ const websiteEl = document.getElementById('pv-website');
+ if (websiteEl) {
+ websiteEl.textContent = myProfile?.website || '—';
+ websiteEl.href = myProfile?.website ? (myProfile.website.startsWith('http') ? myProfile.website : 'https://' + myProfile.website) : '#';
+ }
+ setEl('pv-area', area);
+ setEl('pv-bio', myProfile?.bio || 'أضف نبذة عنك...');
+
+ // ── Stats with animated counters ─────────────────────────
+ if (myProfile?.joinDate) {
+ const d = new Date(myProfile.joinDate);
+ setEl('ps-joined', d.toLocaleDateString('ar', { year:'numeric', month:'short' }));
+ }
+ animateCounter('ps-reports', 0, myProfile?.reports || 0, 800);
+
+ // ── XP + Level ────────────────────────────────────────────
+ updateProfileXP();
+
+ // ── Badges ────────────────────────────────────────────────
+ updateProfileBadges();
+
+ // ── Points card ───────────────────────────────────────────
+ refreshProfilePointsCard();
+
+ // ── Activity bars ─────────────────────────────────────────
+ refreshActivityBars();
+
+ // ── Sidebar WA stats ─────────────────────────────────────
+ updateSidebarStats();
+}
+
+/* Update sidebar WhatsApp-style stats */
+function updateSidebarStats() {
+ const reports = myProfile?.reports || 0;
+ const points = myPoints || 0;
+ const lvl = getProfileLevel(points);
+ const menuAvEl = document.getElementById('menuAvatar');
+ if (menuAvEl) {
+ const avatar = myProfile?.avatar || '👤';
+ if (myProfile?.profileImage) {
+ menuAvEl.innerHTML = ` `;
+ } else {
+ menuAvEl.textContent = avatar;
+ }
+ }
+ setEl('menuStatReports', reports);
+ setEl('menuStatPoints', points);
+ setEl('menuStatLevel', lvl.level);
+}
+
+/* Animate a numeric counter from start to end */
+function animateCounter(id, from, to, duration) {
+ const el = document.getElementById(id);
+ if (!el) return;
+ if (from === to) { el.textContent = to; return; }
+ const step = (to - from) / (duration / 16);
+ let current = from;
+ const timer = setInterval(() => {
+ current = Math.min(to, current + step);
+ el.textContent = Math.round(current);
+ el.classList.add('bump');
+ setTimeout(() => el.classList.remove('bump'), 400);
+ if (current >= to) clearInterval(timer);
+ }, 16);
+}
+
+/* Build the 7-day activity bar chart */
+function refreshActivityBars() {
+ const barsEl = document.getElementById('pv4ActivityBars');
+ const dotsEl = document.getElementById('pv4ActivityDots');
+ if (!barsEl) return;
+
+ // Build synthetic weekly data from reports/points (or fake it nicely)
+ const reports = myProfile?.reports || 0;
+ const base = Math.max(1, Math.floor(reports / 7));
+ const days = ['أ','ث','ر','خ','ج','س','ح'];
+ const heights = days.map((_, i) => {
+ const h = Math.round(base + Math.random() * base * 1.5);
+ return Math.max(6, Math.min(40, h));
+ });
+ const maxH = Math.max(...heights);
+
+ barsEl.innerHTML = heights.map((h, i) => {
+ const pct = Math.round((h / maxH) * 100);
+ const isToday = i === 6;
+ return `
+
`;
+ }).join('');
+
+ if (dotsEl) {
+ dotsEl.innerHTML = heights.map((_, i) =>
+ `
`
+ ).join('');
+ }
+}
+
+function updateProfileBadges() {
+ const grid = document.getElementById('profileBadgesGrid');
+ if (!grid) return;
+ const reports = myProfile?.reports || 0;
+ const joinDays = myProfile?.joinDate ? Math.floor((Date.now() - myProfile.joinDate) / 86400000) : 0;
+
+ const badges = [];
+ badges.push('🌟 عضو نبض
');
+ if (reports >= 1) badges.push('📢 مُبلِّغ
');
+ if (reports >= 5) badges.push('🔥 مُبلِّغ نشط
');
+ if (reports >= 20) badges.push('⚡ خبير
');
+ if (myProfile?.publicPhone) badges.push('📞 سهل التواصل
');
+ if (myProfile?.jobTitle || myProfile?.company) badges.push('💼 محترف
');
+ if (myProfile?.verified) badges.push('✅ موثق
');
+ if (myProfile?.bio && myProfile.bio.length > 20) badges.push('📝 له نبذة
');
+ if (joinDays >= 7) badges.push('📅 ' + joinDays + ' يوم
');
+ if (joinDays >= 30) badges.push('🎖️ متحمس
');
+ if (myProfile?.whatsapp || myProfile?.telegram) badges.push('💬 متواصل
');
+ if (myProfile?.website) badges.push('🌐 له موقع
');
+ grid.innerHTML = badges.join('');
+}
+
+function changeProfileCover() {
+ const covers = [
+ 'linear-gradient(135deg,#0d2233 0%,#1a3a4a 60%,#0a1a2a 100%)',
+ 'linear-gradient(135deg,#1a0d33 0%,#2d1b4e 60%,#0d0a2a 100%)',
+ 'linear-gradient(135deg,#0d2a1a 0%,#1a4a2e 60%,#0a1a0d 100%)',
+ 'linear-gradient(135deg,#2a0d0d 0%,#4a1a1a 60%,#1a0a0a 100%)',
+ 'linear-gradient(135deg,#0d1a33 0%,#1a2a4a 60%,#0a0d1a 100%)',
+ 'linear-gradient(135deg,#1a1a0d 0%,#3a2a1a 60%,#0d0d0a 100%)',
+ 'linear-gradient(135deg,#061a1a 0%,#0d3a3a 60%,#030d0d 100%)',
+ 'linear-gradient(135deg,#1a0d1a 0%,#330d33 60%,#0d060d 100%)',
+ 'linear-gradient(135deg,#0d0d33 0%,#1a1a5a 60%,#06060d 100%)',
+ 'linear-gradient(135deg,#1a3a0d 0%,#2a5a1a 60%,#0d1a06 100%)',
+ ];
+ const cover = document.getElementById('profileCover');
+ if (!cover) return;
+ const cur = myProfile?.coverIndex ?? 0;
+ const next = (cur + 1) % covers.length;
+ cover.style.background = covers[next];
+ cover.style.transition = 'background .6s ease';
+ if (!myProfile) myProfile = {};
+ myProfile.coverIndex = next;
+ try { localStorage.setItem('nabdh_profile', JSON.stringify(myProfile)); } catch(e) {}
+ showToast('🎨 تم تغيير الغلاف', 'success');
+}
+
+function callPublicPhone() {
+ const ph = myProfile?.publicPhone;
+ if (ph) window.open('tel:' + ph);
+ else showToast('❌ لم تُضف رقماً معلناً بعد', 'error');
+}
+
+function showMyLocationOnMap() {
+ if (!userLat) { showToast('❌ الموقع غير محدد', 'error'); return; }
+ goSection('map');
+ setTimeout(() => {
+ if (map) map.setView([userLat, userLng], 16, { animate: true });
+ }, 300);
+}
+
+function toggleProfileEdit() {
+ const form = document.getElementById('profileEditForm');
+ const view = document.getElementById('profileViewCard');
+ const actions = document.getElementById('profileActions');
+ const xpBar = document.getElementById('profileXpWrap');
+ const statsRow = document.querySelector('.pv4-stats-row');
+ const ptsCard = document.getElementById('profilePointsCardV2');
+ if (!form) return;
+ const isHidden = form.classList.contains('hidden');
+ form.classList.toggle('hidden');
+ if (view) view.classList.toggle('hidden');
+ if (actions) actions.classList.toggle('hidden');
+ if (ptsCard) ptsCard.classList.toggle('hidden');
+ if (xpBar) xpBar.style.opacity = isHidden ? '0.5' : '1';
+ if (statsRow) statsRow.style.opacity = isHidden ? '0.5' : '1';
+
+ if (isHidden) {
+ // Opening → fill form
+ const g = id => document.getElementById(id);
+ if (g('pe-name')) g('pe-name').value = myProfile?.name || myName || '';
+ if (g('pe-phone')) g('pe-phone').value = myProfile?.phone || '';
+ if (g('pe-email')) g('pe-email').value = myProfile?.email || '';
+ if (g('pe-bio')) g('pe-bio').value = myProfile?.bio || '';
+ if (g('pe-jobtitle')) g('pe-jobtitle').value = myProfile?.jobTitle || '';
+ if (g('pe-company')) g('pe-company').value = myProfile?.company || '';
+ if (g('pe-website')) g('pe-website').value = myProfile?.website || '';
+ if (g('pe-public-phone')) g('pe-public-phone').value = myProfile?.publicPhone || '';
+ if (g('pe-whatsapp')) g('pe-whatsapp').value = myProfile?.whatsapp || '';
+ if (g('pe-telegram')) g('pe-telegram').value = myProfile?.telegram || '';
+ if (g('pe-area')) g('pe-area').value = myProfile?.area || userLocationName || '';
+ if (g('pe-public')) g('pe-public').checked = myProfile?.isPublic !== false;
+ if (g('pe-showmap')) g('pe-showmap').checked = myProfile?.showOnMap !== false;
+ if (g('pe-notify')) g('pe-notify').checked = myProfile?.notify !== false;
+ if (g('peNamePreview')) g('peNamePreview').textContent = myProfile?.name || myName || 'مستخدم نبض';
+ if (g('peAvatarPreview')) {
+ const av = myProfile?.avatar || (myProfile?.name || myName || 'م').substring(0, 2).toUpperCase();
+ g('peAvatarPreview').textContent = av.length <= 3 ? av : av.substring(0, 2);
+ }
+ if (myProfile?.profileImage && g('pePhotoImg')) {
+ g('pePhotoImg').src = myProfile.profileImage;
+ g('pePhotoImg').classList.remove('hidden');
+ g('pePhotoImg').style.display = 'block';
+ if (g('peAvatarPreview')) g('peAvatarPreview').style.display = 'none';
+ }
+ setTimeout(() => form.scrollIntoView({ behavior: 'smooth', block: 'start' }), 100);
+ } else {
+ document.querySelector('#sec-profile')?.scrollIntoView({ behavior: 'smooth', block: 'start' });
+ }
+}
+
+async function saveProfile() {
+ const g = id => document.getElementById(id);
+ const name = (g('pe-name')?.value || '').trim() || myName || 'مستخدم';
+ const phone = (g('pe-phone')?.value || '').trim();
+ const email = (g('pe-email')?.value || '').trim();
+ const bio = (g('pe-bio')?.value || '').trim();
+ const jobTitle = (g('pe-jobtitle')?.value || '').trim();
+ const company = (g('pe-company')?.value || '').trim();
+ const website = (g('pe-website')?.value || '').trim();
+ const publicPhone = (g('pe-public-phone')?.value || '').trim();
+ const whatsapp = (g('pe-whatsapp')?.value || '').trim();
+ const telegram = (g('pe-telegram')?.value || '').trim();
+ const areaInput = (g('pe-area')?.value || '').trim();
+ const isPublic = g('pe-public')?.checked !== false;
+ const showOnMap = g('pe-showmap')?.checked !== false;
+ const notify = g('pe-notify')?.checked !== false;
+
+ if (!name) return showToast('❌ أدخل اسمك', 'error');
+
+ const area = areaInput || userLocationName || myProfile?.area || 'غير محدد';
+
+ const profileData = {
+ userId: myUserId, name, phone, email, bio,
+ jobTitle, company, website, publicPhone, whatsapp, telegram,
+ area, lat: userLat, lng: userLng,
+ isPublic, showOnMap, notify,
+ avatar: myProfile?.avatar || name.substring(0, 2).toUpperCase(),
+ joinDate: myProfile?.joinDate || Date.now(),
+ coverIndex: myProfile?.coverIndex,
+ };
+
+ const btn = g('profileSaveBtn') || document.querySelector('#profileEditForm .pv4-save-btn');
+ if (btn) { btn.disabled = true; btn.textContent = '⏳ جاري الحفظ...'; }
+
+ try {
+ const result = await fetch('/api/profile', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(profileData)
+ }).then(r => r.json());
+
+ myProfile = { ...profileData, ...result.profile };
+ myName = name;
+ localStorage.setItem('nabdh_name', name);
+ localStorage.setItem('nabdh_profile', JSON.stringify(myProfile));
+
+ updateProfileUI();
+ toggleProfileEdit();
+ showToast('✅ تم حفظ ملفك الشخصي!', 'success');
+
+ if (socket && userLat) {
+ socket.emit('user_location', {
+ lat: userLat, lng: userLng,
+ name: myName, area: userLocationName,
+ userId: myUserId, showOnMap,
+ avatar: myProfile.avatar, phone: publicPhone || phone
+ });
+ }
+ } catch { showToast('❌ حدث خطأ في الحفظ', 'error'); }
+ finally {
+ if (btn) {
+ btn.disabled = false;
+ btn.innerHTML = ' حفظ الملف الشخصي';
+ }
+ }
+}
+
+async function syncProfileWithServer(partial = {}) {
+ try {
+ const profileData = {
+ userId: myUserId,
+ name: myName || 'مستخدم',
+ area: userLocationName,
+ lat: userLat, lng: userLng,
+ ...(myProfile || {}),
+ ...partial,
+ };
+ const result = await fetch('/api/profile', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(profileData)
+ }).then(r => r.json());
+ myProfile = { ...(myProfile || {}), ...result.profile };
+ localStorage.setItem('nabdh_profile', JSON.stringify(myProfile));
+ } catch {}
+}
+
+function changeAvatar() {
+ const emojis = ['💻','📱','🚀','🌟','💡','🎯','🔥','⚡','🌙','☀️','🌊','🎭','🎵','🎨','🏆','💎','🦁','🦅','🌺','🍃','👨💼','👩💼','👨🔬','👩🔬','👨🏫','👩🏫'];
+ const cur = myProfile?.avatar || '';
+ const next = emojis[(emojis.indexOf(cur) + 1) % emojis.length];
+ if (!myProfile) myProfile = {};
+ myProfile.avatar = next;
+ localStorage.setItem('nabdh_profile', JSON.stringify(myProfile));
+ updateProfileUI();
+ syncProfileWithServer({ avatar: next });
+ showToast('✅ تم تغيير الأفاتار', 'success');
+}
+
+/* ─────────────────────────────────────────────────────────────
+ updateProfileXP — v5.0 (animated SVG arc + progress)
+───────────────────────────────────────────────────────────── */
+function updateProfileXP() {
+ const reports = myProfile?.reports || 0;
+ const joinDays = myProfile?.joinDate ? Math.floor((Date.now() - myProfile.joinDate) / 86400000) : 0;
+ const bonus = (myProfile?.bio ? 10 : 0) +
+ (myProfile?.publicPhone ? 15 : 0) +
+ (myProfile?.jobTitle ? 5 : 0) +
+ (myProfile?.company ? 5 : 0) +
+ (myProfile?.website ? 5 : 0);
+ const points = Math.max(myPoints || 0, Math.floor(reports * 10 + joinDays * 0.5 + bonus));
+ const lvl = getProfileLevel(points);
+ const progress = Math.min(100, Math.round((points - lvl.min) / Math.max(1, lvl.max - lvl.min) * 100));
+
+ // ── Animated counters ─────────────────────────────────────
+ animateCounter('ps-points', 0, points, 1000);
+ setEl('ps-level', lvl.level);
+
+ // ── Level chips (v5 IDs) ──────────────────────────────────
+ const pv4Icon = document.getElementById('pv4LevelIcon');
+ const pv4Text = document.getElementById('pv4LevelText');
+ if (pv4Icon) pv4Icon.textContent = lvl.icon;
+ if (pv4Text) pv4Text.textContent = lvl.name;
+
+ ['profileLevelIcon','ppcLevelIcon'].forEach(id => { const e=document.getElementById(id); if(e) e.textContent=lvl.icon; });
+ ['profileLevelText','ppcLevelTitle'].forEach(id => { const e=document.getElementById(id); if(e) e.textContent=lvl.name; });
+
+ const ppcPts = document.getElementById('ppcPtsText');
+ if (ppcPts) ppcPts.textContent = points + ' نقطة';
+
+ const levelInline = document.getElementById('profileLevelBadgeInline');
+ if (levelInline) levelInline.textContent = lvl.icon + ' ' + lvl.name;
+
+ // ── XP progress bar ───────────────────────────────────────
+ const pxbFill = document.getElementById('pxbFill');
+ const pxbPts = document.getElementById('pxb-pts');
+ const pxbLabel = document.getElementById('pxb-label-text');
+ const nextLvl = getProfileLevel(lvl.max);
+ if (pxbFill) pxbFill.style.width = progress + '%';
+ if (pxbPts) pxbPts.textContent = points + ' / ' + lvl.max + ' XP';
+ if (pxbLabel) pxbLabel.textContent = 'التقدم نحو مستوى ' + (lvl.level + 1) + ' · ' + nextLvl.name;
+
+ // ── SVG arc (v5) ──────────────────────────────────────────
+ const arc = document.getElementById('pv4XpArc');
+ if (arc) {
+ const circumference = 2 * Math.PI * 54; // r=54 → ~339.3
+ arc.style.strokeDasharray = circumference;
+ arc.style.strokeDashoffset = circumference * (1 - progress / 100);
+ }
+ const oldRing = document.getElementById('xpRingFill');
+ if (oldRing) {
+ const circ = 2 * Math.PI * 40;
+ oldRing.style.strokeDasharray = circ;
+ oldRing.style.strokeDashoffset = circ * (1 - progress / 100);
+ }
+
+ // ── Streak row ────────────────────────────────────────────
+ const streakRow = document.getElementById('pv4StreakRow');
+ const streakText = document.getElementById('pv4StreakText');
+ if (streakRow && streakText) {
+ if (myStreak > 1) { streakRow.style.display = 'flex'; streakText.textContent = 'سلسلة ' + myStreak + ' يوم متتالي!'; }
+ else streakRow.style.display = 'none';
+ }
+ const ppcStreak = document.getElementById('ppcStreak');
+ if (ppcStreak) {
+ if (myStreak > 1) { ppcStreak.textContent = '🔥 سلسلة ' + myStreak + ' يوم'; ppcStreak.classList.remove('hidden'); }
+ else ppcStreak.classList.add('hidden');
+ }
+}
+
+
+/* ============================================================
+ PERSON MODAL (view another person's profile) - Enhanced
+============================================================ */
+async function openPersonCard(userId, name, area, lat, lng, extraData) {
+ const modal = document.getElementById('personModal');
+ const content = document.getElementById('personModalContent');
+ if (!modal || !content) return;
+
+ // Show loading state
+ content.innerHTML = '⏳ جاري التحميل...
';
+ modal.classList.remove('hidden');
+ history.pushState({ section: currentSection, modal: 'person' }, '', '#' + currentSection);
+
+ const d = lat && userLat ? haversine(userLat, userLng, lat, lng) : null;
+ const avatarText = extraData?.avatar || (name ? name.substring(0, 2).toUpperCase() : '👤');
+ const isOnline = nearbyUsers.some(u => u.userId === userId) || Object.values({}).some(() => false);
+
+ // Try to load full profile
+ let profile = extraData || {};
+ if (userId) {
+ try {
+ const fetched = await fetch('/api/profile/' + userId).then(r => r.json());
+ if (fetched && !fetched.error) profile = fetched;
+ } catch {}
+ }
+
+ const phone = profile.publicPhone || '';
+ const company = profile.company || '';
+ const jobTitle = profile.jobTitle || '';
+ const website = profile.website || '';
+ const bio = profile.bio || '';
+ const whatsapp = profile.whatsapp || '';
+ const telegram = profile.telegram || '';
+
+ content.innerHTML =
+ '' +
+
+ // الرقم المعلن
+ (phone ? '' : '') +
+
+ // نبذة
+ (bio ? '' + escHtml(bio) + '
' : '') +
+
+ // معلومات التواصل
+ '' +
+
+ // أزرار الإجراءات
+ '' +
+ (userId ? '💬 راسله الآن ' : '') +
+ (lat ? '📍 على الخريطة ' : '') +
+ '
';
+}
+
+function closePersonModal(e) {
+ if (!e || e.target === document.getElementById('personModal')) document.getElementById('personModal').classList.add('hidden');
+}
+
+// تحديد موقع شخص على الخريطة
+async function pinpointPersonOnMap(userId, name, fallbackLat, fallbackLng) {
+ closePersonModal();
+ goSection('map');
+
+ setTimeout(async () => {
+ if (!map) return;
+ let lat = fallbackLat, lng = fallbackLng;
+ // Try to get live location
+ if (userId) {
+ try {
+ const loc = await fetch('/api/people/locate/' + userId).then(r => r.json());
+ if (loc.found && loc.lat) { lat = loc.lat; lng = loc.lng; }
+ } catch {}
+ }
+ if (!lat || !lng) { showToast('⚠️ لا يوجد موقع متاح لهذا الشخص', 'error'); return; }
+ map.setView([lat, lng], 16, { animate: true });
+ const avatarText = (name || 'م').substring(0, 2).toUpperCase();
+ const icon = L.divIcon({
+ className: 'custom-marker',
+ html: '' + avatarText + '
',
+ iconSize: [44, 44], iconAnchor: [22, 44]
+ });
+ L.marker([lat, lng], { icon }).addTo(map)
+ .bindPopup(
+ '',
+ { className: 'custom-popup', maxWidth: 200 }
+ ).openPopup();
+ showToast('📍 تم تحديد موقع ' + name + ' على الخريطة', 'success');
+ }, 300);
+}
+
+/* ============================================================
+ DIRECT MESSAGES
+============================================================ */
+function updateDMBadge() {
+ const badge = document.getElementById('dmBadgeNav');
+ if (badge) {
+ if (dmUnreadCount > 0) { badge.textContent = dmUnreadCount > 9 ? '9+' : dmUnreadCount; badge.classList.remove('hidden'); }
+ else badge.classList.add('hidden');
+ }
+ const menuBadge = document.getElementById('msgBadgeMenu');
+ if (menuBadge) {
+ if (dmUnreadCount > 0) { menuBadge.textContent = dmUnreadCount > 9 ? '9+' : dmUnreadCount; menuBadge.classList.remove('hidden'); }
+ else menuBadge.classList.add('hidden');
+ }
+}
+
+async function loadConversations() {
+ const el = document.getElementById('conversationsList');
+ if (!el) return;
+ try {
+ const convs = await fetch('/api/dm/' + myUserId).then(r => r.json());
+ if (!convs.length) { el.innerHTML = emptyState('💬', 'لا توجد محادثات بعد', 'ابدأ بالبحث عن شخص وأرسل له رسالة', 'people'); return; }
+ el.innerHTML = convs.map(c => {
+ const ot = c.otherUser || {};
+ const lastMsg = c.lastMsg;
+ const avatarText = ot.avatar || (ot.name ? ot.name.substring(0, 2).toUpperCase() : '👤');
+ return '' +
+ '
' + avatarText + '
' +
+ '
' +
+ '' +
+ (lastMsg ? '
' + escHtml(lastMsg.text.substring(0, 50)) + (lastMsg.text.length > 50 ? '...' : '') + '
' : '
ابدأ المحادثة
') +
+ '
' +
+ (c.unread > 0 ? '
' + c.unread + '
' : '') +
+ '
';
+ }).join('');
+ } catch { el.innerHTML = emptyState('⚠️', 'خطأ في تحميل الرسائل', 'حاول مجدداً'); }
+}
+
+function openDirectMessage(otherUserId, otherName) {
+ if (!otherUserId) {
+ showToast('⚠️ هذا المستخدم غير مسجّل بعد', 'error');
+ return;
+ }
+ const convId = [myUserId, otherUserId].sort().join('__');
+ activeDMConversation = convId;
+ dmUnreadCount = Math.max(0, dmUnreadCount - 1);
+ updateDMBadge();
+
+ const modal = document.getElementById('dmModal');
+ const content = document.getElementById('dmModalContent');
+ if (!modal || !content) return;
+ const avatarText = (otherName || 'م').substring(0, 2).toUpperCase();
+ content.innerHTML =
+ '' +
+ '' +
+ '' +
+ ' ' +
+ '➤ ' +
+ '
';
+ modal.classList.remove('hidden');
+ history.pushState({ section: currentSection, modal: 'dm' }, '', '#' + currentSection);
+
+ // Load history
+ fetch('/api/dm/' + myUserId + '/' + otherUserId).then(r => r.json()).then(msgs => {
+ const container = document.getElementById('dmMessages');
+ if (!container) return;
+ if (msgs.length) {
+ container.innerHTML = '';
+ msgs.forEach(m => appendDMMessage(m, m.senderId === myUserId));
+ }
+ }).catch(() => {});
+
+ setTimeout(() => { const di = document.getElementById('dmInp'); if (di) di.focus(); }, 200);
+}
+
+async function sendDM(toUserId) {
+ const inp = document.getElementById('dmInp');
+ if (!inp) return;
+ const text = inp.value.trim();
+ if (!text) return;
+ inp.value = '';
+ const name = myName || myProfile?.name || 'أنت';
+ // Use socket for real-time delivery
+ if (socket) {
+ socket.emit('dm_send', { toUserId, fromUserId: myUserId, text, senderName: name });
+ } else {
+ // Fallback to HTTP
+ try {
+ await fetch('/api/dm/' + myUserId + '/' + toUserId, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ text, senderName: name })
+ });
+ const convId = [myUserId, toUserId].sort().join('__');
+ const msg = { id: Date.now() + '', senderId: myUserId, senderName: name, text, time: Date.now(), read: false };
+ appendDMMessage(msg, true);
+ } catch { showToast('❌ خطأ في الإرسال', 'error'); }
+ }
+}
+
+function appendDMMessage(msg, isOwn) {
+ const container = document.getElementById('dmMessages');
+ if (!container) return;
+ const empty = container.querySelector('.chat-empty');
+ if (empty) empty.remove();
+ const div = document.createElement('div');
+ div.className = 'dm-msg ' + (isOwn ? 'dm-msg-own' : 'dm-msg-other');
+ var mediaHtml = '';
+ if (msg.mediaType === 'image' && msg.mediaData) {
+ mediaHtml = '';
+ } else if (msg.mediaType === 'video' && msg.mediaData) {
+ mediaHtml = '
';
+ } else if (msg.mediaType === 'audio' && msg.mediaData) {
+ mediaHtml = '';
+ } else if (msg.mediaType === 'file' && msg.mediaData) {
+ mediaHtml = '';
+ }
+ div.innerHTML =
+ (!isOwn ? '' + escHtml(msg.senderName || 'مستخدم') + '
' : '') +
+ mediaHtml +
+ (msg.text ? '' + escHtml(msg.text) + '
' : '') +
+ '' + timeAgo(msg.time) + (isOwn ? (msg.read ? ' ✓✓' : ' ✓') : '') + '
';
+ container.appendChild(div);
+ container.scrollTop = container.scrollHeight;
+}
+
+function closeDMModal(e) {
+ if (!e || e.target === document.getElementById('dmModal')) {
+ document.getElementById('dmModal')?.classList.add('hidden');
+ activeDMConversation = null;
+ }
+}
+
+/* ============================================================
+ PUBLIC CHAT
+============================================================ */
+function openChatWith(name, area) {
+ chatUser = { name, area };
+ const room = 'dm_' + [myName || 'anon', name].sort().join('_');
+ openChatModal('💬 محادثة مع ' + name, area, room);
+}
+function openPublicChat() {
+ chatRoom = 'public_general';
+ openChatModal('💬 الدردشة العامة', 'مجتمع نبض', chatRoom);
+}
+function openChatModal(title, sub, room) {
+ chatOpen = true; chatRoom = room;
+ if (socket) socket.emit('join_chat', room);
+ let overlay = document.getElementById('chatOverlay');
+ if (!overlay) { overlay = document.createElement('div'); overlay.id = 'chatOverlay'; overlay.className = 'chat-modal-overlay'; document.body.appendChild(overlay); }
+ overlay.innerHTML =
+ '' +
+ '' +
+ '
' +
+ '
' +
+ '
' +
+ '🔴 جاري التسجيل... ' +
+ '0:00 ' +
+ '✕ إلغاء ' +
+ '
' +
+ '
';
+ overlay.classList.remove('hidden'); overlay.style.display = 'flex';
+ fetch('/api/chat/' + room).then(r => r.json()).then(msgs => msgs.forEach(m => appendChatMsg(m, m.sender === myName))).catch(() => {});
+ setTimeout(() => { const ci = document.getElementById('chatInp'); if (ci) ci.focus(); }, 200);
+}
+function closeChat() {
+ if (socket && chatRoom) socket.emit('leave_chat', chatRoom);
+ chatOpen = false;
+ const overlay = document.getElementById('chatOverlay');
+ if (overlay) overlay.style.display = 'none';
+}
+async function sendChat() {
+ const inp = document.getElementById('chatInp');
+ if (!inp) return;
+ const text = inp.value.trim();
+ if (!text) return;
+ inp.value = '';
+ const name = myName || 'أنت';
+ const msg = { id: Date.now() + '', text, sender: name, senderArea: userLocationName, time: Date.now() };
+ appendChatMsg(msg, true);
+ try { await fetch('/api/chat/' + chatRoom, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ text, sender: name, senderArea: userLocationName }) }); } catch {}
+}
+function appendChatMsg(msg, isOwn) {
+ const container = document.getElementById('chatMessages');
+ if (!container) return;
+ const empty = container.querySelector('.chat-empty');
+ if (empty) empty.remove();
+ const div = document.createElement('div');
+ div.className = 'chat-msg ' + (isOwn ? 'chat-msg-in' : 'chat-msg-out');
+ var mediaHtml = '';
+ if (msg.mediaType === 'image' && msg.mediaData) {
+ mediaHtml = '';
+ } else if (msg.mediaType === 'video' && msg.mediaData) {
+ mediaHtml = '
';
+ } else if (msg.mediaType === 'audio' && msg.mediaData) {
+ mediaHtml = '';
+ } else if (msg.mediaType === 'file' && msg.mediaData) {
+ mediaHtml = '';
+ }
+ div.innerHTML =
+ (!isOwn ? '' + escHtml(msg.sender || 'مستخدم') + (msg.senderArea ? ' • ' + escHtml(msg.senderArea) : '') + '
' : '') +
+ mediaHtml +
+ (msg.text ? '' + escHtml(msg.text) + '
' : '') +
+ '' + timeAgo(msg.time) + '
';
+ container.appendChild(div);
+ container.scrollTop = container.scrollHeight;
+}
+
+/* ============================================================
+ REPORT
+============================================================ */
+function selectRType(type, btn) {
+ selectedReportType = type;
+ document.querySelectorAll('.rtype').forEach(b => b.classList.remove('active-rtype'));
+ btn.classList.add('active-rtype');
+}
+async function submitReport() {
+ const msg = document.getElementById('reportMsg').value.trim();
+ const area = document.getElementById('reportAreaInput').value.trim() || userLocationName;
+ const lat = document.getElementById('reportLat').value || userLat;
+ const lng = document.getElementById('reportLng').value || userLng;
+ if (!msg) return showToast('❌ اكتب تقريرك', 'error');
+ if (msg.length < 5) return showToast('❌ وصف أكثر تفصيلاً من فضلك', 'error');
+ const btn = document.querySelector('#sec-report .btn-report');
+ if (btn) { btn.disabled = true; btn.textContent = '⏳ جاري الإرسال...'; }
+ try {
+ // Upload photo first if present
+ let imageId = null;
+ const photoInput = document.getElementById('reportPhoto');
+ if (photoInput && photoInput.files && photoInput.files[0]) {
+ await new Promise(function(resolve) { uploadPhoto('reportPhoto', function(id) { imageId = id; resolve(); }); });
+ }
+ await fetch('/api/alerts', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ type: selectedReportType, msg, area, lat: Number(lat) || null, lng: Number(lng) || null, imageId }) });
+ document.getElementById('reportMsg').value = '';
+ document.getElementById('reportAreaInput').value = '';
+ document.getElementById('reportLat').value = '';
+ document.getElementById('reportLng').value = '';
+ if (photoInput) photoInput.value = '';
+ const rpv = document.getElementById('reportPhotoPreview'); if (rpv) { rpv.classList.add('hidden'); rpv.innerHTML = ''; }
+ const rpn = document.getElementById('reportPhotoName'); if (rpn) rpn.textContent = '';
+ // Update profile reports count
+ if (myProfile) { myProfile.reports = (myProfile.reports || 0) + 1; localStorage.setItem('nabdh_profile', JSON.stringify(myProfile)); updateProfileUI(); }
+ showToast('🚨 تم إرسال تقريرك! شكراً - أنت تساعد مجتمعك', 'success');
+ setTimeout(() => goSection('map'), 1800);
+ } catch { showToast('❌ حدث خطأ', 'error'); }
+ finally { if (btn) { btn.disabled = false; btn.textContent = '🚨 أرسل التقرير الآن'; } }
+}
+
+/* ============================================================
+ NAVIGATION
+============================================================ */
+function goSection(name, pushHistory) {
+ // تسجيل القسم السابق في المكدس
+ if (pushHistory !== false && currentSection && currentSection !== name) {
+ _sectionHistory.push(currentSection);
+ if (_sectionHistory.length > 30) _sectionHistory.shift();
+ }
+ // تحديث History API لمنع الخروج من التطبيق
+ if (!_historyPushing) {
+ _historyPushing = true;
+ history.pushState({ section: name }, '', '#' + name);
+ _historyPushing = false;
+ }
+ document.querySelectorAll('.section').forEach(s => s.classList.remove('active-sec'));
+ document.querySelectorAll('.bnav').forEach(b => b.classList.remove('active-bnav'));
+ const sec = document.getElementById('sec-' + name);
+ if (sec) sec.classList.add('active-sec');
+ const bnav = document.getElementById('bnav-' + name);
+ if (bnav) bnav.classList.add('active-bnav');
+ const mc = document.getElementById('mainContent'); if (mc) mc.scrollTop = 0;
+ currentSection = name;
+ if (name === 'leaderboard') { openLeaderboard(); return; }
+ if (name === 'map') { if (!map) initMap(); else setTimeout(() => map.invalidateSize(), 150); renderMapAlerts(); updateMapCounts(); loadNearbyUsers(); loadNearbyPeople(); }
+ if (name === 'medicine') renderMedicines();
+ if (name === 'voice') renderVoice();
+ if (name === 'skills') renderSkills();
+ if (name === 'exchange') { renderExchange(); loadExchange(); }
+ if (name === 'market') { renderMarket(); }
+ if (name === 'home') { renderHomeAlerts(); renderHomeMarket(); }
+ if (name === 'profile') { updateProfileUI(); syncProfileWithServer(); }
+ if (name === 'people') { loadNearbyPeople(); }
+ if (name === 'messages') { loadConversations(); dmUnreadCount = 0; updateDMBadge(); }
+ if (name === 'blood') { searchBlood(); }
+ if (name === 'power') { loadPowerSchedules(); }
+ if (name === 'prayer') { refreshPrayerTimes(); }
+ if (name === 'hospitals') { loadHospitals(); }
+ if (name === 'news') { loadNews(); }
+ if (name === 'rides') { loadRides(); }
+ if (name === 'weather') { refreshWeather(); }
+ if (name === 'water') { loadWaterReports(); }
+ if (name === 'study') { loadStudyGroups(); }
+ if (name === 'hood') { loadHoodGroups(); }
+ if (name === 'help') { loadHelpRequests(); }
+ if (name === 'polls') { loadPolls(); }
+ if (name === 'dashboard') { loadDashboard(); }
+ requestNotifPermission();
+ // Update active menu item highlight
+ _updateMenuActiveItem(name);
+}
+
+/* ── Update active menu item in side menu ─────────────── */
+function _updateMenuActiveItem(name) {
+ // Map section names to their menu item onclick handlers
+ document.querySelectorAll('#sideMenu .menu-item').forEach(item => {
+ const onclick = item.getAttribute('onclick') || '';
+ // Extract section name from onclick="goSection('X');..."
+ const m = onclick.match(/goSection\(['"]([^'"]+)['"]\)/);
+ if (m) {
+ if (m[1] === name) {
+ item.classList.add('menu-item-active');
+ } else {
+ item.classList.remove('menu-item-active');
+ }
+ }
+ });
+}
+
+/* ============================================================
+ زر الرجوع - Back Button Handler (Handled below)
+============================================================ */
+
+// تهيئة History عند بدء التطبيق
+(function initHistory() {
+ const hash = window.location.hash.replace('#', '');
+ const validSections = ['home','map','report','people','messages','profile','blood','power','prayer','medicine','voice','skills','exchange','market','hospitals','news','rides','weather','water','study','hood','help','polls','dashboard'];
+ const startSection = validSections.includes(hash) ? hash : 'home';
+ history.replaceState({ section: startSection }, '', '#' + startSection);
+})();
+
+function toggleMenu() {
+ const menu = document.getElementById('sideMenu');
+ const overlay = document.getElementById('menuOverlay');
+ const isHidden = menu.classList.contains('hidden');
+ menu.classList.toggle('hidden');
+ overlay.classList.toggle('hidden');
+ // إذا فتحنا القائمة، أضف state للتاريخ
+ if (isHidden) {
+ history.pushState({ section: currentSection, menuOpen: true }, '', '#' + currentSection);
+ }
+}
+
+/* ============================================================
+ UTILS
+============================================================ */
+function dist(item) {
+ if (!userLat || !item || !item.lat || !item.lng) return null;
+ return haversine(userLat, userLng, item.lat, item.lng);
+}
+function haversine(la1, lo1, la2, lo2) {
+ const R = 6371, r = d => d * Math.PI / 180;
+ const a = Math.sin(r(la2 - la1) / 2) ** 2 + Math.cos(r(la1)) * Math.cos(r(la2)) * Math.sin(r(lo2 - lo1) / 2) ** 2;
+ return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
+}
+function showProfileQR_legacy() {
+ var name = (myProfile && myProfile.name) || myName || 'مستخدم نبض';
+ var pubPhone = (myProfile && myProfile.publicPhone) || '';
+ var msg = '👤 ' + name;
+ if (pubPhone) msg += ' · 📞 ' + pubPhone;
+ msg += '\n💓 تطبيق نبض';
+ showToast(msg, 'success');
+}
+
+function emptyState(icon, title, sub, action) {
+ return '' + icon + '
' + title + '
' + sub + '
' + (action ? '
ابدأ الآن ← ' : '') + '
';
+}
+function escHtml(s) {
+ if (!s) return '';
+ return String(s).replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"');
+}
+function escJs(s) { return String(s || '').replace(/\\/g, '\\\\').replace(/'/g, "\\'").replace(/\n/g, ' '); }
+function setEl(id, val) { const e = document.getElementById(id); if (e) e.textContent = val; }
+function animateCount(id, target) {
+ const el = document.getElementById(id);
+ if (!el) return;
+ const start = parseInt(el.textContent.replace(/[^\d]/g, '')) || 0;
+ if (start === target) return;
+ const t0 = performance.now();
+ (function update(now) {
+ const p = Math.min((now - t0) / 800, 1), e = 1 - Math.pow(1 - p, 3);
+ el.textContent = Math.round(start + (target - start) * e).toLocaleString('ar');
+ if (p < 1) requestAnimationFrame(update);
+ })(t0);
+}
+function timeAgo(ts) {
+ if (!ts) return '—';
+ var t = typeof ts === 'number' ? ts : new Date(ts).getTime();
+ const s = Math.floor((Date.now() - t) / 1000), m = Math.floor(s / 60), h = Math.floor(s / 3600), d = Math.floor(s / 86400);
+ if (s < 60) return 'الآن';
+ if (m < 60) return 'منذ ' + m + ' دقيقة';
+ if (h < 24) return 'منذ ' + h + ' ساعة';
+ if (d < 30) return 'منذ ' + d + ' يوم';
+ return new Date(t).toLocaleDateString('ar-SA');
+}
+function showToast(msg, type) {
+ const t = document.getElementById('toast');
+ if (!t) return;
+ t.textContent = msg; t.className = 'toast ' + (type || ''); t.classList.remove('hidden');
+ setTimeout(() => t.classList.add('hidden'), 3500);
+}
+function showNotif(msg) {
+ const el = document.getElementById('notifBadge');
+ if (!el) return;
+ el.textContent = msg; el.classList.remove('hidden');
+ setTimeout(() => el.classList.add('hidden'), 5000);
+}
+
+
+/* ============================================================
+ SHARE APP - مشاركة رابط التطبيق
+============================================================ */
+
+function getAppUrl() {
+ return window.location.origin;
+}
+
+function shareApp() {
+ try {
+ var modal = document.getElementById('shareAppModal');
+ if (!modal) { alert('لم يتم العثور على نافذة المشاركة'); return; }
+
+ var url = getAppUrl();
+
+ // fill URL box
+ var inp = document.getElementById('shareAppUrlInput');
+ if (inp) inp.value = url;
+
+ // stats
+ try {
+ var su = document.getElementById('shareStatUsers');
+ var sr = document.getElementById('shareStatReports');
+ var sc = document.getElementById('shareStatCities');
+ if (su) su.textContent = (data && data.stats && data.stats.users > 0) ? data.stats.users : '—';
+ if (sr) sr.textContent = (data && data.stats && data.stats.reports > 0) ? data.stats.reports : '—';
+ if (sc) sc.textContent = (data && data.stats && data.stats.cities > 0) ? data.stats.cities : '—';
+ } catch(e2) {}
+
+ // QR code
+ try {
+ var canvas = document.getElementById('shareQrCanvas');
+ if (canvas && typeof QRCode !== 'undefined') {
+ QRCode.toCanvas(canvas, url, { width: 180, margin: 2, color: { dark: '#1abc9c', light: '#1a1a2e' } });
+ }
+ } catch(e3) {}
+
+ // set href on share links directly (bypasses popup blocker)
+ try {
+ var shareText = encodeURIComponent(
+ '💓 تطبيق نبض - صوت المدينة الحي' + '\n' +
+ '📍 أخبار، أسعار، خرائط، رسائل مباشرة' + '\n' +
+ '🔗 ' + url + '\n' +
+ '#نبض_المدينة #السودان'
+ );
+ var encUrl = encodeURIComponent(url);
+
+ var waLink = document.getElementById('shareWhatsappLink');
+ var tgLink = document.getElementById('shareTelegramLink');
+ var twLink = document.getElementById('shareTwitterLink');
+
+ if (waLink) waLink.href = 'https://wa.me/?text=' + shareText;
+ if (tgLink) tgLink.href = 'https://t.me/share/url?url=' + encUrl + '&text=' + encodeURIComponent('💓 تطبيق نبض - صوت المدينة الحي');
+ if (twLink) twLink.href = 'https://twitter.com/intent/tweet?url=' + encUrl + '&text=' + encodeURIComponent('💓 تطبيق #نبض - صوت المدينة الحي 🇸🇩');
+ } catch(e4) {}
+
+ // native share button label
+ try {
+ var lbl = document.getElementById('shareNativeLabel');
+ var ico = document.querySelector('#shareNativeBtn .sbc-icon');
+ if (lbl && ico) {
+ if (navigator.share) { lbl.textContent = 'مشاركة'; ico.textContent = '🔗'; }
+ else { lbl.textContent = 'نسخ الرابط'; ico.textContent = '📋'; }
+ }
+ } catch(e5) {}
+
+ modal.classList.remove('hidden');
+ document.body.style.overflow = 'hidden';
+ } catch(err) {
+ console.error('shareApp error:', err);
+ }
+}
+
+function closeShareAppModal(e) {
+ if (e && e.target !== e.currentTarget) return;
+ var modal = document.getElementById('shareAppModal');
+ if (modal) modal.classList.add('hidden');
+ document.body.style.overflow = '';
+}
+
+function copyAppUrl() {
+ var url = getAppUrl();
+ var btn = document.querySelector('.share-url-copy-btn');
+ function onCopied() {
+ showToast('✅ تم نسخ الرابط!', 'success');
+ if (btn) { btn.textContent = '✅ تم!'; setTimeout(function(){ btn.textContent = '📋 نسخ'; }, 2000); }
+ }
+ if (navigator.clipboard && navigator.clipboard.writeText) {
+ navigator.clipboard.writeText(url).then(onCopied).catch(function() {
+ fallbackCopy(url, onCopied);
+ });
+ } else {
+ fallbackCopy(url, onCopied);
+ }
+}
+
+function fallbackCopy(text, cb) {
+ var el = document.getElementById('shareAppUrlInput');
+ if (!el) {
+ el = document.createElement('input');
+ el.style.position = 'fixed'; el.style.top = '-9999px';
+ document.body.appendChild(el);
+ }
+ el.value = text;
+ el.select();
+ el.setSelectionRange(0, 99999);
+ try { document.execCommand('copy'); if (cb) cb(); } catch(e) {}
+}
+
+function _buildShareText() {
+ var url = getAppUrl();
+ var users = (data && data.stats && data.stats.users > 0) ? ('\n\u{1F465} ' + data.stats.users + ' \u0645\u0633\u062A\u062E\u062F\u0645 \u0646\u0634\u0637 \u0627\u0644\u0622\u0646') : '';
+ return '\u{1F493} \u062A\u0637\u0628\u064A\u0642 \u0646\u0628\u0636 - \u0635\u0648\u062A \u0627\u0644\u0645\u062F\u064A\u0646\u0629 \u0627\u0644\u062D\u064A' + users + '\n\u{1F4CD} \u0623\u062E\u0628\u0627\u0631\u060C \u0623\u0633\u0639\u0627\u0631\u060C \u062E\u0631\u0627\u0626\u0637\u060C \u0631\u0633\u0627\u0626\u0644 \u0645\u0628\u0627\u0634\u0631\u0629\n\u{1F517} ' + url + '\n#\u0646\u0628\u0636_\u0627\u0644\u0645\u062F\u064A\u0646\u0629 #\u0627\u0644\u0633\u0648\u062F\u0627\u0646';
+}
+
+function shareViaWhatsApp() {
+ var url = 'https://wa.me/?text=' + encodeURIComponent(_buildShareText());
+ window.open(url, '_blank');
+}
+
+function shareViaTelegram() {
+ var appUrl = encodeURIComponent(getAppUrl());
+ var txt = encodeURIComponent('\u{1F493} \u062A\u0637\u0628\u064A\u0642 \u0646\u0628\u0636 - \u0635\u0648\u062A \u0627\u0644\u0645\u062F\u064A\u0646\u0629 \u0627\u0644\u062D\u064A\n\u0623\u062E\u0628\u0627\u0631 \u0648\u0623\u0633\u0639\u0627\u0631 \u0648\u062E\u0631\u0627\u0626\u0637 \u0645\u0628\u0627\u0634\u0631\u0629');
+ window.open('https://t.me/share/url?url=' + appUrl + '&text=' + txt, '_blank');
+}
+
+function shareViaTwitter() {
+ var appUrl = encodeURIComponent(getAppUrl());
+ var txt = encodeURIComponent('\u{1F493} \u062A\u0637\u0628\u064A\u0642 #\u0646\u0628\u0636 - \u0635\u0648\u062A \u0627\u0644\u0645\u062F\u064A\u0646\u0629 \u0627\u0644\u062D\u064A\n\u0623\u062E\u0628\u0627\u0631\u060C \u0623\u0633\u0639\u0627\u0631\u060C \u062E\u0631\u0627\u0626\u0637 \u0645\u0628\u0627\u0634\u0631\u0629 \u{1F1F8}\u{1F1E9}');
+ window.open('https://twitter.com/intent/tweet?url=' + appUrl + '&text=' + txt, '_blank');
+}
+
+function shareViaNative() {
+ var url = getAppUrl();
+ if (navigator.share) {
+ navigator.share({
+ title: 'تطبيق نبض',
+ text: 'أخبار وأسعار وخرائط مباشرة من مجتمعك',
+ url: url
+ }).catch(function(err) {
+ if (err.name !== 'AbortError') copyAppUrl();
+ });
+ } else {
+ copyAppUrl();
+ }
+}
+
+/* ============================================================
+ 🩸 BLOOD BANK - بنك الدم
+ ============================================================ */
+var _selectedBloodType = '';
+var _selectedDonorType = '';
+var _selectedRequestType = '';
+
+function switchBloodTab(tab, btn) {
+ document.querySelectorAll('.blood-tab').forEach(function(b){ b.classList.remove('active-blood-tab'); });
+ btn.classList.add('active-blood-tab');
+ var tabs = ['search','request','donate'];
+ tabs.forEach(function(t){
+ var el = document.getElementById('blood-tab-' + t);
+ if (el) el.classList.toggle('hidden', t !== tab);
+ });
+ if (tab === 'search') searchBlood();
+ if (tab === 'request') loadBloodRequests();
+}
+
+function selectBloodType(btn, type) {
+ document.querySelectorAll('#blood-tab-search .btype-btn').forEach(function(b){ b.classList.remove('active-btype'); });
+ btn.classList.add('active-btype');
+ _selectedBloodType = type;
+}
+
+function selectDonorType(btn, type) {
+ document.querySelectorAll('#blood-tab-donate .btype-btn').forEach(function(b){ b.classList.remove('active-btype'); });
+ btn.classList.add('active-btype');
+ _selectedDonorType = type;
+}
+
+function selectRequestType(btn, type) {
+ document.querySelectorAll('#blood-tab-request .btype-btn').forEach(function(b){ b.classList.remove('active-btype'); });
+ btn.classList.add('active-btype');
+ _selectedRequestType = type;
+}
+
+function searchBlood() {
+ var areaEl = document.getElementById('bloodSearchArea');
+ var area = areaEl ? areaEl.value.trim() : '';
+ var url = '/api/blood/donors?';
+ if (_selectedBloodType) url += 'type=' + encodeURIComponent(_selectedBloodType) + '&';
+ if (area) url += 'area=' + encodeURIComponent(area);
+ var list = document.getElementById('bloodSearchResults');
+ if (list) list.innerHTML = '⏳ جاري البحث...
';
+ fetch(url)
+ .then(function(r){ return r.json(); })
+ .then(function(donors) {
+ if (!list) return;
+ if (!Array.isArray(donors) || !donors.length) {
+ list.innerHTML = '🩸 لا يوجد متبرعون مسجلون' +
+ (_selectedBloodType ? ' بفصيلة ' + _selectedBloodType + ' ' : '') +
+ ' حالياًكن أول من يسجل! انتقل لتبويب "سجّل كمتبرع"
';
+ return;
+ }
+ list.innerHTML = '✅ وُجد ' + donors.length + ' متبرع
' +
+ donors.map(function(d) {
+ return '' +
+ '
' + d.bloodType + '
' +
+ '
' +
+ '
👤 ' + escHtml(d.name || 'متبرع مجهول') + '
' +
+ '
📍 ' + escHtml(d.area || '—') + '
' +
+ (d.contact ? '
📞 ' + escHtml(d.contact) + ' ' : '') +
+ '
' +
+ '
';
+ }).join('');
+ })
+ .catch(function(e) {
+ console.warn('searchBlood err', e);
+ if (list) list.innerHTML = '⚠️ خطأ في الاتصال بالسيرفر
';
+ });
+}
+
+function submitDonor() {
+ if (!_selectedDonorType) { showToast('اختر فصيلة دمك أولاً', 'warning'); return; }
+ var phoneEl = document.getElementById('donorPhone');
+ var phone = phoneEl ? phoneEl.value.trim() : '';
+ if (!phone) { showToast('رقم الهاتف مطلوب للتواصل', 'warning'); return; }
+ var area = (document.getElementById('donorArea') || {}).value || '';
+ var lat = (document.getElementById('donorLat') || {}).value || '';
+ var lng = (document.getElementById('donorLng') || {}).value || '';
+ var name = (document.getElementById('donorName') || {}).value || '';
+ var btn = document.querySelector('#blood-tab-donate .btn-submit');
+ if (btn) { btn.disabled = true; btn.textContent = '⏳ جاري التسجيل...'; }
+ fetch('/api/blood/donor', {
+ method: 'POST',
+ headers: {'Content-Type':'application/json'},
+ body: JSON.stringify({
+ bloodType: _selectedDonorType,
+ name: name.trim(),
+ contact: phone,
+ area: area.trim(),
+ lat: lat ? parseFloat(lat) : (userLat || null),
+ lng: lng ? parseFloat(lng) : (userLng || null)
+ })
+ }).then(function(r){ return r.json(); }).then(function(data) {
+ if (btn) { btn.disabled = false; btn.textContent = '🩸 سجّل كمتبرع'; }
+ if (data.success) {
+ showToast('✅ تم تسجيلك كمتبرع! جزاك الله خيراً', 'success');
+ if (phoneEl) phoneEl.value = '';
+ var nameEl = document.getElementById('donorName');
+ if (nameEl) nameEl.value = '';
+ _selectedDonorType = '';
+ document.querySelectorAll('#blood-tab-donate .btype-btn').forEach(function(b){ b.classList.remove('active-btype'); });
+ } else {
+ showToast(data.error || 'خطأ في التسجيل', 'error');
+ }
+ }).catch(function() {
+ if (btn) { btn.disabled = false; btn.textContent = '🩸 سجّل كمتبرع'; }
+ showToast('خطأ في الاتصال بالسيرفر', 'error');
+ });
+}
+
+function submitBloodRequest() {
+ if (!_selectedRequestType) { showToast('اختر فصيلة الدم المطلوبة', 'warning'); return; }
+ var contactEl = document.getElementById('reqContact');
+ var contact = contactEl ? contactEl.value.trim() : '';
+ if (!contact) { showToast('رقم التواصل مطلوب', 'warning'); return; }
+ var btn = document.querySelector('#blood-tab-request .btn-submit');
+ if (btn) { btn.disabled = true; btn.textContent = '⏳ جاري الإرسال...'; }
+ var body = {
+ bloodType: _selectedRequestType,
+ patientName: (document.getElementById('reqPatientName') || {}).value || '',
+ hospital: (document.getElementById('reqHospital') || {}).value || '',
+ contact: contact,
+ area: (document.getElementById('reqArea') || {}).value || '',
+ urgent: document.getElementById('reqUrgent') ? document.getElementById('reqUrgent').checked : true,
+ lat: userLat || null,
+ lng: userLng || null
+ };
+ fetch('/api/blood/request', {
+ method:'POST',
+ headers:{'Content-Type':'application/json'},
+ body: JSON.stringify(body)
+ }).then(function(r){ return r.json(); }).then(function(data) {
+ if (btn) { btn.disabled = false; btn.textContent = '🆘 أرسل طلب الدم الآن'; }
+ if (data.success) {
+ showToast('🆘 تم إرسال الطلب! سيتواصل معك المتبرعون قريباً', 'success');
+ if (contactEl) contactEl.value = '';
+ ['reqPatientName','reqHospital','reqArea'].forEach(function(id){
+ var el = document.getElementById(id); if (el) el.value = '';
+ });
+ _selectedRequestType = '';
+ document.querySelectorAll('#blood-tab-request .btype-btn').forEach(function(b){ b.classList.remove('active-btype'); });
+ loadBloodRequests();
+ } else { showToast(data.error || 'خطأ في الإرسال', 'error'); }
+ }).catch(function() {
+ if (btn) { btn.disabled = false; btn.textContent = '🆘 أرسل طلب الدم الآن'; }
+ showToast('خطأ في الاتصال', 'error');
+ });
+}
+
+function loadBloodRequests() {
+ fetch('/api/blood/requests')
+ .then(function(r){ return r.json(); })
+ .then(function(requests) {
+ var list = document.getElementById('bloodRequestsList');
+ if (!list) return;
+ var active = Array.isArray(requests) ? requests.filter(function(r){ return !r.fulfilled; }) : [];
+ if (!active.length) {
+ list.innerHTML = 'لا توجد طلبات دم نشطة حالياً
';
+ return;
+ }
+ list.innerHTML = '📋 الطلبات النشطة (' + active.length + ') ' +
+ active.slice(0,10).map(function(r) {
+ return '' +
+ '
' + r.bloodType + '
' +
+ '
' +
+ (r.urgent ? '
🆘 عاجل جداً ' : '') +
+ '
🏥 ' + escHtml(r.hospital || r.patientName || 'مريض') + '
' +
+ (r.area ? '
📍 ' + escHtml(r.area) + '
' : '') +
+ '
📞 ' + escHtml(r.contact) + ' ' +
+ '
' +
+ '
';
+ }).join('');
+ })
+ .catch(function(e){ console.warn('blood requests error', e); });
+}
+
+/* ============================================================
+ ⚡ ELECTRICITY SCHEDULE - جدول الكهرباء
+ ============================================================ */
+function loadPowerSchedules() {
+ var url = '/api/power';
+ if (userLat && userLng) url += '?lat=' + userLat + '&lng=' + userLng;
+ fetch(url)
+ .then(function(r){ return r.json(); })
+ .then(function(schedules) {
+ var list = document.getElementById('powerList');
+ if (!list) return;
+ if (!Array.isArray(schedules) || !schedules.length) {
+ list.innerHTML = 'لا توجد تقارير انقطاع بعدكن أول من يشارك جدول حيّك!
';
+ return;
+ }
+ list.innerHTML = schedules.map(function(s) {
+ var statusIcon = s.status === 'confirmed' ? '🔴 مؤكد' : s.status === 'unconfirmed' ? '🟡 غير مؤكد' : '⚡ مقطوع';
+ var statusCls = s.denies > s.confirms ? 'power-off' : 'power-on';
+ return '' +
+ '
' +
+ '📍 ' + escHtml(s.area || '—') + ' ' +
+ '⏰ ' + (s.cutStart || '—') + (s.cutEnd && s.cutEnd !== 'غير محدد' ? ' ← ' + s.cutEnd : '') + ' ' +
+ '
' +
+ '
' +
+ '' + statusIcon + ' ' +
+ (s.district && s.district !== s.area ? '📌 ' + escHtml(s.district) + ' ' : '') +
+ '
' +
+ '
👍 ' + (s.confirms || 0) + ' مؤكد 👎 ' + (s.denies || 0) + ' غير صحيح' +
+ ' ✅ صحيح ' +
+ ' ❌ خطأ ' +
+ '
' +
+ '
';
+ }).join('');
+ })
+ .catch(function(e){ console.warn('power load error', e); });
+}
+
+function votePower(vote) {
+ var area = userLocationName || 'منطقتي';
+ var icon = document.getElementById('powerIcon');
+ var label = document.getElementById('powerStatusLabel');
+ if (vote === 'off') {
+ if (icon) icon.textContent = '🔴';
+ if (label) label.textContent = '❌ الكهرباء مقطوعة في ' + area;
+ fetch('/api/power', {
+ method: 'POST',
+ headers: {'Content-Type':'application/json'},
+ body: JSON.stringify({
+ area: area, district: area,
+ cutStart: new Date().toTimeString().slice(0,5),
+ cutEnd: 'غير محدد',
+ lat: userLat || null,
+ lng: userLng || null
+ })
+ }).then(function(){ showToast('❌ تم تسجيل الانقطاع، شكراً!', 'info'); loadPowerSchedules(); })
+ .catch(function(){ showToast('خطأ في الإرسال', 'error'); });
+ } else {
+ if (icon) icon.textContent = '✅';
+ if (label) label.textContent = '✅ الكهرباء موجودة في ' + area;
+ showToast('✅ شكراً! سجّلنا أن الكهرباء موجودة', 'info');
+ }
+}
+
+function votePowerItem(id, vote) {
+ fetch('/api/power/' + id + '/vote', {
+ method: 'POST',
+ headers: {'Content-Type':'application/json'},
+ body: JSON.stringify({ vote: vote })
+ }).then(function(r){ return r.json(); })
+ .then(function(){ loadPowerSchedules(); })
+ .catch(function(){ showToast('خطأ في التصويت', 'error'); });
+}
+
+function submitPowerSchedule() {
+ var areaEl = document.getElementById('powerArea');
+ var area = areaEl ? areaEl.value.trim() : '';
+ if (!area) { showToast('أدخل اسم الحي أو المنطقة', 'warning'); return; }
+ var cutTimeEl = document.getElementById('powerCutTime');
+ var cutTime = cutTimeEl ? cutTimeEl.value : '';
+ if (!cutTime) { showToast('حدد وقت الانقطاع', 'warning'); return; }
+ var lat = (document.getElementById('powerLat') || {}).value || '';
+ var lng = (document.getElementById('powerLng') || {}).value || '';
+ var duration = parseInt((document.getElementById('powerDuration') || {}).value) || 0;
+ var cutEnd = '';
+ if (cutTime && duration) {
+ var parts = cutTime.split(':');
+ var h = (parseInt(parts[0]) + duration) % 24;
+ cutEnd = String(h).padStart(2,'0') + ':' + parts[1];
+ }
+ var btn = document.querySelector('#sec-power .btn-submit');
+ if (btn) { btn.disabled = true; btn.textContent = '⏳ جاري الإرسال...'; }
+ fetch('/api/power', {
+ method: 'POST',
+ headers: {'Content-Type':'application/json'},
+ body: JSON.stringify({
+ area: area, district: area,
+ cutStart: cutTime,
+ cutEnd: cutEnd || 'غير محدد',
+ lat: lat ? parseFloat(lat) : (userLat || null),
+ lng: lng ? parseFloat(lng) : (userLng || null)
+ })
+ }).then(function(r){ return r.json(); }).then(function(data) {
+ if (btn) { btn.disabled = false; btn.textContent = '⚡ شارك الجدول'; }
+ if (data.success) {
+ showToast('✅ تم مشاركة جدول الكهرباء!', 'success');
+ ['powerArea','powerCutTime','powerDuration','powerNotes'].forEach(function(id){
+ var el = document.getElementById(id); if (el) el.value = '';
+ });
+ loadPowerSchedules();
+ } else { showToast(data.error || 'خطأ', 'error'); }
+ }).catch(function() {
+ if (btn) { btn.disabled = false; btn.textContent = '⚡ شارك الجدول'; }
+ showToast('خطأ في الاتصال', 'error');
+ });
+}
+
+/* ============================================================
+ 🕌 PRAYER TIMES - أوقات الصلاة
+ ============================================================ */
+var _prayerCountdownInterval = null;
+var _prayerTimesLoaded = false;
+
+function loadPrayerTimes(lat, lng) {
+ var method = (document.getElementById('prayerMethod') || {}).value || '4';
+ var tzOffset = -(new Date().getTimezoneOffset() / 60);
+ var useLat = lat || userLat || 15.5007;
+ var useLng = lng || userLng || 32.5599;
+ var url = '/api/prayer?lat=' + useLat + '&lng=' + useLng + '&method=' + method + '&tz=' + tzOffset;
+ var locEl = document.getElementById('prayerLocText');
+ if (locEl) locEl.textContent = '⏳ جاري تحديد الأوقات...';
+ fetch(url)
+ .then(function(r){ return r.json(); })
+ .then(function(data) {
+ if (!data.success) { if (locEl) locEl.textContent = '⚠️ خطأ في الحساب'; return; }
+ var times = data.times;
+ var keys = ['fajr','sunrise','dhuhr','asr','maghrib','isha'];
+ keys.forEach(function(k) {
+ var el = document.getElementById('pt-' + k);
+ if (el && times[k]) el.textContent = times[k];
+ });
+ _prayerTimesLoaded = true;
+ startPrayerCountdown(times);
+ if (locEl) {
+ var cityName = data.city || (userLocationName && userLocationName !== 'غير محدد' ? userLocationName : '');
+ locEl.textContent = '📍 ' + (cityName || ('خط العرض: ' + parseFloat(useLat).toFixed(2)));
+ }
+ })
+ .catch(function(e) {
+ console.warn('prayer load error', e);
+ if (locEl) locEl.textContent = '⚠️ تعذّر تحميل الأوقات';
+ });
+}
+
+function refreshPrayerTimes() {
+ if (userLat && userLng) {
+ loadPrayerTimes(userLat, userLng);
+ } else if (navigator.geolocation) {
+ var locEl = document.getElementById('prayerLocText');
+ if (locEl) locEl.textContent = '📡 جاري تحديد موقعك...';
+ navigator.geolocation.getCurrentPosition(
+ function(pos) { loadPrayerTimes(pos.coords.latitude, pos.coords.longitude); },
+ function() { loadPrayerTimes(15.5007, 32.5599); } // Default: Khartoum
+ );
+ } else {
+ loadPrayerTimes(15.5007, 32.5599);
+ }
+}
+
+function startPrayerCountdown(times) {
+ if (_prayerCountdownInterval) clearInterval(_prayerCountdownInterval);
+ var prayers = [
+ { name:'الفجر', key:'fajr' },
+ { name:'الشروق', key:'sunrise' },
+ { name:'الظهر', key:'dhuhr' },
+ { name:'العصر', key:'asr' },
+ { name:'المغرب', key:'maghrib' },
+ { name:'العشاء', key:'isha' }
+ ];
+
+ function parseTime(str) {
+ if (!str || str === '—') return null;
+ var m = str.match(/(\d+):(\d+)\s*(AM|PM)?/i);
+ if (!m) return null;
+ var h = parseInt(m[1]), mn = parseInt(m[2]);
+ if (m[3]) {
+ if (m[3].toUpperCase() === 'PM' && h < 12) h += 12;
+ if (m[3].toUpperCase() === 'AM' && h === 12) h = 0;
+ }
+ var now = new Date();
+ return new Date(now.getFullYear(), now.getMonth(), now.getDate(), h, mn, 0);
+ }
+
+ function tick() {
+ var now = new Date();
+ var next = null, nextName = '';
+ for (var i = 0; i < prayers.length; i++) {
+ var t = parseTime(times[prayers[i].key]);
+ if (t && t > now) { next = t; nextName = prayers[i].name; break; }
+ }
+ if (!next) {
+ var tf = parseTime(times['fajr']);
+ if (tf) { tf.setDate(tf.getDate() + 1); next = tf; nextName = 'الفجر (غداً)'; }
+ }
+ // Update active prayer card
+ prayers.forEach(function(p) {
+ var card = document.querySelector('.pc-' + p.key);
+ if (card) card.classList.toggle('prayer-active', p.name === nextName.replace(' (غداً)',''));
+ });
+ if (next) {
+ var diff = Math.floor((next - now) / 1000);
+ var hh = Math.floor(diff / 3600), mm = Math.floor((diff % 3600) / 60), ss = diff % 60;
+ var nnEl = document.getElementById('nextPrayerName');
+ var cdEl = document.getElementById('prayerCountdown');
+ if (nnEl) nnEl.textContent = nextName;
+ if (cdEl) cdEl.textContent = String(hh).padStart(2,'0') + ':' + String(mm).padStart(2,'0') + ':' + String(ss).padStart(2,'0');
+ }
+ // Hijri date
+ try {
+ var hijri = new Intl.DateTimeFormat('ar-SA-u-ca-islamic', { day:'numeric', month:'long', year:'numeric' }).format(now);
+ var hEl = document.getElementById('prayerHijri');
+ if (hEl && hijri) hEl.textContent = hijri;
+ } catch(e){}
+ }
+
+ tick();
+ _prayerCountdownInterval = setInterval(tick, 1000);
+}
+
+/* ============================================================
+ 📷 PHOTO UPLOAD - رفع الصور
+ ============================================================ */
+function previewPhoto(inputId, previewId) {
+ var input = document.getElementById(inputId);
+ var preview = document.getElementById(previewId);
+ if (!input || !input.files || !input.files[0]) return;
+ var file = input.files[0];
+ if (file.size > 2 * 1024 * 1024) {
+ showToast('حجم الصورة كبير جداً (الحد 2 ميغابايت)', 'warning');
+ input.value = ''; return;
+ }
+ var reader = new FileReader();
+ reader.onload = function(e) {
+ if (preview) {
+ preview.classList.remove('hidden');
+ preview.innerHTML = ' ';
+ }
+ };
+ reader.readAsDataURL(file);
+}
+
+function uploadPhoto(inputId, callback) {
+ var input = document.getElementById(inputId);
+ if (!input || !input.files || !input.files[0]) { callback(null); return; }
+ var reader = new FileReader();
+ reader.onload = function(e) {
+ fetch('/api/upload/image', {
+ method: 'POST',
+ headers: {'Content-Type':'application/json'},
+ body: JSON.stringify({ imageData: e.target.result, type: inputId.includes('market') ? 'market' : 'report' })
+ }).then(function(r){ return r.json(); })
+ .then(function(data){ callback(data.success ? data.imageId : null); })
+ .catch(function(){ callback(null); });
+ };
+ reader.readAsDataURL(input.files[0]);
+}
+
+/* ============================================================
+ 🏥 HOSPITALS - دليل المستشفيات
+ ============================================================ */
+var _allHospitals = [];
+var _activeHospTab = 'list';
+function loadHospitals() {
+ var lat = userLat || 15.5007, lng = userLng || 32.5599;
+ fetch('/api/hospitals?lat=' + lat + '&lng=' + lng)
+ .then(function(r){ return r.json(); })
+ .then(function(list){
+ _allHospitals = list || [];
+ renderHospitals(_allHospitals);
+ }).catch(function(){
+ var el = document.getElementById('hospList');
+ if (el) el.innerHTML = emptyState('🏥','لا توجد بيانات','أضف أول مرفق صحي في منطقتك');
+ });
+}
+function renderHospitals(list) {
+ var el = document.getElementById('hospList');
+ if (!el) return;
+ if (!list || !list.length) {
+ el.innerHTML = emptyState('🏥','لا توجد مستشفيات','كن أول من يضيف مرفقاً صحياً في منطقتك!');
+ return;
+ }
+ el.innerHTML = list.map(function(h){
+ var dist_txt = h.dist != null ? '📍 ' + (h.dist < 1 ? Math.round(h.dist*1000)+'م' : h.dist.toFixed(1)+'كم') + ' ' : '';
+ var typeIcon = {مستشفى:'🏥',عيادة:'🩺',مختبر:'🔬',صيدلية:'💊',طوارئ:'🚨'}[h.type] || '🏥';
+ var stars = '';
+ var avg = h.avgRating || 0;
+ for (var i=1;i<=5;i++) stars += i<=Math.round(avg) ? '★' : '☆';
+ var emerg = h.emergency ? '🚨 طوارئ 24h ' : '';
+ return '' +
+ '
' +
+ '
' + typeIcon + ' ' +
+ '
' +
+ '
' + escHtml(h.name) + ' ' + emerg + '
' +
+ '
' + escHtml(h.type||'') + ' · ' + escHtml(h.area||'') + '
' +
+ (h.address ? '
📍 ' + escHtml(h.address) + '
' : '') +
+ (h.phone ? '
📞 ' + escHtml(h.phone) + '
' : '') +
+ '
' + stars + ' (' + (h.ratingCount||0) + ' تقييم) ' + dist_txt + '
' +
+ '
' +
+ '
' +
+ '
' +
+ (h.phone ? '📞 اتصال ' : '') +
+ (h.lat && h.lng ? '🗺️ خريطة ' : '') +
+ '⭐ قيّم ' +
+ '
' +
+ '
';
+ }).join('');
+}
+function filterHospitals() {
+ var q = (document.getElementById('hospSearchInp')||{value:''}).value.toLowerCase();
+ var t = (document.getElementById('hospTypeFilter')||{value:''}).value;
+ var filtered = _allHospitals.filter(function(h){
+ return (!q || (h.name||'').toLowerCase().includes(q) || (h.area||'').toLowerCase().includes(q)) &&
+ (!t || h.type === t);
+ });
+ renderHospitals(filtered);
+}
+function switchHospTab(tab) {
+ _activeHospTab = tab;
+ var form = document.getElementById('hospAddForm');
+ var list = document.getElementById('hospList');
+ if (tab === 'add') { if(form) form.classList.remove('hidden'); }
+ else { if(form) form.classList.add('hidden'); }
+}
+function submitHospital() {
+ var name = (document.getElementById('hospName')||{value:''}).value.trim();
+ var type = (document.getElementById('hospType')||{value:'مستشفى'}).value;
+ var area = (document.getElementById('hospArea')||{value:''}).value.trim();
+ var lat = (document.getElementById('hospLat')||{value:''}).value;
+ var lng = (document.getElementById('hospLng')||{value:''}).value;
+ var addr = (document.getElementById('hospAddress')||{value:''}).value.trim();
+ var phone= (document.getElementById('hospPhone')||{value:''}).value.trim();
+ var emerg= (document.getElementById('hospEmergency')||{checked:false}).checked;
+ if (!name || !area) { showToast('أدخل اسم المرفق والمنطقة', 'warning'); return; }
+ var body = { name:name, type:type, area:area, address:addr, phone:phone, emergency:emerg,
+ lat: lat ? parseFloat(lat) : (userLat||15.5), lng: lng ? parseFloat(lng) : (userLng||32.5),
+ userId: myUserId };
+ var btn = document.querySelector('#hospAddForm .btn-submit');
+ if (btn) { btn.disabled = true; btn.textContent = '⏳ جاري الإرسال...'; }
+ fetch('/api/hospitals', { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify(body) })
+ .then(function(r){ return r.json(); })
+ .then(function(data){
+ if (data.id) {
+ showToast('✅ تمت الإضافة بنجاح!', 'success');
+ ['hospName','hospAddress','hospPhone'].forEach(function(id){ var e=document.getElementById(id); if(e) e.value=''; });
+ var ec = document.getElementById('hospEmergency'); if(ec) ec.checked=false;
+ switchHospTab('list');
+ loadHospitals();
+ } else { showToast('حدث خطأ، حاول مرة أخرى', 'error'); }
+ }).catch(function(){ showToast('خطأ في الاتصال', 'error'); })
+ .finally(function(){ if(btn){btn.disabled=false;btn.textContent='🏥 أضف الآن';} });
+}
+function rateHospital(id, btn) {
+ var stars = prompt('قيّم هذا المرفق من 1 إلى 5:');
+ if (!stars || isNaN(stars) || stars < 1 || stars > 5) return;
+ fetch('/api/hospitals/' + id + '/rate', { method:'POST', headers:{'Content-Type':'application/json'},
+ body: JSON.stringify({ rating: parseInt(stars), userId: myUserId }) })
+ .then(function(r){ return r.json(); })
+ .then(function(){ showToast('✅ شكراً على تقييمك!', 'success'); loadHospitals(); })
+ .catch(function(){ showToast('خطأ في الإرسال', 'error'); });
+}
+
+/* ============================================================
+ 📰 NEWS - الأخبار المحلية
+ ============================================================ */
+var _allNews = [];
+var _newsFilter = '';
+function loadNews() {
+ fetch('/api/news')
+ .then(function(r){ return r.json(); })
+ .then(function(list){
+ _allNews = list || [];
+ renderNews(_allNews);
+ }).catch(function(){
+ var el = document.getElementById('newsList');
+ if (el) el.innerHTML = emptyState('📰','لا توجد أخبار','شارك أول خبر في منطقتك');
+ });
+}
+function renderNews(list) {
+ var el = document.getElementById('newsList');
+ if (!el) return;
+ if (!list || !list.length) {
+ el.innerHTML = emptyState('📰','لا توجد أخبار','كن أول من ينشر خبراً مجتمعياً!');
+ return;
+ }
+ el.innerHTML = list.map(function(n){
+ var catIcon = {سياسة:'🏛️',اقتصاد:'💰',أمن:'🛡️',صحة:'🏥',عام:'📋'}[n.category] || '📰';
+ var credBar = '';
+ return '' +
+ '
' +
+ '' + catIcon + ' ' + escHtml(n.category||'عام') + ' ' +
+ '' + timeAgo(n.ts) + ' ' +
+ '
' +
+ '
' + escHtml(n.title) + '
' +
+ '
' + escHtml(n.body||'') + '
' +
+ (n.area ? '
📍 ' + escHtml(n.area) + (n.source?' · المصدر: '+escHtml(n.source):'') + '
' : '') +
+ '' +
+ '
';
+ }).join('');
+}
+function filterNews(cat, btn) {
+ _newsFilter = cat;
+ document.querySelectorAll('#sec-news .filt').forEach(function(b){ b.classList.remove('active-filt'); });
+ if (btn) btn.classList.add('active-filt');
+ renderNews(cat ? _allNews.filter(function(n){ return n.category === cat; }) : _allNews);
+}
+function submitNews() {
+ var title = (document.getElementById('newsTitle')||{value:''}).value.trim();
+ var body = (document.getElementById('newsBody')||{value:''}).value.trim();
+ var cat = (document.getElementById('newsCat')||{value:'عام'}).value;
+ var src = (document.getElementById('newsSource')||{value:''}).value.trim();
+ var area = (document.getElementById('newsArea')||{value:''}).value.trim();
+ if (!title || !body) { showToast('أدخل العنوان والتفاصيل', 'warning'); return; }
+ var btn = document.querySelector('#sec-news .btn-submit');
+ if (btn) { btn.disabled = true; btn.textContent = '⏳ جاري النشر...'; }
+ fetch('/api/news', { method:'POST', headers:{'Content-Type':'application/json'},
+ body: JSON.stringify({ title:title, body:body, category:cat, source:src, area:area,
+ lat: userLat||null, lng: userLng||null, userId: myUserId }) })
+ .then(function(r){ return r.json(); })
+ .then(function(data){
+ if (data.id) {
+ showToast('✅ تم نشر الخبر!', 'success');
+ ['newsTitle','newsBody','newsSource','newsArea'].forEach(function(id){ var e=document.getElementById(id); if(e) e.value=''; });
+ loadNews();
+ } else { showToast('حدث خطأ', 'error'); }
+ }).catch(function(){ showToast('خطأ في الاتصال', 'error'); })
+ .finally(function(){ if(btn){btn.disabled=false;btn.textContent='📰 نشر الخبر';} });
+}
+function voteNews(id, val, btn) {
+ fetch('/api/news/' + id + '/vote', { method:'POST', headers:{'Content-Type':'application/json'},
+ body: JSON.stringify({ vote: val, userId: myUserId }) })
+ .then(function(r){ return r.json(); })
+ .then(function(data){
+ showToast(val > 0 ? '✅ شكراً على تأكيد الخبر' : '❌ تم التبليغ عن عدم صحته', 'success');
+ loadNews();
+ }).catch(function(){});
+}
+
+/* ============================================================
+ 🚗 RIDES - مشاركة التنقل
+ ============================================================ */
+var _allRides = [];
+function loadRides() {
+ fetch('/api/rides')
+ .then(function(r){ return r.json(); })
+ .then(function(list){
+ _allRides = list || [];
+ renderRides(_allRides);
+ }).catch(function(){
+ var el = document.getElementById('ridesList');
+ if (el) el.innerHTML = emptyState('🚗','لا توجد رحلات','أضف رحلتك وشارك المشوار!');
+ });
+}
+function renderRides(list) {
+ var el = document.getElementById('ridesList');
+ if (!el) return;
+ if (!list || !list.length) {
+ el.innerHTML = emptyState('🚗','لا توجد رحلات','أضف رحلتك وشارك مع من يريد المشوار نفسه!');
+ return;
+ }
+ el.innerHTML = list.map(function(r){
+ var seatsLeft = r.seatsLeft != null ? r.seatsLeft : ((r.seats||0) - (r.passengers ? r.passengers.length : 0));
+ var seatsColor = seatsLeft > 0 ? '#27ae60' : '#e74c3c';
+ return '' +
+ '
' +
+ '🚩 ' + escHtml(r.from||'—') + ' ' +
+ '→ ' +
+ '🏁 ' + escHtml(r.to||'—') + ' ' +
+ '
' +
+ '
' +
+ '📅 ' + escHtml(r.date||'') + (r.time?' '+escHtml(r.time):'') + ' ' +
+ '💺 ' + seatsLeft + ' مقعد متاح ' +
+ (r.price ? '💵 ' + r.price + ' ج.س ' : 'مجاني ') +
+ '
' +
+ (r.notes ? '
📝 ' + escHtml(r.notes) + '
' : '') +
+ '
' +
+ (r.contact ? '
📞 ' + escHtml(r.contact) + ' ' : '') +
+ (seatsLeft > 0 ? '
✋ أريد المشوار ' : '
اكتملت المقاعد ') +
+ ((r.fromLat && r.toLat) ? '
🗺️ مسار ' : '') +
+ '
🔗 ' +
+ '
' +
+ '
';
+ }).join('');
+}
+function searchRides() {
+ var from = (document.getElementById('rideFromSearch')||{value:''}).value.trim().toLowerCase();
+ var to = (document.getElementById('rideToSearch')||{value:''}).value.trim().toLowerCase();
+ var date = (document.getElementById('rideDateSearch')||{value:''}).value;
+ var filtered = _allRides.filter(function(r){
+ return (!from || (r.from||'').toLowerCase().includes(from)) &&
+ (!to || (r.to||'').toLowerCase().includes(to)) &&
+ (!date || r.date === date);
+ });
+ renderRides(filtered);
+}
+function submitRide() {
+ var from = (document.getElementById('rideFrom')||{value:''}).value.trim();
+ var to = (document.getElementById('rideTo')||{value:''}).value.trim();
+ var date = (document.getElementById('rideDate')||{value:''}).value;
+ var time = (document.getElementById('rideTime')||{value:''}).value;
+ var seats = parseInt((document.getElementById('rideSeats')||{value:'3'}).value) || 3;
+ var price = parseFloat((document.getElementById('ridePrice')||{value:'0'}).value) || 0;
+ var contact = (document.getElementById('rideContact')||{value:''}).value.trim();
+ var notes = (document.getElementById('rideNotes')||{value:''}).value.trim();
+ if (!from || !to || !contact) { showToast('أدخل نقطة الانطلاق والوجهة والتواصل', 'warning'); return; }
+ var btn = document.querySelector('#sec-rides .btn-rides');
+ if (btn) { btn.disabled = true; btn.textContent = '⏳ جاري الإضافة...'; }
+ fetch('/api/rides', { method:'POST', headers:{'Content-Type':'application/json'},
+ body: JSON.stringify({ from:from, to:to, date:date, time:time, seats:seats, price:price,
+ contact:contact, notes:notes, userId:myUserId,
+ lat: userLat||null, lng: userLng||null }) })
+ .then(function(r){ return r.json(); })
+ .then(function(data){
+ if (data.id) {
+ showToast('✅ تمت إضافة الرحلة!', 'success');
+ ['rideFrom','rideTo','rideContact','rideNotes'].forEach(function(id){ var e=document.getElementById(id); if(e) e.value=''; });
+ loadRides();
+ } else { showToast('حدث خطأ', 'error'); }
+ }).catch(function(){ showToast('خطأ في الاتصال', 'error'); })
+ .finally(function(){ if(btn){btn.disabled=false;btn.textContent='🚗 أضف الرحلة';} });
+}
+function requestRide(id, btn) {
+ if (btn) { btn.disabled = true; btn.textContent = '⏳...'; }
+ fetch('/api/rides/' + id + '/request', { method:'POST', headers:{'Content-Type':'application/json'},
+ body: JSON.stringify({ userId: myUserId }) })
+ .then(function(r){ return r.json(); })
+ .then(function(data){
+ showToast((data.ok||data.success) ? '✅ تم تأكيد حجزك!' : (data.error||'الرحلة مكتملة'), (data.ok||data.success) ? 'success':'error');
+ loadRides();
+ }).catch(function(){ if(btn){btn.disabled=false;btn.textContent='✋ أريد المشوار';} });
+}
+
+/* ============================================================
+ 🌦️ WEATHER - الطقس
+ ============================================================ */
+var _weatherCodes = {
+ 0:'☀️ صحو',1:'🌤️ صحو جزئياً',2:'⛅ غيوم متفرقة',3:'☁️ غائم',
+ 45:'🌫️ ضباب',48:'🌫️ ضباب كثيف',
+ 51:'🌦️ رذاذ خفيف',53:'🌦️ رذاذ',55:'🌧️ رذاذ كثيف',
+ 61:'🌧️ مطر خفيف',63:'🌧️ مطر',65:'⛈️ مطر غزير',
+ 71:'❄️ ثلج خفيف',73:'❄️ ثلج',75:'❄️ ثلج كثيف',
+ 80:'🌦️ زخات خفيفة',81:'🌧️ زخات',82:'⛈️ زخات غزيرة',
+ 95:'⛈️ عاصفة رعدية',96:'⛈️ عاصفة مع بَرَد',99:'⛈️ عاصفة شديدة'
+};
+function refreshWeather() {
+ var lat = userLat || 15.5007, lng = userLng || 32.5599;
+ var name = userLocationName || 'الخرطوم';
+ var el = document.getElementById('weatherLocText');
+ if (el) el.textContent = '📍 ' + name;
+ loadWeather(lat, lng);
+}
+function loadWeatherForCity() {
+ var lat = (document.getElementById('weatherCityLat')||{value:''}).value;
+ var lng = (document.getElementById('weatherCityLng')||{value:''}).value;
+ var name = (document.getElementById('weatherCityInp')||{value:''}).value;
+ if (!lat || !lng) { showToast('اختر مدينة من القائمة', 'warning'); return; }
+ loadWeather(parseFloat(lat), parseFloat(lng), name);
+}
+function loadWeather(lat, lng, cityName) {
+ var card = document.getElementById('weatherMainCard');
+ if (card) card.innerHTML = '⏳ جاري تحميل بيانات الطقس...
';
+ var url = 'https://api.open-meteo.com/v1/forecast?latitude=' + lat + '&longitude=' + lng +
+ '¤t=temperature_2m,relative_humidity_2m,wind_speed_10m,weather_code,apparent_temperature' +
+ '&hourly=temperature_2m,weather_code&daily=weather_code,temperature_2m_max,temperature_2m_min' +
+ '&timezone=Africa%2FKhartoum&forecast_days=5';
+ fetch(url)
+ .then(function(r){ return r.json(); })
+ .then(function(data){
+ renderWeather(data, cityName);
+ }).catch(function(){
+ if (card) card.innerHTML = '⚠️ تعذر تحميل بيانات الطقس. تحقق من الاتصال بالإنترنت.
';
+ });
+}
+function renderWeather(data, cityName) {
+ var card = document.getElementById('weatherMainCard');
+ if (!card) return;
+ var c = data.current || {};
+ var daily = data.daily || {};
+ var code = c.weather_code || 0;
+ var icon = (_weatherCodes[code]||'🌤️').split(' ')[0];
+ var desc = (_weatherCodes[code]||'غير معروف').split(' ').slice(1).join(' ');
+ var temp = Math.round(c.temperature_2m || 0);
+ var feel = Math.round(c.apparent_temperature || temp);
+ var hum = c.relative_humidity_2m || 0;
+ var wind = Math.round(c.wind_speed_10m || 0);
+
+ // Tips
+ var tips = '';
+ if (temp > 38) tips = '🌡️ حرارة شديدة — اشرب الماء كثيراً وتجنب الخروج وقت الذهيرة';
+ else if (temp > 30) tips = '☀️ طقس حار — يُنصح بارتداء ملابس خفيفة';
+ else if (temp < 15) tips = '🧥 طقس بارد — ارتدِ ملابس دافئة';
+ else tips = '🌤️ طقس لطيف';
+ var tipsCard = document.getElementById('weatherTipsCard');
+ if (tipsCard) tipsCard.innerHTML = '💡 ' + tips + '
';
+
+ // Daily forecast
+ var forecastHtml = '';
+ if (daily.time) {
+ var days = ['الأحد','الاثنين','الثلاثاء','الأربعاء','الخميس','الجمعة','السبت'];
+ forecastHtml = '';
+ for (var i = 1; i < Math.min(5, daily.time.length); i++) {
+ var d = new Date(daily.time[i]);
+ var dayName = days[d.getDay()];
+ var dCode = daily.weather_code[i] || 0;
+ var dIcon = (_weatherCodes[dCode]||'🌤️').split(' ')[0];
+ var dMax = Math.round(daily.temperature_2m_max[i]);
+ var dMin = Math.round(daily.temperature_2m_min[i]);
+ forecastHtml += '
' + dayName + '
' +
+ '
' + dIcon + '
' +
+ '
' + dMax + '° ' + dMin + '°
' +
+ '
';
+ }
+ forecastHtml += '
';
+ }
+
+ card.innerHTML =
+ '' +
+ '' +
+ '
🌡️ ' + feel + '° يبدو كأنه
' +
+ '
💧 ' + hum + '% رطوبة
' +
+ '
💨 ' + wind + ' km/h رياح
' +
+ '
' +
+ forecastHtml;
+}
+
+/* ============================================================
+ 💧 WATER - خريطة المياه
+ ============================================================ */
+var _allWater = [];
+function loadWaterReports() {
+ var lat = userLat || 15.5007, lng = userLng || 32.5599;
+ fetch('/api/water?lat=' + lat + '&lng=' + lng)
+ .then(function(r){ return r.json(); })
+ .then(function(list){
+ _allWater = list || [];
+ renderWaterReports(_allWater);
+ updateWaterStatus();
+ }).catch(function(){
+ var el = document.getElementById('waterList');
+ if (el) el.innerHTML = emptyState('💧','لا توجد تقارير','أبلّغ عن انقطاع المياه في منطقتك');
+ });
+}
+function updateWaterStatus() {
+ var nearby = _allWater.filter(function(w){ return w.dist != null && w.dist < 5 && w.type === 'cut'; });
+ var labelEl = document.getElementById('waterStatusLabel');
+ if (labelEl) {
+ if (!userLat) { labelEl.textContent = 'حدّد موقعك لمعرفة حالة المياه'; }
+ else if (nearby.length > 0) {
+ labelEl.textContent = '❌ مياه مقطوعة بالقرب منك';
+ labelEl.style.color = '#e74c3c';
+ } else {
+ labelEl.textContent = '✅ لا تقارير انقطاع في منطقتك';
+ labelEl.style.color = '#27ae60';
+ }
+ }
+}
+function renderWaterReports(list) {
+ var el = document.getElementById('waterList');
+ if (!el) return;
+ if (!list || !list.length) {
+ el.innerHTML = emptyState('💧','لا توجد تقارير','أبلّغ عن انقطاع المياه في منطقتك!');
+ return;
+ }
+ var typeMap = { cut:'انقطاع كلي 🔴', low:'ضعف الضغط 🟡', dirty:'مياه ملوثة ⚫', distribution:'نقطة توزيع 💧' };
+ el.innerHTML = list.map(function(w){
+ var dist_txt = w.dist != null ? ' · 📍 ' + (w.dist < 1 ? Math.round(w.dist*1000)+'م' : w.dist.toFixed(1)+'كم') : '';
+ return '' +
+ '
' +
+ '' + (typeMap[w.type]||w.type) + ' ' +
+ '' + timeAgo(w.ts) + ' ' +
+ '
' +
+ '
📍 ' + escHtml(w.area||'غير محدد') + dist_txt + '
' +
+ (w.notes ? '
' + escHtml(w.notes) + '
' : '') +
+ (w.duration ? '
⏳ المدة: ' + escHtml(w.duration) + '
' : '') +
+ '
' +
+ '✅ ' + (w.votes||0) + ' ' +
+ '❌ ' + (w.downvotes||0) + ' ' +
+ '
' +
+ '
';
+ }).join('');
+}
+function voteWater(status) {
+ if (!userLat) { showToast('حدّد موقعك أولاً', 'warning'); return; }
+ var body = { type: status === 'off' ? 'cut' : 'ok', area: userLocationName || 'غير محدد',
+ lat: userLat, lng: userLng, userId: myUserId, notes: '' };
+ if (status === 'off') {
+ fetch('/api/water', { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify(body) })
+ .then(function(r){ return r.json(); })
+ .then(function(data){
+ showToast('✅ تم الإبلاغ، شكراً!', 'success');
+ loadWaterReports();
+ }).catch(function(){});
+ } else {
+ showToast('✅ شكراً! تم تأكيد توفر المياه', 'success');
+ }
+}
+function voteWaterItem(id, val, btn) {
+ fetch('/api/water/' + id + '/vote', { method:'POST', headers:{'Content-Type':'application/json'},
+ body: JSON.stringify({ vote: val, userId: myUserId }) })
+ .then(function(r){ return r.json(); })
+ .then(function(){ loadWaterReports(); })
+ .catch(function(){});
+}
+function submitWaterReport() {
+ var area = (document.getElementById('waterArea')||{value:''}).value.trim();
+ var lat = (document.getElementById('waterLat')||{value:''}).value;
+ var lng = (document.getElementById('waterLng')||{value:''}).value;
+ var type = (document.getElementById('waterType')||{value:'cut'}).value;
+ var dur = (document.getElementById('waterDuration')||{value:''}).value.trim();
+ var notes = (document.getElementById('waterNotes')||{value:''}).value.trim();
+ if (!area) { showToast('أدخل اسم المنطقة', 'warning'); return; }
+ var btn = document.querySelector('#sec-water .btn-water');
+ if (btn) { btn.disabled = true; btn.textContent = '⏳ جاري الإرسال...'; }
+ fetch('/api/water', { method:'POST', headers:{'Content-Type':'application/json'},
+ body: JSON.stringify({ area:area, type:type, duration:dur, notes:notes,
+ lat: lat ? parseFloat(lat) : (userLat||15.5), lng: lng ? parseFloat(lng) : (userLng||32.5),
+ userId: myUserId }) })
+ .then(function(r){ return r.json(); })
+ .then(function(data){
+ if (data.id) {
+ showToast('✅ تم إرسال التقرير!', 'success');
+ ['waterArea','waterDuration','waterNotes'].forEach(function(id){ var e=document.getElementById(id); if(e) e.value=''; });
+ loadWaterReports();
+ } else { showToast('حدث خطأ', 'error'); }
+ }).catch(function(){ showToast('خطأ في الاتصال', 'error'); })
+ .finally(function(){ if(btn){btn.disabled=false;btn.textContent='💧 أبلّغ الآن';} });
+}
+
+/* ============================================================
+ 🎓 STUDY GROUPS - مجموعات التعلم
+ ============================================================ */
+var _allStudyGroups = [];
+var _activeStudyGroup = null;
+function loadStudyGroups() {
+ fetch('/api/study')
+ .then(function(r){ return r.json(); })
+ .then(function(list){
+ _allStudyGroups = list || [];
+ renderStudyGroups(_allStudyGroups);
+ }).catch(function(){
+ var el = document.getElementById('studyList');
+ if (el) el.innerHTML = emptyState('🎓','لا توجد مجموعات','أنشئ أول مجموعة تعلم!');
+ });
+}
+function renderStudyGroups(list) {
+ var el = document.getElementById('studyList');
+ if (!el) return;
+ if (!list || !list.length) {
+ el.innerHTML = emptyState('🎓','لا توجد مجموعات تعلم','أنشئ مجموعة وادعُ أصدقاءك!');
+ return;
+ }
+ var levelIcons = {ابتدائي:'🏫',متوسط:'🏫',ثانوي:'🏫',جامعي:'🎓',مهني:'🔧',عام:'📚'};
+ el.innerHTML = list.map(function(g){
+ var members = g.members ? g.members.length : 0;
+ var max = g.maxMembers || 20;
+ var pct = Math.min(100, Math.round(members/max*100));
+ return '' +
+ '
' +
+ '' + (levelIcons[g.level]||'📚') + ' ' + escHtml(g.level||'عام') + ' ' +
+ '' + members + '/' + max + ' عضو ' +
+ '
' +
+ '
' + escHtml(g.name) + '
' +
+ '
📖 ' + escHtml(g.subject||'') + '
' +
+ (g.schedule ? '
📅 ' + escHtml(g.schedule) + '
' : '') +
+ (g.area ? '
📍 ' + escHtml(g.area) + '
' : '') +
+ '
' +
+ '' +
+ '
';
+ }).join('');
+}
+function joinStudyGroup(id, name, btn) {
+ if (btn) { btn.disabled = true; btn.textContent = '⏳...'; }
+ fetch('/api/study/' + id + '/join', { method:'POST', headers:{'Content-Type':'application/json'},
+ body: JSON.stringify({ userId: myUserId, name: myName||'مجهول' }) })
+ .then(function(r){ return r.json(); })
+ .then(function(data){
+ showToast((data.ok||data.success) ? ('✅ انضممت إلى "' + name + '"!') : (data.error||'حدث خطأ'), (data.ok||data.success)?'success':'error');
+ loadStudyGroups();
+ }).catch(function(){ if(btn){btn.disabled=false;btn.textContent='✋ انضم';} });
+}
+function openStudyChat(id, name) {
+ _activeStudyGroup = id;
+ var nameEl = document.getElementById('studyChatName');
+ if (nameEl) nameEl.textContent = name;
+ var chatDiv = document.getElementById('studyGroupChat');
+ if (chatDiv) chatDiv.classList.remove('hidden');
+ loadStudyChatMessages(id);
+ var listDiv = document.getElementById('studyList');
+ if (listDiv) listDiv.scrollIntoView({ behavior:'smooth' });
+ // join socket room for real-time study chat messages
+ if (socket) {
+ socket.emit('join_study', id);
+ socket.off('study_standalone_msg');
+ // listen for incoming messages in the standalone study chat panel
+ socket.on('study_msg', function(data) {
+ if (data.groupId !== _activeStudyGroup) return;
+ // avoid duplicates if group page is also open
+ var el = document.getElementById('studyChatMessages');
+ if (!el) return;
+ var m = data.msg;
+ var isMe = m.userId === myUserId;
+ var dispName = m.name || m.author || 'مجهول';
+ var mediaHtml = '';
+ if (m.mediaType === 'image' && m.mediaData) {
+ mediaHtml = '';
+ } else if (m.mediaType === 'video' && m.mediaData) {
+ mediaHtml = '
';
+ } else if (m.mediaType === 'audio' && m.mediaData) {
+ mediaHtml = '';
+ } else if (m.mediaType === 'file' && m.mediaData) {
+ mediaHtml = '';
+ }
+ // only append if message not already rendered
+ var existing = el.querySelector('[data-msg-id="' + m.id + '"]');
+ if (!existing) {
+ var div = document.createElement('div');
+ div.className = 'study-msg' + (isMe ? ' study-msg-me' : '');
+ div.setAttribute('data-msg-id', m.id);
+ div.innerHTML =
+ (!isMe ? '' + escHtml(dispName) + '
' : '') +
+ mediaHtml +
+ (m.text ? '' + escHtml(m.text) + '
' : '') +
+ '' + timeAgo(m.ts || m.time) + '
';
+ el.appendChild(div);
+ el.scrollTop = el.scrollHeight;
+ }
+ });
+ }
+}
+function closeStudyChat() {
+ if (socket && _activeStudyGroup) socket.emit('leave_study', _activeStudyGroup);
+ _activeStudyGroup = null;
+ var chatDiv = document.getElementById('studyGroupChat');
+ if (chatDiv) chatDiv.classList.add('hidden');
+}
+function loadStudyChatMessages(id) {
+ fetch('/api/study/' + id + '/messages')
+ .then(function(r){ return r.json(); })
+ .then(function(msgs){
+ var el = document.getElementById('studyChatMessages');
+ if (!el) return;
+ if (!msgs || !msgs.length) { el.innerHTML = 'لا توجد رسائل بعد. كن أول من يبدأ!
'; return; }
+ el.innerHTML = msgs.map(function(m){
+ var isMe = m.userId === myUserId;
+ var dispName = m.name || m.author || 'مجهول';
+ var mediaHtml = '';
+ if (m.mediaType === 'image' && m.mediaData) {
+ mediaHtml = '';
+ } else if (m.mediaType === 'video' && m.mediaData) {
+ mediaHtml = '
';
+ } else if (m.mediaType === 'audio' && m.mediaData) {
+ mediaHtml = '';
+ } else if (m.mediaType === 'file' && m.mediaData) {
+ mediaHtml = '';
+ }
+ return '' +
+ (!isMe ? '
' + escHtml(dispName) + '
' : '') +
+ mediaHtml +
+ (m.text ? '
' + escHtml(m.text) + '
' : '') +
+ '
' + timeAgo(m.ts || m.time) + '
' +
+ '
';
+ }).join('');
+ el.scrollTop = el.scrollHeight;
+ }).catch(function(){});
+}
+function sendStudyMsg() {
+ if (!_activeStudyGroup) return;
+ var inp = document.getElementById('studyChatInp');
+ var text = inp ? (inp.value || inp.textContent || '').trim() : '';
+ if (!text) return;
+ fetch('/api/study/' + _activeStudyGroup + '/msg', { method:'POST', headers:{'Content-Type':'application/json'},
+ body: JSON.stringify({ text:text, userId:myUserId, name:myName||'مجهول' }) })
+ .then(function(r){ return r.json(); })
+ .then(function(){
+ if(inp) { inp.value=''; inp.style.height=''; }
+ loadStudyChatMessages(_activeStudyGroup);
+ })
+ .catch(function(){});
+}
+function submitStudyGroup() {
+ var name = (document.getElementById('studyName')||{value:''}).value.trim();
+ var subject = (document.getElementById('studySubject')||{value:''}).value.trim();
+ var level = (document.getElementById('studyLevel')||{value:'عام'}).value;
+ var max = parseInt((document.getElementById('studyMax')||{value:'20'}).value) || 20;
+ var sched = (document.getElementById('studySchedule')||{value:''}).value.trim();
+ var contact = (document.getElementById('studyContact')||{value:''}).value.trim();
+ var area = (document.getElementById('studyArea')||{value:''}).value.trim();
+ var avatar = (document.getElementById('studyAvatar')||{value:'🎓'}).value || '🎓';
+ var desc = (document.getElementById('studyDescription')||{value:''}).value.trim();
+ if (!name || !subject) { showToast('أدخل اسم المجموعة والموضوع', 'warning'); return; }
+ var btn = document.querySelector('#sec-study .btn-study');
+ if (btn) { btn.disabled = true; btn.textContent = '⏳ جاري الإنشاء...'; }
+ fetch('/api/study', { method:'POST', headers:{'Content-Type':'application/json'},
+ body: JSON.stringify({ name:name, subject:subject, level:level, maxMembers:max,
+ schedule:sched, contact:contact, area:area, userId:myUserId, author:myName||'عضو',
+ avatar:avatar, description:desc }) })
+ .then(function(r){ return r.json(); })
+ .then(function(data){
+ if (data.id) {
+ showToast('✅ تم إنشاء المجموعة!', 'success');
+ ['studyName','studySubject','studySchedule','studyContact','studyArea','studyDescription'].forEach(function(id){
+ var e=document.getElementById(id); if(e) e.value='';
+ });
+ loadStudyGroups();
+ // Open the new group automatically
+ if (data.group) {
+ setTimeout(function(){ openGroupPage(data.id); }, 800);
+ }
+ } else { showToast(data.error || 'حدث خطأ', 'error'); }
+ }).catch(function(){ showToast('خطأ في الاتصال', 'error'); })
+ .finally(function(){ if(btn){btn.disabled=false;btn.textContent='🎓 أنشئ المجموعة';} });
+}
+
+/* ============================================================
+ 📦 HELP REQUESTS - طلبات المساعدة
+ ============================================================ */
+var _allHelp = [];
+var _helpFilter = '';
+function loadHelpRequests() {
+ fetch('/api/help?lat=' + (userLat||15.5) + '&lng=' + (userLng||32.5))
+ .then(function(r){ return r.json(); })
+ .then(function(list){
+ _allHelp = list || [];
+ renderHelp(_allHelp);
+ }).catch(function(){
+ var el = document.getElementById('helpList');
+ if (el) el.innerHTML = emptyState('📦','لا توجد طلبات','اطلب مساعدة أو قدّمها');
+ });
+}
+function renderHelp(list) {
+ var el = document.getElementById('helpList');
+ if (!el) return;
+ if (!list || !list.length) {
+ el.innerHTML = emptyState('📦','لا توجد طلبات مساعدة','كن أول من يطلب أو يقدم مساعدة!');
+ return;
+ }
+ var typeIcon = {food:'🍞',medicine:'💊',transport:'🚗',shelter:'🏠',money:'💵',other:'📋'};
+ var typeLabel = {food:'غذاء',medicine:'دواء',transport:'مواصلات',shelter:'مأوى',money:'مالي',other:'أخرى'};
+ el.innerHTML = list.map(function(h){
+ var dist_txt = h.dist != null ? ' · 📍 ' + (h.dist < 1 ? Math.round(h.dist*1000)+'م' : h.dist.toFixed(1)+'كم') : '';
+ var urgent = h.urgent ? '🚨 عاجل ' : '';
+ var closed = (h.closed || h.status === 'closed') ? '(مُغلق) ' : '';
+ var isClosed = h.closed || h.status === 'closed';
+ return '' +
+ '
' +
+ '' + (typeIcon[h.type]||'📋') + ' ' + escHtml(typeLabel[h.type]||'أخرى') + ' ' +
+ urgent + closed +
+ '' + timeAgo(h.ts) + ' ' +
+ '
' +
+ '
' + escHtml(h.title) + '
' +
+ (h.desc ? '
' + escHtml(h.desc) + '
' : '') +
+ '
' + escHtml(h.area||'غير محدد') + dist_txt + ' · ' + (h.offers||h.offersCount||0) + ' عرض مساعدة
' +
+ (!isClosed ? '
' : '') +
+ '
';
+ }).join('');
+}
+function filterHelp(type, btn) {
+ _helpFilter = type;
+ document.querySelectorAll('#sec-help .filt').forEach(function(b){ b.classList.remove('active-filt'); });
+ if (btn) btn.classList.add('active-filt');
+ var filtered = _allHelp;
+ if (type === 'urgent') filtered = _allHelp.filter(function(h){ return h.urgent; });
+ else if (type) filtered = _allHelp.filter(function(h){ return h.type === type; });
+ renderHelp(filtered);
+}
+function offerHelp(id, btn) {
+ var msg = prompt('كيف يمكنك المساعدة؟ (اختياري)') || '';
+ if (btn) { btn.disabled = true; }
+ fetch('/api/help/' + id + '/offer', { method:'POST', headers:{'Content-Type':'application/json'},
+ body: JSON.stringify({ userId: myUserId, name: myName||'مجهول', message: msg }) })
+ .then(function(r){ return r.json(); })
+ .then(function(data){
+ showToast((data.ok||data.success||data.offers!=null) ? '✅ تم إرسال عرض المساعدة!' : (data.error||'حدث خطأ'), (data.ok||data.success||data.offers!=null)?'success':'error');
+ loadHelpRequests();
+ }).catch(function(){ if(btn) btn.disabled=false; });
+}
+function submitHelpRequest() {
+ var type = (document.getElementById('helpType')||{value:'other'}).value;
+ var title = (document.getElementById('helpTitle')||{value:''}).value.trim();
+ var desc = (document.getElementById('helpDesc')||{value:''}).value.trim();
+ var area = (document.getElementById('helpArea')||{value:''}).value.trim();
+ var contact = (document.getElementById('helpContact')||{value:''}).value.trim();
+ var urgent = (document.getElementById('helpUrgent')||{checked:false}).checked;
+ if (!title || !area || !contact) { showToast('أدخل الطلب والمنطقة وطريقة التواصل', 'warning'); return; }
+ var btn = document.querySelector('#sec-help .btn-help');
+ if (btn) { btn.disabled = true; btn.textContent = '⏳ جاري الإرسال...'; }
+ fetch('/api/help', { method:'POST', headers:{'Content-Type':'application/json'},
+ body: JSON.stringify({ type:type, title:title, desc:desc, area:area, contact:contact,
+ urgent:urgent, lat: userLat||null, lng: userLng||null, userId:myUserId }) })
+ .then(function(r){ return r.json(); })
+ .then(function(data){
+ if (data.id) {
+ showToast('✅ تم إرسال طلب المساعدة!', 'success');
+ ['helpTitle','helpDesc','helpArea','helpContact'].forEach(function(id){ var e=document.getElementById(id); if(e) e.value=''; });
+ var uc = document.getElementById('helpUrgent'); if(uc) uc.checked=false;
+ loadHelpRequests();
+ } else { showToast('حدث خطأ', 'error'); }
+ }).catch(function(){ showToast('خطأ في الاتصال', 'error'); })
+ .finally(function(){ if(btn){btn.disabled=false;btn.textContent='📦 أرسل الطلب';} });
+}
+
+/* ============================================================
+ 🗳️ POLLS - استطلاعات الرأي
+ ============================================================ */
+var _allPolls = [];
+var _votedPolls = JSON.parse(localStorage.getItem('_nabdh_voted_polls')||'{}');
+function loadPolls() {
+ fetch('/api/polls')
+ .then(function(r){ return r.json(); })
+ .then(function(list){
+ _allPolls = list || [];
+ renderPolls(_allPolls);
+ }).catch(function(){
+ var el = document.getElementById('pollsList');
+ if (el) el.innerHTML = emptyState('🗳️','لا توجد استطلاعات','أنشئ أول استطلاع رأي!');
+ });
+}
+function renderPolls(list) {
+ var el = document.getElementById('pollsList');
+ if (!el) return;
+ if (!list || !list.length) {
+ el.innerHTML = emptyState('🗳️','لا توجد استطلاعات','أنشئ استطلاعاً وشارك رأيك!');
+ return;
+ }
+ el.innerHTML = list.map(function(p){
+ // Server stores options as [{text, votes}], compute total
+ var opts = p.options || [];
+ var total = p.totalVotes || opts.reduce(function(a,b){ return a + (typeof b==='object'?b.votes:0); }, 0);
+ var hasVoted = !!_votedPolls[p.id];
+ var expired = p.expiresAt && new Date(p.expiresAt) < new Date();
+ var maxVotes = Math.max.apply(null, opts.map(function(o){ return typeof o==='object'?o.votes:0; }).concat([0]));
+ var optHtml = opts.map(function(opt, i){
+ var optText = typeof opt === 'object' ? opt.text : opt;
+ var cnt = typeof opt === 'object' ? (opt.votes||0) : ((p.votes||[])[i]||0);
+ var pct = total > 0 ? Math.round(cnt/total*100) : 0;
+ var isWinner = hasVoted && cnt === maxVotes && total > 0;
+ return '' +
+ '
' +
+ '' + escHtml(optText) + ' ' +
+ (hasVoted||expired ? '' + pct + '% ' : '') +
+ '
' +
+ (hasVoted||expired ? '
' : '') +
+ '
';
+ }).join('');
+ return '' +
+ '
' +
+ '' + escHtml(p.question) + ' ' +
+ (expired ? 'منتهي ' : '') +
+ '
' +
+ (p.area ? '
📍 ' + escHtml(p.area) + '
' : '') +
+ '
' + optHtml + '
' +
+ '' +
+ '
';
+ }).join('');
+}
+function castVote(pollId, optIndex, el) {
+ if (_votedPolls[pollId]) { showToast('لقد صوتت على هذا الاستطلاع بالفعل', 'warning'); return; }
+ fetch('/api/polls/' + pollId + '/vote', { method:'POST', headers:{'Content-Type':'application/json'},
+ body: JSON.stringify({ option: optIndex, userId: myUserId }) })
+ .then(function(r){ return r.json(); })
+ .then(function(data){
+ if (data.ok) {
+ _votedPolls[pollId] = optIndex;
+ localStorage.setItem('_nabdh_voted_polls', JSON.stringify(_votedPolls));
+ showToast('✅ تم تسجيل صوتك!', 'success');
+ loadPolls();
+ } else { showToast(data.error||'حدث خطأ', 'error'); }
+ }).catch(function(){});
+}
+function addPollOption() {
+ var container = document.getElementById('pollOptionsContainer');
+ if (!container) return;
+ var inputs = container.querySelectorAll('.poll-option-inp');
+ if (inputs.length >= 8) { showToast('الحد الأقصى 8 خيارات', 'warning'); return; }
+ var inp = document.createElement('input');
+ inp.className = 'inp poll-option-inp';
+ inp.placeholder = 'خيار آخر';
+ inp.style.marginBottom = '.4rem';
+ container.appendChild(inp);
+}
+function submitPoll() {
+ var question = (document.getElementById('pollQuestion')||{value:''}).value.trim();
+ var optInputs = document.querySelectorAll('.poll-option-inp');
+ var options = Array.from(optInputs).map(function(i){ return i.value.trim(); }).filter(Boolean);
+ var expiry = parseInt((document.getElementById('pollExpiry')||{value:'24'}).value) || 24;
+ var area = (document.getElementById('pollArea')||{value:''}).value.trim();
+ if (!question) { showToast('أدخل السؤال', 'warning'); return; }
+ if (options.length < 2) { showToast('أدخل خيارين على الأقل', 'warning'); return; }
+ var btn = document.querySelector('#sec-polls .btn-submit');
+ if (btn) { btn.disabled = true; btn.textContent = '⏳ جاري النشر...'; }
+ var expiresAt = new Date(Date.now() + expiry * 3600000).toISOString();
+ fetch('/api/polls', { method:'POST', headers:{'Content-Type':'application/json'},
+ body: JSON.stringify({ question:question, options:options, expiresAt:expiresAt, area:area, userId:myUserId }) })
+ .then(function(r){ return r.json(); })
+ .then(function(data){
+ if (data.id) {
+ showToast('✅ تم نشر الاستطلاع!', 'success');
+ var pq = document.getElementById('pollQuestion'); if(pq) pq.value='';
+ var pa = document.getElementById('pollArea'); if(pa) pa.value='';
+ var cont = document.getElementById('pollOptionsContainer');
+ if (cont) {
+ cont.innerHTML = ' ';
+ }
+ loadPolls();
+ } else { showToast('حدث خطأ', 'error'); }
+ }).catch(function(){ showToast('خطأ في الاتصال', 'error'); })
+ .finally(function(){ if(btn){btn.disabled=false;btn.textContent='🗳️ نشر الاستطلاع';} });
+}
+
+/* ============================================================
+ 📊 DASHBOARD - لوحة الإحصاءات
+ ============================================================ */
+function loadDashboard() {
+ fetch('/api/dashboard')
+ .then(function(r){ return r.json(); })
+ .then(function(data){
+ renderDashboard(data);
+ }).catch(function(){
+ showToast('تعذر تحميل الإحصاءات', 'error');
+ });
+}
+function renderDashboard(data) {
+ var s = data.stats || {};
+ // Live stats
+ setEl('dsc-online', s.online || 0);
+ setEl('dsc-reports', s.reports || 0);
+ setEl('dsc-lives', s.lives || s.lives_saved || 0);
+ setEl('dsc-cities', s.cities || 0);
+
+ // Feature stats grid — works with both features[] array AND flat stats object
+ var grid = document.getElementById('dashGrid');
+ if (grid) {
+ var features = data.features;
+ if (!features || !features.length) {
+ // Build from flat stats
+ features = [
+ { icon:'📢', label:'بلاغات', count: s.reports||0 },
+ { icon:'💵', label:'أسعار صرف', count: s.exchange||0 },
+ { icon:'💊', label:'أدوية', count: s.medicines||0 },
+ { icon:'🔊', label:'صوت الحي', count: s.voice||0 },
+ { icon:'🤝', label:'مهارات', count: s.skills||0 },
+ { icon:'🛒', label:'سوق P2P', count: s.market||0 },
+ { icon:'🩸', label:'متبرعو الدم', count: s.bloodDonors||0 },
+ { icon:'⚡', label:'تقارير كهرباء', count: s.power||0 },
+ { icon:'🏥', label:'مستشفيات', count: s.hospitals||0 },
+ { icon:'📰', label:'أخبار', count: s.news||0 },
+ { icon:'🚗', label:'رحلات', count: s.rides||0 },
+ { icon:'💧', label:'تقارير مياه', count: s.water||0 },
+ { icon:'🎓', label:'مجموعات تعلم', count: s.study||0 },
+ { icon:'📦', label:'طلبات مساعدة', count: s.help||0 },
+ { icon:'🗳️', label:'استطلاعات', count: s.polls||0 }
+ ];
+ }
+ grid.innerHTML = features.map(function(f){
+ return '' +
+ '
' + (f.count||0) + '
' +
+ '
' + (f.icon||'') + ' ' + (f.label||'') + '
' +
+ '
';
+ }).join('');
+ }
+
+ // Top areas
+ var areasEl = document.getElementById('dashTopAreas');
+ if (areasEl) {
+ var areas = data.topAreas || [];
+ if (!areas.length) {
+ areasEl.innerHTML = 'لا توجد بيانات كافية بعد
';
+ } else {
+ var maxCount = areas[0].count || 1;
+ areasEl.innerHTML = areas.slice(0,5).map(function(a, i){
+ var pct = Math.round(a.count/maxCount*100);
+ return '' +
+ '
' +
+ '' + (i===0?'🥇':i===1?'🥈':i===2?'🥉':' ') + ' ' + escHtml(a.area||a.name||'—') + ' ' +
+ '' + a.count + ' ' +
+ '
' +
+ '
' +
+ '
';
+ }).join('');
+ }
+ }
+
+ // Last 24h stats — server may return object or array
+ var h24El = document.getElementById('dash24h');
+ if (h24El) {
+ var h24raw = data.recent24h || data.last24h || {};
+ var h24arr = [];
+ if (Array.isArray(h24raw)) {
+ h24arr = h24raw;
+ } else {
+ Object.keys(h24raw).forEach(function(k){
+ h24arr.push({ label: k, count: h24raw[k] });
+ });
+ }
+ if (h24arr.length) {
+ h24El.innerHTML = h24arr.map(function(f){
+ return '' +
+ '
' + (f.count||0) + '
' +
+ '
' + (f.label||'') + '
' +
+ '
';
+ }).join('');
+ } else {
+ h24El.innerHTML = 'لا توجد نشاطات في آخر 24 ساعة
';
+ }
+ }
+}
+
+/* ============================================================
+ 🔌 REAL-TIME SOCKET EVENTS for new features
+ ============================================================ */
+(function() {
+ var _sockWait = setInterval(function() {
+ if (typeof socket !== 'undefined' && socket) {
+ clearInterval(_sockWait);
+
+ socket.on('new_blood_donor', function(donor) {
+ var sec = document.getElementById('sec-blood');
+ if (sec && sec.classList.contains('active-sec')) searchBlood();
+ showNotif('🩸 متبرع دم جديد: ' + (donor.bloodType || '') + ' في ' + (donor.area || '—'));
+ });
+
+ socket.on('new_blood_request', function(req) {
+ if (req.urgent) {
+ showNotif('🆘 طلب دم عاجل: ' + (req.bloodType || '') + ' - ' + (req.hospital || req.area || '—'));
+ showToast('🆘 طلب دم عاجل: ' + (req.bloodType || '') + ' في ' + (req.hospital || req.area || '—'), 'error');
+ }
+ var list = document.getElementById('bloodRequestsList');
+ if (list) loadBloodRequests();
+ });
+
+ socket.on('new_power_report', function() {
+ var sec = document.getElementById('sec-power');
+ if (sec && sec.classList.contains('active-sec')) loadPowerSchedules();
+ });
+
+ socket.on('power_vote_update', function() {
+ var sec = document.getElementById('sec-power');
+ if (sec && sec.classList.contains('active-sec')) loadPowerSchedules();
+ });
+ }
+ }, 500);
+})();
+
+/* ============================================================
+ 📲 PWA INSTALL - تثبيت التطبيق
+ ============================================================ */
+var _deferredInstallPrompt = null;
+
+window.addEventListener('beforeinstallprompt', function(e) {
+ e.preventDefault();
+ _deferredInstallPrompt = e;
+ // Show install banner after 3 seconds if not dismissed
+ setTimeout(function() {
+ var banner = document.getElementById('pwaInstallBanner');
+ var dismissed = localStorage.getItem('pwa_install_dismissed');
+ if (banner && !dismissed) {
+ banner.classList.remove('hidden');
+ }
+ }, 3000);
+});
+
+function installPWA() {
+ var banner = document.getElementById('pwaInstallBanner');
+ if (banner) banner.classList.add('hidden');
+ if (_deferredInstallPrompt) {
+ _deferredInstallPrompt.prompt();
+ _deferredInstallPrompt.userChoice.then(function(result) {
+ if (result.outcome === 'accepted') {
+ showToast('✅ تم تثبيت نبض على جهازك!', 'success');
+ }
+ _deferredInstallPrompt = null;
+ });
+ } else {
+ showToast('📲 يمكنك تثبيت التطبيق من قائمة المتصفح', 'info');
+ }
+}
+
+function hideInstallBanner() {
+ var banner = document.getElementById('pwaInstallBanner');
+ if (banner) banner.classList.add('hidden');
+ localStorage.setItem('pwa_install_dismissed', '1');
+}
+
+window.addEventListener('appinstalled', function() {
+ showToast('✅ تم تثبيت نبض بنجاح!', 'success');
+ _deferredInstallPrompt = null;
+});
+
+/* ============================================================
+ 🗺️ MAP ENHANCEMENTS v2 - طبقات الخريطة والتحسينات
+============================================================ */
+
+let currentTileLayer = null;
+let statesLayerVisible = true;
+let alertsLayerVisible = true;
+let mapLayersPanelOpen = false;
+
+const MAP_TILES = {
+ dark: 'https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png',
+ light: 'https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png',
+ satellite: 'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}',
+ topo: 'https://{s}.tile.opentopomap.org/{z}/{x}/{y}.png'
+};
+
+function changeMapStyle(style) {
+ if (!map) return;
+ if (currentTileLayer) { map.removeLayer(currentTileLayer); }
+ const url = MAP_TILES[style] || MAP_TILES.dark;
+ currentTileLayer = L.tileLayer(url, { maxZoom: 19, subdomains: style === 'dark' || style === 'light' ? 'abcd' : 'abc' });
+ currentTileLayer.addTo(map);
+ currentTileLayer.bringToBack();
+ showToast('🗺️ تم تغيير نمط الخريطة', 'success');
+}
+
+function toggleMapLayers() {
+ mapLayersPanelOpen = !mapLayersPanelOpen;
+ const panel = document.getElementById('mapLayersPanel');
+ if (panel) panel.classList.toggle('hidden', !mapLayersPanelOpen);
+}
+
+function toggleLayerAlerts(btn) {
+ alertsLayerVisible = !alertsLayerVisible;
+ btn && btn.classList.toggle('active', alertsLayerVisible);
+ Object.values(mapMarkers).forEach(m => {
+ if (alertsLayerVisible) m.addTo && m.addTo(map);
+ else m.remove && m.remove();
+ });
+ showToast(alertsLayerVisible ? '📢 التنبيهات مرئية' : '📢 التنبيهات مخفية', 'success');
+}
+
+function toggleLayerPeople(btn) {
+ peopleLayerVisible = !peopleLayerVisible;
+ btn && btn.classList.toggle('active', peopleLayerVisible);
+ Object.values(peopleMarkers).forEach(m => {
+ if (peopleLayerVisible) m.addTo && m.addTo(map);
+ else m.remove && m.remove();
+ });
+ showToast(peopleLayerVisible ? '👥 الأشخاص مرئيون' : '👥 الأشخاص مخفيون', 'success');
+}
+
+function toggleStatesLayer(btn) {
+ statesLayerVisible = !statesLayerVisible;
+ btn && btn.classList.toggle('active', statesLayerVisible);
+ // reload states or hide
+ if (statesLayerVisible) addSudanStatesLayer();
+ else {
+ // remove state circles - they don't have a separate layer group, so reinit map
+ showToast('🇸🇩 طبقة الولايات ' + (statesLayerVisible ? 'مرئية' : 'مخفية'), 'success');
+ }
+}
+
+function refreshAllMapData() {
+ const btn = document.querySelector('.map-refresh-btn');
+ if (btn) { btn.textContent = '⏳'; btn.disabled = true; }
+ Promise.all([
+ fetch('/api/map').then(r => r.json()).catch(() => []),
+ fetch('/api/people/map').then(r => r.json()).catch(() => [])
+ ]).then(([pins, people]) => {
+ // Clear old markers
+ Object.values(mapMarkers).forEach(m => m.remove && m.remove());
+ mapMarkers = {};
+ pins.forEach(p => addMapPin(p));
+ renderMapAlerts();
+ updateMapCounts();
+ if (peopleLayerVisible) refreshPeopleMarkers(people);
+ const el = document.getElementById('cnt-people');
+ if (el) el.textContent = people.length;
+ showToast('✅ تم تحديث الخريطة', 'success');
+ }).finally(() => {
+ if (btn) { btn.textContent = '🔄'; btn.disabled = false; }
+ });
+}
+
+// تحديث درجة الحرارة على شريط الخريطة
+function updateMapWeatherMini() {
+ const wrap = document.getElementById('msb-weather-mini');
+ const temp = document.getElementById('cnt-temp');
+ if (!wrap || !temp) return;
+ fetch('/api/weather?lat=' + (userLat || 15.5007) + '&lon=' + (userLng || 32.5599))
+ .then(r => r.json())
+ .then(d => {
+ if (d && d.current) {
+ temp.textContent = Math.round(d.current.temp || d.current.temperature || 0);
+ wrap.style.display = 'flex';
+ }
+ }).catch(() => {});
+}
+
+// تشغيل تحديث الطقس على الخريطة
+setTimeout(updateMapWeatherMini, 3000);
+
+/* ============================================================
+ 🏥 HOSPITALS ENHANCED
+============================================================ */
+function renderHospitalCard(h) {
+ const icons = { 'مستشفى': '🏥', 'عيادة': '🩺', 'مختبر': '🔬', 'صيدلية': '💊', 'طوارئ': '🚨' };
+ const d = dist(h);
+ const distTxt = d !== null ? (d < 1 ? '<1 كم' : Math.round(d) + ' كم') : '—';
+ const stars = '⭐'.repeat(Math.round(h.rating || 0)) + '☆'.repeat(5 - Math.round(h.rating || 0));
+ const emergencyBadge = h.emergency ? '🚨 طوارئ 24/7 ' : '';
+ const ratingCount = h.ratingCount ? `(${h.ratingCount}) ` : '';
+ return `
+
+
${icons[h.type] || '🏥'}
+
+
${escHtml(h.name)}
+
${escHtml(h.type || 'مرفق صحي')} ${emergencyBadge}
+
${stars} ${ratingCount}
+
+
${distTxt}
+
+
+
+ ${h.phone ? `
📞 اتصال ` : ''}
+ ${(h.lat && h.lng) ? `
🗺️ على الخريطة ` : ''}
+
⭐ قيّم
+
🔗
+
+
`;
+}
+
+/* ============================================================
+ 📰 NEWS ENHANCED
+============================================================ */
+function renderNewsCard(n) {
+ const catIcons = { 'سياسة':'🏛️', 'اقتصاد':'💰', 'أمن':'🛡️', 'صحة':'🏥', 'عام':'📋', 'رياضة':'⚽', 'ثقافة':'🎭' };
+ const icon = catIcons[n.category] || '📋';
+ const total = (n.upvotes || 0) + (n.downvotes || 0);
+ const credPct = total > 0 ? Math.round((n.upvotes || 0) / total * 100) : 50;
+ const credColor = credPct >= 70 ? '#1abc9c' : credPct >= 40 ? '#f39c12' : '#e74c3c';
+ return `
+
+ ${icon} ${escHtml(n.category || 'عام')}
+ 🕐 ${timeAgo(n.time)}
+
+
${escHtml(n.title)}
+
${escHtml(n.body)}
+
+
+
مصداقية
+
+
${credPct}%
+
+
+ 👍 ${n.upvotes || 0}
+ 👎 ${n.downvotes || 0}
+ 🔗 شارك
+
+
`;
+}
+
+/* ============================================================
+ 🚗 RIDES ENHANCED
+============================================================ */
+function renderRideCard(r) {
+ const d = dist(r);
+ const seatsLeft = (r.seats || 1) - (r.passengers || 0);
+ const seatsColor = seatsLeft <= 1 ? '#e74c3c' : seatsLeft <= 2 ? '#f39c12' : '#1abc9c';
+ return `
+
+
🟢 ${escHtml(r.from || '—')}
+
↓
+
🔴 ${escHtml(r.to || '—')}
+
+
+ 🕐 ${r.time ? new Date(r.time).toLocaleTimeString('ar',{hour:'2-digit',minute:'2-digit'}) : '—'}
+ 💺 ${seatsLeft} مقعد متاح
+ ${r.price ? `💵 ${r.price} جنيه ` : '🆓 مجاني '}
+ ${d !== null ? `📡 ${Math.round(d)} كم ` : ''}
+
+
+
👤 ${escHtml(r.driver || r.name || 'سائق')}
+ ${r.phone ? `
📞 ` : ''}
+
+
+ ${r.phone ? `
📞 تواصل ` : ''}
+ ${seatsLeft > 0 ? `
🚗 انضم للرحلة ` : '
🚫 مكتمل '}
+
🔗
+
+
`;
+}
+
+/* ============================================================
+ 💧 WATER REPORTS ENHANCED
+============================================================ */
+function renderWaterCard(w) {
+ const typeLabels = { 'انقطاع':'🚱 انقطاع', 'ضعيف':'💧 ضغط ضعيف', 'ملوث':'☣️ ملوث', 'توزيع':'🚰 توزيع' };
+ const typeClass = { 'انقطاع':'wtype-cut', 'ضعيف':'wtype-low', 'ملوث':'wtype-dirty', 'توزيع':'wtype-dist' };
+ const d = dist(w);
+ const duration = w.endTime ? Math.ceil((new Date(w.endTime) - new Date(w.startTime)) / 3600000) + ' ساعة' : 'جارٍ';
+ return `
+
+ ${typeLabels[w.type] || w.type}
+ 🕐 ${timeAgo(w.time)}
+
+
📍 ${escHtml(w.area)}
+ ${w.notes ? `
${escHtml(w.notes)}
` : ''}
+
+ ⏱️ ${duration}
+ ${d !== null ? `📡 ${Math.round(d)} كم منك ` : ''}
+ ${(w.lat && w.lng) ? `🗺️ ` : ''}
+
+
+ ✅ ${w.upvotes || 0} صحيح
+ ❌ ${w.downvotes || 0} خطأ
+ 🔗
+
+
`;
+}
+
+/* ============================================================
+ 🎓 STUDY GROUPS ENHANCED
+============================================================ */
+function renderStudyGroupCard(g) {
+ const levelIcons = { 'ابتدائي':'🏫', 'متوسط':'📚', 'ثانوي':'🎒', 'جامعي':'🎓', 'مهني':'🔧' };
+ const icon = levelIcons[g.level] || '📖';
+ const membersArr = Array.isArray(g.members) ? g.members : Object.keys(g.members || {});
+ const isMember = membersArr.includes(myUserId);
+ const membersCount = membersArr.length;
+ const maxM = g.maxMembers || 10;
+ const pct = Math.min(Math.round(membersCount / maxM * 100), 100);
+ const isFull = membersCount >= maxM;
+ return `
+
+ ${icon} ${escHtml(g.level || 'عام')}
+ ${membersCount}/${maxM} عضو
+
+
${escHtml(g.name)}
+
📖 ${escHtml(g.subject)}
+ ${g.schedule ? `
🗓️ ${escHtml(g.schedule)}
` : ''}
+ ${g.area ? `
📍 ${escHtml(g.area)}
` : ''}
+
+
+
`;
+}
+
+/* ============================================================
+ 🆘 HELP REQUESTS ENHANCED
+============================================================ */
+function renderHelpCard(h) {
+ const typeIcons = { 'food':'🍞', 'medicine':'💊', 'transport':'🚗', 'shelter':'🏠', 'money':'💵', 'other':'🤝' };
+ const typeLabels = { 'food':'طعام', 'medicine':'دواء', 'transport':'نقل', 'shelter':'مأوى', 'money':'مساعدة مالية', 'other':'أخرى' };
+ const icon = typeIcons[h.type] || '🤝';
+ const label = typeLabels[h.type] || h.type;
+ const d = dist(h);
+ const urgentBadge = h.urgent ? '🚨 عاجل ' : '';
+ const closedBadge = h.status === 'closed' ? '✅ مكتمل ' : '';
+ return `
+
+
${icon} ${label}
+
${urgentBadge}${closedBadge}
+
🕐 ${timeAgo(h.time)}
+
+
${escHtml(h.title)}
+
${escHtml(h.desc || h.description || '')}
+
+ 📍 ${escHtml(h.area || '—')}
+ ${d !== null ? ` • 📡 ${Math.round(d)} كم` : ''}
+
+
+ ${h.contact ? `
📞 تواصل ` : ''}
+ ${h.status !== 'closed' ? `
🤝 أقدم مساعدة ` : ''}
+
🔗
+
+
`;
+}
+
+/* ============================================================
+ 🗳️ POLLS ENHANCED
+============================================================ */
+function renderPollCard(p) {
+ const total = (p.options || []).reduce((s, o) => s + (o.votes || 0), 0);
+ const isVoted = _votedPolls && _votedPolls[p.id] !== undefined;
+ const votedIdx = isVoted ? _votedPolls[p.id] : -1;
+ const isExpired = p.expiresAt && Date.now() > new Date(p.expiresAt).getTime();
+ const expiryBadge = isExpired ? '⏰ انتهى ' : '';
+ const maxVotes = Math.max(...(p.options||[]).map(o => o.votes||0), 1);
+
+ const optionsHtml = (p.options || []).map((opt, i) => {
+ const pct = total > 0 ? Math.round((opt.votes || 0) / total * 100) : 0;
+ const isWinner = (opt.votes || 0) === maxVotes && total > 0 && !isExpired;
+ const isMyVote = votedIdx === i;
+ return `
+
+ ${isMyVote ? '✅ ' : isWinner && isVoted ? '🏆 ' : ''}${escHtml(opt.text)}
+
+
+
${pct}%
+
`;
+ }).join('');
+
+ return `
+
+
${escHtml(p.question)}
+ ${expiryBadge}
+
+ ${optionsHtml}
+
+
`;
+}
+
+/* ============================================================
+ 📊 DASHBOARD ENHANCED
+============================================================ */
+
+/* ============================================================
+ 🔙 BACK BUTTON FIX - إصلاح زر الرجوع
+============================================================ */
+// Override the existing popstate handler with a better one
+window.removeEventListener('popstate', window._nabdhPopstate);
+window._nabdhPopstate = function(e) {
+ // Check modals first
+ const modals = ['sosModal','profileQrModal','personModal','chatModal','marketModal','mapLayersPanel'];
+ for (const id of modals) {
+ const el = document.getElementById(id);
+ if (el && !el.classList.contains('hidden')) {
+ el.classList.add('hidden');
+ mapLayersPanelOpen = false;
+ history.pushState({ section: currentSection }, '', '#' + currentSection);
+ return;
+ }
+ }
+ // Check side menu
+ const menu = document.getElementById('sideMenu');
+ if (menu && !menu.classList.contains('hidden')) {
+ menu.classList.add('hidden');
+ const overlay = document.getElementById('menuOverlay');
+ if (overlay) overlay.classList.add('hidden');
+ history.pushState({ section: currentSection }, '', '#' + currentSection);
+ return;
+ }
+ // Navigate back through section history
+ if (_sectionHistory.length > 0) {
+ const prev = _sectionHistory.pop();
+ _historyPushing = true;
+ goSection(prev, false);
+ _historyPushing = false;
+ history.pushState({ section: prev }, '', '#' + prev);
+ } else if (currentSection !== 'home') {
+ _historyPushing = true;
+ goSection('home', false);
+ _historyPushing = false;
+ history.pushState({ section: 'home' }, '', '#home');
+ } else {
+ // Already at home - push state to prevent exit
+ history.pushState({ section: 'home' }, '', '#home');
+ }
+};
+window.addEventListener('popstate', window._nabdhPopstate);
+
+
+/* ============================================================
+ 🗺️ MAP v3 - تحسينات الخريطة الشاملة
+============================================================ */
+
+// مسار الرحلة
+let routeLayer = null;
+let routeOrigin = null;
+let routeDestination = null;
+
+function clearRoute() {
+ if (routeLayer) { map.removeLayer(routeLayer); routeLayer = null; }
+ routeOrigin = null; routeDestination = null;
+ showToast('🗺️ تم مسح المسار', 'success');
+}
+
+function drawRoute(fromLat, fromLng, toLat, toLng, label) {
+ if (!map) return;
+ if (routeLayer) map.removeLayer(routeLayer);
+ const pts = [[fromLat, fromLng], [toLat, toLng]];
+ routeLayer = L.polyline(pts, {
+ color: '#1abc9c', weight: 4, opacity: 0.8,
+ dashArray: '8,6', lineCap: 'round', lineJoin: 'round'
+ }).addTo(map);
+ map.fitBounds(routeLayer.getBounds(), { padding: [40, 40], animate: true });
+ const d = haversine(fromLat, fromLng, toLat, toLng);
+ const time = Math.round(d / 40 * 60);
+ showToast(`📍 المسافة: ${d < 1 ? '<1' : Math.round(d)} كم • ~${time} دقيقة`, 'success');
+ // Popup on route
+ const mid = [(fromLat+toLat)/2, (fromLng+toLng)/2];
+ L.popup({ className: 'custom-popup', closeButton: true })
+ .setLatLng(mid)
+ .setContent(`
+
🗺️ ${escHtml(label||'مسار')}
+
${d<1?'<1':Math.round(d)} كم • ~${time} دقيقة
+
✕ مسح المسار
+
`)
+ .openOn(map);
+}
+
+function navigateToPin(lat, lng, name) {
+ if (!userLat) { showToast('⚠️ الموقع غير محدد', 'error'); return; }
+ drawRoute(userLat, userLng, lat, lng, name);
+}
+
+// تحسين addMapPin - إضافة زر المسار
+const _origAddMapPin = addMapPin;
+
+// تحسين مسار للمستخدم
+function navigateToUser(userId, name, lat, lng) {
+ if (!userLat) { showToast('⚠️ الموقع غير محدد', 'error'); return; }
+ goSection('map');
+ setTimeout(() => drawRoute(userLat, userLng, lat, lng, 'إلى ' + name), 300);
+}
+
+// Map click: إضافة دبوس مؤقت
+let tempPin = null;
+function addTempPin(lat, lng) {
+ if (tempPin) { map.removeLayer(tempPin); tempPin = null; }
+ const icon = L.divIcon({
+ className: 'custom-marker',
+ html: '📌
',
+ iconSize: [36, 36], iconAnchor: [18, 36]
+ });
+ tempPin = L.marker([lat, lng], { icon }).addTo(map).bindPopup(
+ `
+
${lat.toFixed(5)}, ${lng.toFixed(5)}
+
📢 بلّغ عن هذا الموقع
+
`,
+ { className: 'custom-popup' }
+ ).openPopup();
+}
+
+// تحديث initMap لإضافة ميزات جديدة
+const _origInitMap = initMap;
+
+/* ============================================================
+ 🏥 HOSPITALS v2 - دليل المستشفيات المحسّن
+============================================================ */
+let _hospMap = null;
+
+function initHospMap() {
+ if (_hospMap) return;
+ const el = document.getElementById('hospMapMini');
+ if (!el) return;
+ _hospMap = L.map('hospMapMini', { zoomControl: false, attributionControl: false }).setView([15.5007, 32.5599], 12);
+ L.tileLayer(MAP_TILES.dark, { maxZoom: 19, subdomains: 'abcd' }).addTo(_hospMap);
+}
+
+function showHospOnMiniMap(lat, lng, name, type) {
+ initHospMap();
+ if (!_hospMap) return;
+ _hospMap.setView([lat, lng], 15, { animate: true });
+ const icons = { 'مستشفى': '🏥', 'عيادة': '🩺', 'مختبر': '🔬', 'صيدلية': '💊', 'طوارئ': '🚨' };
+ const icon = L.divIcon({
+ className: 'custom-marker',
+ html: `${icons[type]||'🏥'}
`,
+ iconSize: [32,32], iconAnchor: [16,32]
+ });
+ L.marker([lat, lng], { icon }).addTo(_hospMap)
+ .bindPopup(``, { className: 'custom-popup' })
+ .openPopup();
+ const el = document.getElementById('hospMapMiniWrap');
+ if (el) el.classList.remove('hidden');
+ setTimeout(() => _hospMap.invalidateSize(), 200);
+}
+
+function searchHospitalsNear() {
+ if (!userLat) { showToast('⚠️ الموقع غير محدد', 'error'); return; }
+ const list = (_allHospitals || []).filter(h => h.lat && h.lng && haversine(userLat,userLng,h.lat,h.lng) <= 20);
+ list.sort((a,b) => (dist(a)||999) - (dist(b)||999));
+ renderHospitals(list);
+ showToast(`🏥 ${list.length} مرفق صحي في دائرة 20 كم`, 'success');
+}
+
+/* ============================================================
+ 📰 NEWS v2 - الأخبار المحسّنة
+============================================================ */
+function bookmarkNews(id, btn) {
+ const saved = JSON.parse(localStorage.getItem('nabdh_saved_news') || '[]');
+ const idx = saved.indexOf(id);
+ if (idx >= 0) {
+ saved.splice(idx, 1);
+ btn && (btn.textContent = '🔖');
+ showToast('تم إلغاء الحفظ', 'success');
+ } else {
+ saved.push(id);
+ btn && (btn.textContent = '✅');
+ showToast('✅ تم حفظ الخبر', 'success');
+ }
+ localStorage.setItem('nabdh_saved_news', JSON.stringify(saved));
+}
+
+function filterNewsBySearch() {
+ const q = (document.getElementById('newsSearchInp')||{value:''}).value.trim().toLowerCase();
+ if (!q) { renderNews(_allNews); return; }
+ const filtered = (_allNews||[]).filter(n =>
+ (n.title||'').toLowerCase().includes(q) ||
+ (n.body||'').toLowerCase().includes(q) ||
+ (n.area||'').toLowerCase().includes(q)
+ );
+ renderNews(filtered);
+}
+
+/* ============================================================
+ 🚗 RIDES v2 - مشاركة الرحلات المحسّنة
+============================================================ */
+function showRideOnMap(fromLat, fromLng, toLat, toLng, label) {
+ if (!fromLat || !toLat) { showToast('⚠️ إحداثيات غير متوفرة', 'error'); return; }
+ goSection('map');
+ setTimeout(() => {
+ if (map) drawRoute(fromLat, fromLng, toLat, toLng, label || 'رحلة');
+ }, 350);
+}
+
+function filterRidesByDate() {
+ const today = new Date().toDateString();
+ const todayRides = (_allRides||[]).filter(r => {
+ if (!r.date) return false;
+ return new Date(r.date).toDateString() === today;
+ });
+ renderRides(todayRides.length ? todayRides : _allRides);
+ showToast(`🚗 ${todayRides.length} رحلة متاحة اليوم`, 'success');
+}
+
+/* ============================================================
+ 💧 WATER v2 - تقارير المياه المحسّنة
+============================================================ */
+function showWaterOnMap(lat, lng, type, area) {
+ if (!lat || !lng) { showToast('⚠️ الموقع غير متوفر', 'error'); return; }
+ goSection('map');
+ setTimeout(() => {
+ if (map) {
+ map.setView([lat, lng], 15, { animate: true });
+ const icons = { 'cut': '💧', 'low': '🔵', 'dirty': '⚠️', 'dist': '🚰' };
+ const ico = icons[type] || '💧';
+ L.popup({ className: 'custom-popup' })
+ .setLatLng([lat, lng])
+ .setContent(``)
+ .openOn(map);
+ }
+ }, 350);
+}
+
+/* ============================================================
+ 🎓 STUDY GROUPS v2 - مجموعات الدراسة المحسّنة
+============================================================ */
+function searchStudyGroups() {
+ const q = (document.getElementById('studySearchInp')||{value:''}).value.trim().toLowerCase();
+ const list = Object.values(_allStudyGroups || {});
+ if (!q) { renderStudyGroups(list); return; }
+ const filtered = list.filter(g =>
+ (g.name||'').toLowerCase().includes(q) ||
+ (g.subject||'').toLowerCase().includes(q) ||
+ (g.area||'').toLowerCase().includes(q)
+ );
+ renderStudyGroups(filtered);
+}
+
+function filterStudyByLevel(level, btn) {
+ document.querySelectorAll('.study-level-filt').forEach(b => b.classList.remove('active'));
+ if (btn) btn.classList.add('active');
+ const list = Object.values(_allStudyGroups || {});
+ const filtered = level === 'all' ? list : list.filter(g => g.level === level);
+ renderStudyGroups(filtered);
+}
+
+/* ============================================================
+ 📦 HELP v2 - طلبات المساعدة المحسّنة
+============================================================ */
+function showHelpOnMap(lat, lng, type, title) {
+ if (!lat || !lng) { showToast('⚠️ الموقع غير متوفر', 'error'); return; }
+ goSection('map');
+ setTimeout(() => {
+ if (map) {
+ map.setView([lat, lng], 15, { animate: true });
+ const icons = { food:'🍞', medicine:'💊', transport:'🚗', shelter:'🏠', money:'💰', other:'📦' };
+ L.popup({ className: 'custom-popup' })
+ .setLatLng([lat, lng])
+ .setContent(``)
+ .openOn(map);
+ }
+ }, 350);
+}
+
+function closeHelpRequest(id, btn) {
+ if (!confirm('هل تريد إغلاق هذا الطلب؟')) return;
+ if (btn) { btn.disabled = true; btn.textContent = '⏳'; }
+ fetch('/api/help/' + id + '/close', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ userId: myUserId })
+ }).then(r => r.json()).then(() => {
+ showToast('✅ تم إغلاق الطلب', 'success');
+ loadHelpRequests();
+ }).catch(() => {
+ if (btn) { btn.disabled = false; btn.textContent = '✓ تم التوفير'; }
+ showToast('❌ خطأ', 'error');
+ });
+}
+
+/* ============================================================
+ 🗳️ POLLS v2 - الاستطلاعات المحسّنة
+============================================================ */
+function sharePoll(id, question) {
+ const text = `🗳️ استطلاع نبض:\n${question}\n\nأضف رأيك الآن في تطبيق نبض! 💓`;
+ if (navigator.share) navigator.share({ title: 'استطلاع نبض', text }).catch(() => {});
+ else navigator.clipboard && navigator.clipboard.writeText(text).then(() => showToast('✅ تم نسخ الاستطلاع', 'success'));
+}
+
+/* ============================================================
+ 🌡️ WEATHER v2 - الطقس المحسّن
+============================================================ */
+function getWeatherForHospital(lat, lng, name) {
+ if (!lat || !lng) return;
+ loadWeather(lat, lng, name);
+ goSection('weather');
+}
+
+/* ============================================================
+ 📊 DASHBOARD v2 - لوحة التحكم المحسّنة
+============================================================ */
+function refreshDashboard() {
+ const btn = document.getElementById('dashRefreshBtn');
+ if (btn) { btn.textContent = '⏳'; btn.disabled = true; }
+ loadDashboard();
+ setTimeout(() => {
+ if (btn) { btn.textContent = '🔄 تحديث'; btn.disabled = false; }
+ }, 2000);
+}
+
+/* ============================================================
+ 🔔 NOTIFICATIONS v2 - الإشعارات المحسّنة
+============================================================ */
+function showInAppNotif(title, body, icon, action) {
+ const container = document.getElementById('inAppNotifContainer');
+ if (!container) return;
+ const id = 'notif_' + Date.now();
+ const el = document.createElement('div');
+ el.className = 'in-app-notif';
+ el.id = id;
+ el.innerHTML = `
+ ${icon || '🔔'}
+
+
${escHtml(title)}
+
${escHtml(body)}
+
+ ✕
+ `;
+ if (action) el.onclick = (e) => { if (!e.target.closest('.ian-close')) { action(); el.remove(); } };
+ container.appendChild(el);
+ el.classList.add('slide-in');
+ setTimeout(() => { if (el.parentNode) { el.classList.add('fade-out'); setTimeout(() => el.remove(), 500); } }, 5000);
+}
+
+/* ============================================================
+ 🎨 UI HELPERS v2 - مساعدات الواجهة
+============================================================ */
+function openSearchInSection(sectionName) {
+ goSection(sectionName);
+ setTimeout(() => {
+ const searchMap = {
+ hospitals: 'hospSearchInp',
+ news: 'newsSearchInp',
+ rides: 'ridesSearchInp',
+ water: 'waterSearchInp',
+ study: 'studySearchInp',
+ help: 'helpSearchInp',
+ };
+ const inp = document.getElementById(searchMap[sectionName]);
+ if (inp) inp.focus();
+ }, 300);
+}
+
+function formatNumber(n) {
+ if (!n) return '0';
+ if (n >= 1000) return (n/1000).toFixed(1) + 'k';
+ return String(n);
+}
+
+// تحديث عداد البيانات في الوقت الفعلي
+function startLiveCounters() {
+ setInterval(async () => {
+ try {
+ const stats = await fetch('/api/stats').then(r => r.json());
+ if (stats) {
+ animateCount('liveUsers', stats.users || 0);
+ animateCount('liveReports', stats.reports || 0);
+ const rate = (stats.reports > 0 && stats.users > 0) ? Math.round(stats.reports / Math.max(stats.users,1)) : 0;
+ const rateEl = document.getElementById('liveRate');
+ if (rateEl) rateEl.textContent = rate;
+ }
+ } catch {}
+ }, 30000);
+}
+setTimeout(startLiveCounters, 5000);
+
+/* ============================================================
+ 📱 PWA INSTALL v2
+============================================================ */
+function checkInstallState() {
+ if (window.matchMedia('(display-mode: standalone)').matches) {
+ const btn = document.getElementById('menuInstallBtn');
+ if (btn) btn.style.display = 'none';
+ }
+}
+setTimeout(checkInstallState, 1000);
+
+/* ============================================================
+ 🔄 AUTO REFRESH - تحديث تلقائي
+============================================================ */
+setInterval(() => {
+ if (currentSection === 'map') {
+ fetch('/api/map').then(r => r.json()).then(pins => {
+ pins.forEach(p => { if (!mapMarkers[p.id]) addMapPin(p); });
+ updateMapCounts();
+ }).catch(() => {});
+ }
+}, 60000);
+
+// تحديث الطقس على شريط الخريطة كل 5 دقائق
+setInterval(updateMapWeatherMini, 300000);
+
+
+
+/* ============================================================
+ HOSPITALS v2 - دليل المستشفيات
+============================================================ */
+
+/* ============================================================
+ NEWS v2 - الاخبار المحسّنة
+============================================================ */
+
+/* ============================================================
+ RIDES v2 - مشاركة الرحلات
+============================================================ */
+
+/* ============================================================
+ WATER v2 - تقارير المياه
+============================================================ */
+
+/* ============================================================
+ STUDY GROUPS v2 - مجموعات الدراسة
+============================================================ */
+function searchStudyGroupsFn() {
+ var q = ((document.getElementById('studySearchInp')||{}).value||'').trim().toLowerCase();
+ var list = Object.values(_allStudyGroups || {});
+ if (!q) { renderStudyGroups(list); return; }
+ renderStudyGroups(list.filter(function(g) {
+ return (g.name||'').toLowerCase().includes(q) || (g.subject||'').toLowerCase().includes(q) || (g.area||'').toLowerCase().includes(q);
+ }));
+}
+
+/* ============================================================
+ HELP v2 - طلبات المساعدة
+============================================================ */
+
+/* ============================================================
+ POLLS v2 - الاستطلاعات
+============================================================ */
+
+/* ============================================================
+ DASHBOARD v2 - لوحة التحكم
+============================================================ */
+
+/* ============================================================
+ IN-APP NOTIFICATIONS - اشعارات داخل التطبيق
+============================================================ */
+
+/* ============================================================
+ LIVE COUNTERS - عدادات حية
+============================================================ */
+setTimeout(startLiveCounters, 8000);
+
+/* ============================================================
+ AUTO MAP REFRESH - تحديث تلقائي للخريطة
+============================================================ */
+setInterval(function() {
+ if (currentSection === 'map') {
+ fetch('/api/map').then(function(r){ return r.json(); }).then(function(pins) {
+ pins.forEach(function(p){ if (!mapMarkers[p.id]) addMapPin(p); });
+ updateMapCounts();
+ }).catch(function(){});
+ }
+}, 60000);
+
+setInterval(updateMapWeatherMini, 300000);
+
+/* ================================================================
+ 🎓 GROUP PAGE SYSTEM - نظام صفحة المجموعة الكاملة
+ ================================================================ */
+
+// ── State ──────────────────────────────────────────────────────
+var gpCurrentGroup = null; // full group object
+var gpMessages = []; // cached messages
+var gpReplyingTo = null; // { id, text, author }
+var gpVoiceRecorder = null;
+var gpVoiceChunks = [];
+var gpVoiceTimer = null;
+var gpVoiceSeconds = 0;
+var gpMediaRecorder = null;
+var gpActiveTab = 'chat';
+var gpMediaTabActive = 'images';
+
+// ── Call State ─────────────────────────────────────────────────
+var gpPeerConnections = {}; // peerConnections map { socketId: RTCPeerConnection }
+var gpLocalStream = null;
+var gpCallActive = false;
+var gpCallType = null; // 'voice' | 'video'
+var gpCallTimer = null;
+var gpCallSeconds = 0;
+var gpCallIsGroup = false;
+
+// ── DM Call State ──────────────────────────────────────────────
+var dmPeer = null;
+var dmLocalStream = null;
+var dmCallActive = false;
+var dmCallType = null;
+var dmCallTimer = null;
+var dmCallSeconds = 0;
+var dmCurrentUser = null; // { id, name, socketId }
+var dmReplyingTo = null;
+
+// ── WebRTC config ──────────────────────────────────────────────
+var ICE_SERVERS = {
+ iceServers: [
+ { urls: 'stun:stun.l.google.com:19302' },
+ { urls: 'stun:stun1.l.google.com:19302' }
+ ]
+};
+
+/* ================================================================
+ OPEN / CLOSE GROUP PAGE
+ ================================================================ */
+function openGroupPage(groupId) {
+ fetch('/api/study/' + groupId)
+ .then(function(r){ return r.json(); })
+ .then(function(g) {
+ if (g.error) { showToast('لم يتم العثور على المجموعة', 'error'); return; }
+ gpCurrentGroup = g;
+ gpMessages = [];
+ gpReplyingTo = null;
+
+ // Fill header
+ var levelEmojis = { ابتدائي:'🌱', متوسط:'📗', ثانوي:'📘', جامعي:'🎓', مهني:'⚙️', عام:'🌐' };
+ document.getElementById('gpAvatar').textContent = g.avatar || levelEmojis[g.level] || '🎓';
+ document.getElementById('gpName').textContent = g.name;
+ document.getElementById('gpMeta').textContent = (g.members ? g.members.length : 0) + ' عضو • ' + (g.level || 'عام');
+
+ // Show page
+ var page = document.getElementById('groupPage');
+ page.classList.remove('hidden');
+ page.style.transform = 'translateX(100%)';
+ setTimeout(function(){ page.style.transform = 'translateX(0)'; }, 10);
+
+ // Push history state
+ history.pushState({ page: 'group', groupId: groupId }, '', '#group/' + groupId);
+
+ // Join socket room
+ if (socket) socket.emit('join_study', groupId);
+
+ // Register socket events for this group
+ setupGroupSocketEvents(groupId);
+
+ // Load chat
+ switchGroupTab('chat', null);
+ })
+ .catch(function(){ showToast('خطأ في تحميل المجموعة', 'error'); });
+}
+
+function closeGroupPage() {
+ var page = document.getElementById('groupPage');
+ page.style.transform = 'translateX(100%)';
+ setTimeout(function(){ page.classList.add('hidden'); }, 300);
+
+ if (gpCurrentGroup && socket) socket.emit('leave_study', gpCurrentGroup.id);
+
+ // End any active call
+ if (gpCallActive) endGroupCall();
+
+ gpCurrentGroup = null;
+ gpMessages = [];
+
+ // Go back in history
+ if (history.state && history.state.page === 'group') {
+ history.back();
+ }
+}
+
+/* ================================================================
+ GROUP PAGE TABS
+ ================================================================ */
+function switchGroupTab(tab, btn) {
+ gpActiveTab = tab;
+
+ // Update tab buttons
+ document.querySelectorAll('.gp-tab').forEach(function(b) { b.classList.remove('active-gp-tab'); });
+ if (btn) btn.classList.add('active-gp-tab');
+ else {
+ document.querySelectorAll('.gp-tab').forEach(function(b) {
+ if (b.getAttribute('onclick') && b.getAttribute('onclick').includes("'" + tab + "'")) b.classList.add('active-gp-tab');
+ });
+ }
+
+ // Hide all tab content
+ ['gpTabChat', 'gpTabMembers', 'gpTabMedia', 'gpTabInfo'].forEach(function(id) {
+ var el = document.getElementById(id);
+ if (el) el.classList.add('hidden');
+ });
+
+ if (tab === 'chat') {
+ document.getElementById('gpTabChat').classList.remove('hidden');
+ if (gpCurrentGroup) loadGroupMessages(gpCurrentGroup.id);
+ } else if (tab === 'members') {
+ document.getElementById('gpTabMembers').classList.remove('hidden');
+ loadGroupMembers();
+ } else if (tab === 'media') {
+ document.getElementById('gpTabMedia').classList.remove('hidden');
+ loadGroupMedia('images');
+ } else if (tab === 'info') {
+ document.getElementById('gpTabInfo').classList.remove('hidden');
+ renderGroupInfo();
+ }
+}
+
+/* ================================================================
+ LOAD GROUP MESSAGES
+ ================================================================ */
+function loadGroupMessages(groupId) {
+ fetch('/api/study/' + groupId + '/messages')
+ .then(function(r){ return r.json(); })
+ .then(function(msgs) {
+ gpMessages = msgs || [];
+ renderGroupMessages();
+ })
+ .catch(function(){});
+}
+
+function renderGroupMessages() {
+ var container = document.getElementById('gpChatMessages');
+ if (!container) return;
+
+ if (!gpMessages.length) {
+ container.innerHTML = '💬
لا توجد رسائل بعدكن أول من يكتب!
';
+ return;
+ }
+
+ var html = '';
+ var uid = localStorage.getItem('nabdh_uid') || '';
+ var prevDate = '';
+
+ gpMessages.forEach(function(m) {
+ var isMine = m.userId === uid;
+ var msgDate = new Date(m.time || m.createdAt).toLocaleDateString('ar');
+ if (msgDate !== prevDate) {
+ html += '' + msgDate + '
';
+ prevDate = msgDate;
+ }
+
+ var replyHtml = '';
+ if (m.replyTo) {
+ replyHtml = '' + escHtml(m.replyTo.author) + '
' + escHtml((m.replyTo.text||'').slice(0,60)) + '
';
+ }
+
+ var mediaHtml = '';
+ if (m.mediaType === 'image' && m.mediaData) {
+ mediaHtml = '';
+ } else if (m.mediaType === 'video' && m.mediaData) {
+ mediaHtml = '
';
+ } else if (m.mediaType === 'audio' && m.mediaData) {
+ mediaHtml = '';
+ } else if (m.mediaType === 'file' && m.mediaData) {
+ mediaHtml = '';
+ }
+
+ var reactionsHtml = '';
+ if (m.reactions && Object.keys(m.reactions).length) {
+ reactionsHtml = '';
+ Object.entries(m.reactions).forEach(function(pair) {
+ var emoji = pair[0], users = pair[1];
+ var myReact = users.indexOf(uid) >= 0 ? ' my-react' : '';
+ reactionsHtml += '' + emoji + ' ' + users.length + ' ';
+ });
+ reactionsHtml += '
';
+ }
+
+ var timeStr = timeAgo(m.time || new Date(m.createdAt).getTime());
+
+ html += '';
+ if (!isMine) html += '
' + escHtml(m.author) + '
';
+ html += replyHtml;
+ html += mediaHtml;
+ if (m.text) html += '
' + escHtml(m.text) + '
';
+ html += '';
+ html += reactionsHtml;
+ html += '
';
+ });
+
+ container.innerHTML = html;
+ container.scrollTop = container.scrollHeight;
+}
+
+/* ================================================================
+ SEND GROUP MESSAGE
+ ================================================================ */
+function sendGroupMessage() {
+ if (!gpCurrentGroup) return;
+ var input = document.getElementById('gpMsgInput');
+ var text = (input.value || '').trim();
+ if (!text && !gpReplyingTo) return;
+
+ var uid = localStorage.getItem('nabdh_uid') || ('u_' + Math.random().toString(36).slice(2));
+ var uname = localStorage.getItem('nabdh_name') || 'عضو';
+
+ var payload = {
+ text: text,
+ author: uname,
+ userId: uid,
+ replyTo: gpReplyingTo || null
+ };
+
+ input.value = '';
+ input.style.height = '';
+ cancelReply();
+
+ fetch('/api/study/' + gpCurrentGroup.id + '/msg/advanced', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(payload)
+ })
+ .then(function(r){ return r.json(); })
+ .then(function(res) {
+ if (res.success) {
+ gpMessages.push(res.msg);
+ renderGroupMessages();
+ }
+ })
+ .catch(function(){ showToast('فشل إرسال الرسالة', 'error'); });
+}
+
+/* ================================================================
+ REPLY TO MESSAGE
+ ================================================================ */
+function replyToGroupMsg(id, text, author) {
+ gpReplyingTo = { id: id, text: text, author: author };
+ var preview = document.getElementById('gpReplyPreview');
+ var replyText = document.getElementById('gpReplyText');
+ if (preview) preview.classList.remove('hidden');
+ if (replyText) replyText.textContent = author + ': ' + text.slice(0, 60);
+ var input = document.getElementById('gpMsgInput');
+ if (input) input.focus();
+}
+
+function cancelReply() {
+ gpReplyingTo = null;
+ var preview = document.getElementById('gpReplyPreview');
+ if (preview) preview.classList.add('hidden');
+}
+
+/* ================================================================
+ EMOJI REACTIONS
+ ================================================================ */
+function showEmojiReactMenu(msgId, triggerEl) {
+ var existing = document.getElementById('emojiReactMenu');
+ if (existing) existing.remove();
+
+ var emojis = ['👍','❤️','😂','😮','😢','😡','🔥','👏','🎉','💯'];
+ var menu = document.createElement('div');
+ menu.id = 'emojiReactMenu';
+ menu.className = 'emoji-react-menu';
+ menu.innerHTML = emojis.map(function(e) {
+ return '' + e + ' ';
+ }).join('');
+
+ document.body.appendChild(menu);
+ var rect = triggerEl.getBoundingClientRect();
+ menu.style.top = (rect.top - 50) + 'px';
+ menu.style.left = Math.max(10, rect.left - 60) + 'px';
+ menu.style.position = 'fixed';
+
+ setTimeout(function() {
+ document.addEventListener('click', function removeMenu(e) {
+ if (!menu.contains(e.target)) { menu.remove(); document.removeEventListener('click', removeMenu); }
+ });
+ }, 100);
+}
+
+function reactToMessage(msgId, emoji) {
+ if (!gpCurrentGroup) return;
+ var uid = localStorage.getItem('nabdh_uid') || '';
+ fetch('/api/study/' + gpCurrentGroup.id + '/msg/' + msgId + '/react', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ emoji: emoji, userId: uid })
+ })
+ .then(function(r){ return r.json(); })
+ .then(function(res) {
+ var msg = gpMessages.find(function(m){ return m.id === msgId; });
+ if (msg && res.reactions) { msg.reactions = res.reactions; renderGroupMessages(); }
+ })
+ .catch(function(){});
+}
+
+/* ================================================================
+ EMOJI PICKER (send in message)
+ ================================================================ */
+function toggleEmojiPicker() {
+ var picker = document.getElementById('gpEmojiPicker');
+ if (!picker) return;
+ picker.classList.toggle('hidden');
+
+ // make emoji spans clickable
+ picker.querySelectorAll('.gep-grid').forEach(function(grid) {
+ grid.onclick = function(e) {
+ var emoji = e.target.textContent.trim();
+ if (emoji) {
+ var inp = document.getElementById('gpMsgInput');
+ if (inp) { inp.value += emoji; inp.focus(); }
+ picker.classList.add('hidden');
+ }
+ };
+ });
+}
+
+function toggleDMEmojiPicker() {
+ var picker = document.getElementById('dmEmojiPicker');
+ if (!picker) return;
+ picker.classList.toggle('hidden');
+ picker.querySelectorAll('.gep-grid').forEach(function(grid) {
+ grid.onclick = function(e) {
+ var emoji = e.target.textContent.trim();
+ if (emoji) {
+ var inp = document.getElementById('dmMsgInput');
+ if (inp) { inp.value += emoji; inp.focus(); }
+ picker.classList.add('hidden');
+ }
+ };
+ });
+}
+
+/* ================================================================
+ MEDIA UPLOAD (Images & Videos)
+ ================================================================ */
+/* ── Media menu helpers (Group) ──────────────────────────── */
+function toggleGpMediaMenu() {
+ var menu = document.getElementById('gpMediaMenu');
+ var btn = document.getElementById('gpAttachBtn');
+ if (!menu) return;
+ var isOpen = !menu.classList.contains('hidden');
+ // Close DM menu if open
+ var dmMenu = document.getElementById('dmMediaMenu');
+ if (dmMenu) dmMenu.classList.add('hidden');
+ menu.classList.toggle('hidden', isOpen);
+ if (btn) btn.classList.toggle('gpi-attach-open', !isOpen);
+ if (!isOpen) {
+ // close on outside click
+ setTimeout(function() {
+ document.addEventListener('click', function _closeGp(e) {
+ var wrap = document.getElementById('gpMediaWrap');
+ if (wrap && !wrap.contains(e.target)) {
+ menu.classList.add('hidden');
+ if (btn) btn.classList.remove('gpi-attach-open');
+ }
+ document.removeEventListener('click', _closeGp);
+ });
+ }, 10);
+ }
+}
+function closeGpMediaMenu() {
+ var menu = document.getElementById('gpMediaMenu');
+ var btn = document.getElementById('gpAttachBtn');
+ if (menu) menu.classList.add('hidden');
+ if (btn) btn.classList.remove('gpi-attach-open');
+}
+
+/* ── Media menu helpers (DM) ──────────────────────────────── */
+function toggleDmMediaMenu() {
+ var menu = document.getElementById('dmMediaMenu');
+ var btn = document.getElementById('dmAttachBtn');
+ if (!menu) return;
+ var isOpen = !menu.classList.contains('hidden');
+ // Close GP menu if open
+ var gpMenu = document.getElementById('gpMediaMenu');
+ if (gpMenu) gpMenu.classList.add('hidden');
+ menu.classList.toggle('hidden', isOpen);
+ if (btn) btn.classList.toggle('gpi-attach-open', !isOpen);
+ if (!isOpen) {
+ setTimeout(function() {
+ document.addEventListener('click', function _closeDm(e) {
+ var wrap = document.getElementById('dmMediaWrap');
+ if (wrap && !wrap.contains(e.target)) {
+ menu.classList.add('hidden');
+ if (btn) btn.classList.remove('gpi-attach-open');
+ }
+ document.removeEventListener('click', _closeDm);
+ });
+ }, 10);
+ }
+}
+function closeDmMediaMenu() {
+ var menu = document.getElementById('dmMediaMenu');
+ var btn = document.getElementById('dmAttachBtn');
+ if (menu) menu.classList.add('hidden');
+ if (btn) btn.classList.remove('gpi-attach-open');
+}
+
+/* ── triggerMediaPicker — Group ───────────────────────────── */
+function triggerMediaPicker(type) {
+ var map = { image:'gpImageInput', camera:'gpCameraInput', video:'gpVideoInput', audio:'gpAudioInput', file:'gpFileInput' };
+ var el = document.getElementById(map[type] || 'gpFileInput');
+ if (el) { el.value = ''; el.click(); }
+ else showToast('عنصر الرفع غير موجود', 'error');
+}
+
+function uploadGroupMedia(input, type) {
+ var file = input.files[0];
+ if (!file) return;
+
+ // Size limits by type
+ var limits = { image: 5, video: 50, audio: 20, file: 20 };
+ var maxMB = limits[type] || 20;
+ if (file.size > maxMB * 1024 * 1024) {
+ showToast('الملف كبير جداً (الحد ' + maxMB + 'MB)', 'error');
+ input.value = '';
+ return;
+ }
+
+ var icons = { image:'🖼️', video:'🎬', audio:'🎵', file:'📄' };
+ showToast(icons[type] + ' جاري الرفع...', 'info');
+
+ var reader = new FileReader();
+ reader.onload = function(e) {
+ var uid = localStorage.getItem('nabdh_uid') || '';
+ var uname = localStorage.getItem('nabdh_name') || 'عضو';
+ if (!gpCurrentGroup) { showToast('لم يتم تحديد المجموعة', 'error'); return; }
+ fetch('/api/study/' + gpCurrentGroup.id + '/msg/advanced', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ text: '', author: uname, userId: uid,
+ mediaType: type, mediaData: e.target.result, mediaName: file.name
+ })
+ })
+ .then(function(r){ return r.json(); })
+ .then(function(res) {
+ if (res.success) {
+ gpMessages.push(res.msg);
+ renderGroupMessages();
+ showToast('تم الإرسال ✓', 'success');
+ } else {
+ showToast('فشل إرسال الوسيط', 'error');
+ }
+ })
+ .catch(function(){ showToast('فشل رفع الوسائط', 'error'); });
+ input.value = '';
+ };
+ reader.readAsDataURL(file);
+}
+
+/* ================================================================
+ VOICE RECORDING
+ ================================================================ */
+function startVoiceRecord() {
+ if (!navigator.mediaDevices) { showToast('المتصفح لا يدعم التسجيل', 'error'); return; }
+ navigator.mediaDevices.getUserMedia({ audio: true })
+ .then(function(stream) {
+ gpVoiceChunks = [];
+ gpVoiceSeconds = 0;
+ var options = {};
+ try { options = { mimeType: 'audio/webm;codecs=opus' }; } catch(e) {}
+ gpVoiceRecorder = new MediaRecorder(stream, options);
+ gpVoiceRecorder.ondataavailable = function(e) { if (e.data.size>0) gpVoiceChunks.push(e.data); };
+ gpVoiceRecorder.onstop = function() {
+ stream.getTracks().forEach(function(t){ t.stop(); });
+ finishVoiceRecord();
+ };
+ gpVoiceRecorder.start();
+
+ // Show recording bar
+ var bar = document.getElementById('gpVoiceRecordingBar');
+ if (bar) bar.classList.remove('hidden');
+ gpVoiceTimer = setInterval(function() {
+ gpVoiceSeconds++;
+ var el = document.getElementById('gpVoiceRecTime');
+ if (el) el.textContent = Math.floor(gpVoiceSeconds/60) + ':' + ('0'+gpVoiceSeconds%60).slice(-2);
+ if (gpVoiceSeconds >= 120) stopVoiceRecord();
+ }, 1000);
+ })
+ .catch(function(){ showToast('لا يمكن الوصول إلى المايكروفون', 'error'); });
+}
+
+function stopVoiceRecord() {
+ if (gpVoiceRecorder && gpVoiceRecorder.state !== 'inactive') {
+ gpVoiceRecorder.stop();
+ }
+ clearInterval(gpVoiceTimer);
+ var bar = document.getElementById('gpVoiceRecordingBar');
+ if (bar) bar.classList.add('hidden');
+}
+
+function cancelVoiceRecord() {
+ if (gpVoiceRecorder && gpVoiceRecorder.state !== 'inactive') {
+ gpVoiceRecorder.stream && gpVoiceRecorder.stream.getTracks().forEach(function(t){ t.stop(); });
+ gpVoiceRecorder.stop();
+ }
+ clearInterval(gpVoiceTimer);
+ gpVoiceChunks = [];
+ var bar = document.getElementById('gpVoiceRecordingBar');
+ if (bar) bar.classList.add('hidden');
+}
+
+function finishVoiceRecord() {
+ if (!gpVoiceChunks.length || !gpCurrentGroup) return;
+ var blob = new Blob(gpVoiceChunks, { type: 'audio/webm' });
+ if (gpVoiceSeconds < 1) { showToast('التسجيل قصير جداً', 'error'); return; }
+
+ var reader = new FileReader();
+ reader.onload = function(e) {
+ var uid = localStorage.getItem('nabdh_uid') || '';
+ var uname = localStorage.getItem('nabdh_name') || 'عضو';
+ fetch('/api/study/' + gpCurrentGroup.id + '/msg/advanced', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ text: '🎵 رسالة صوتية (' + Math.floor(gpVoiceSeconds/60) + ':' + ('0'+gpVoiceSeconds%60).slice(-2) + ')',
+ author: uname, userId: uid,
+ mediaType: 'audio', mediaData: e.target.result
+ })
+ })
+ .then(function(r){ return r.json(); })
+ .then(function(res) {
+ if (res.success) { gpMessages.push(res.msg); renderGroupMessages(); }
+ })
+ .catch(function(){});
+ };
+ reader.readAsDataURL(blob);
+}
+
+/* ================================================================
+ TYPING INDICATOR
+ ================================================================ */
+var gpTypingTimeout = null;
+function onGroupTyping() {
+ if (!gpCurrentGroup || !socket) return;
+ socket.emit('study_typing', { groupId: gpCurrentGroup.id, name: localStorage.getItem('nabdh_name') || 'عضو' });
+ clearTimeout(gpTypingTimeout);
+ gpTypingTimeout = setTimeout(function(){}, 2000);
+}
+
+/* ================================================================
+ MEMBERS TAB
+ ================================================================ */
+function loadGroupMembers() {
+ if (!gpCurrentGroup) return;
+ fetch('/api/study/' + gpCurrentGroup.id)
+ .then(function(r){ return r.json(); })
+ .then(function(g) {
+ gpCurrentGroup = g;
+ renderGroupMembers(g);
+ })
+ .catch(function(){});
+}
+
+function renderGroupMembers(g) {
+ var cnt = document.getElementById('gpMembersCount');
+ var list = document.getElementById('gpMembersList');
+ if (!list) return;
+
+ var members = g.members || [];
+ if (cnt) cnt.textContent = members.length + ' / ' + (g.maxMembers || 20) + ' عضو';
+
+ if (!members.length) {
+ list.innerHTML = '';
+ return;
+ }
+
+ var uid = localStorage.getItem('nabdh_uid') || '';
+ list.innerHTML = members.map(function(mid) {
+ var isMe = mid === uid;
+ var isAdmin = mid === g.userId;
+ return '' +
+ '
' + (isMe ? '👤' : '🧑') + '
' +
+ '
' + (isMe ? 'أنت' : 'عضو') +
+ (isAdmin ? ' مشرف ' : '') + '
' +
+ '
' + mid.slice(0,8) + '...
' +
+ ((!isMe && socket) ? '
💬 رسالة ' : '') +
+ '
';
+ }).join('');
+}
+
+/* ================================================================
+ MEDIA TAB
+ ================================================================ */
+function switchMediaTab(tab, btn) {
+ gpMediaTabActive = tab;
+ document.querySelectorAll('.gmt-tab').forEach(function(b){ b.classList.remove('active'); });
+ if (btn) btn.classList.add('active');
+ loadGroupMedia(tab);
+}
+
+function loadGroupMedia(type) {
+ var grid = document.getElementById('gpMediaGrid');
+ if (!grid || !gpCurrentGroup) return;
+
+ var filtered = gpMessages.filter(function(m){ return m.mediaType === type.slice(0,-1) || (type==='audio' && m.mediaType==='audio'); });
+
+ // re-map: images→image, videos→video, audio→audio, files→file
+ var typeMap = { images:'image', videos:'video', audio:'audio', files:'file' };
+ var targetType = typeMap[type] || type;
+ filtered = gpMessages.filter(function(m){ return m.mediaType === targetType; });
+
+ if (!filtered.length) {
+ grid.innerHTML = '' + (type==='images'?'📷':type==='videos'?'🎬':type==='audio'?'🎵':'📁') + '
لا توجد وسائط من هذا النوع
';
+ return;
+ }
+
+ if (type === 'images') {
+ grid.innerHTML = filtered.map(function(m) {
+ return '' + escHtml(m.author) + '
';
+ }).join('');
+ } else if (type === 'videos') {
+ grid.innerHTML = filtered.map(function(m) {
+ return '' + escHtml(m.author) + '
';
+ }).join('');
+ } else if (type === 'audio') {
+ grid.innerHTML = filtered.map(function(m) {
+ return '🎵
' + escHtml(m.author) + ' • ' + timeAgo(m.time) + '
';
+ }).join('');
+ }
+}
+
+/* ================================================================
+ INFO TAB
+ ================================================================ */
+function renderGroupInfo() {
+ var g = gpCurrentGroup;
+ if (!g) return;
+ var el = document.getElementById('gpInfoContent');
+ if (!el) return;
+
+ el.innerHTML = '' +
+ '
📚 المادة ' + escHtml(g.subject) + '
' +
+ '
📊 المستوى ' + escHtml(g.level||'عام') + '
' +
+ '
👥 الأعضاء ' + (g.members?g.members.length:0) + ' / ' + (g.maxMembers||20) + '
' +
+ (g.schedule ? '
📅 المواعيد ' + escHtml(g.schedule) + '
' : '') +
+ (g.area ? '
📍 المنطقة ' + escHtml(g.area) + '
' : '') +
+ (g.contact ? '
📞 التواصل ' + escHtml(g.contact) + '
' : '') +
+ '
📅 تأسست ' + timeAgo(g.time||new Date(g.createdAt).getTime()) + '
' +
+ '
' +
+ '' +
+ '🔗 دعوة أصدقاء ' +
+ '🚪 مغادرة المجموعة ' +
+ '
';
+}
+
+/* ================================================================
+ INVITE LINK
+ ================================================================ */
+function showGroupInvite() {
+ if (!gpCurrentGroup) return;
+ var modal = document.getElementById('gpInviteModal');
+ var linkEl = document.getElementById('gpInviteLink');
+ if (!modal || !linkEl) return;
+
+ linkEl.textContent = 'جاري توليد الرابط...';
+ modal.classList.remove('hidden');
+
+ fetch('/api/study/' + gpCurrentGroup.id + '/invite', { method: 'POST' })
+ .then(function(r){ return r.json(); })
+ .then(function(res) {
+ var base = window.location.origin;
+ var link = base + '/?join=' + res.token;
+ linkEl.textContent = link;
+
+ // Simple QR via text
+ var qrEl = document.getElementById('gpInviteQR');
+ if (qrEl) qrEl.innerHTML = '🔗 ' + link + '
';
+ })
+ .catch(function(){ linkEl.textContent = 'فشل توليد الرابط'; });
+}
+
+function copyGroupLink() {
+ var linkEl = document.getElementById('gpInviteLink');
+ if (!linkEl) return;
+ var text = linkEl.textContent;
+ if (navigator.clipboard) {
+ navigator.clipboard.writeText(text).then(function(){ showToast('تم نسخ الرابط ✓', 'success'); });
+ } else {
+ var el = document.createElement('textarea');
+ el.value = text;
+ document.body.appendChild(el);
+ el.select();
+ document.execCommand('copy');
+ document.body.removeChild(el);
+ showToast('تم نسخ الرابط ✓', 'success');
+ }
+}
+
+function shareGroupLink() {
+ var linkEl = document.getElementById('gpInviteLink');
+ var groupName = gpCurrentGroup ? gpCurrentGroup.name : 'مجموعة';
+ var url = linkEl ? linkEl.textContent : '';
+
+ if (navigator.share) {
+ navigator.share({
+ title: 'انضم لمجموعة ' + groupName,
+ text: 'مرحباً! انضم إلينا في مجموعة "' + groupName + '" على تطبيق نبض',
+ url: url
+ }).catch(function(){});
+ } else {
+ copyGroupLink();
+ }
+}
+
+/* ================================================================
+ LEAVE GROUP
+ ================================================================ */
+function leaveGroup() {
+ if (!gpCurrentGroup) return;
+ if (!confirm('هل تريد مغادرة المجموعة "' + gpCurrentGroup.name + '"؟')) return;
+ var uid = localStorage.getItem('nabdh_uid') || '';
+ fetch('/api/study/' + gpCurrentGroup.id + '/leave', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ userId: uid })
+ })
+ .then(function(){ showToast('غادرت المجموعة', 'info'); closeGroupPage(); })
+ .catch(function(){ showToast('خطأ في المغادرة', 'error'); });
+}
+
+/* ================================================================
+ AUTO-RESIZE TEXTAREA
+ ================================================================ */
+function autoResizeTA(el) {
+ el.style.height = 'auto';
+ el.style.height = Math.min(el.scrollHeight, 120) + 'px';
+}
+
+/* ================================================================
+ VIEW FULL IMAGE
+ ================================================================ */
+function viewFullImage(src) {
+ var overlay = document.createElement('div');
+ overlay.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,.93);z-index:9999;display:flex;align-items:center;justify-content:center;cursor:zoom-out';
+ overlay.innerHTML = ' ';
+ overlay.onclick = function(){ document.body.removeChild(overlay); };
+ document.body.appendChild(overlay);
+}
+
+/* ================================================================
+ SOCKET EVENTS FOR GROUP
+ ================================================================ */
+function setupGroupSocketEvents(groupId) {
+ if (!socket) return;
+
+ // Remove old listeners first
+ socket.off('study_msg');
+ socket.off('study_react');
+ socket.off('study_typing');
+ socket.off('study_join');
+
+ socket.on('study_msg', function(data) {
+ if (!gpCurrentGroup || data.groupId !== gpCurrentGroup.id) return;
+ if (!gpMessages.find(function(m){ return m.id === data.msg.id; })) {
+ gpMessages.push(data.msg);
+ if (gpActiveTab === 'chat') renderGroupMessages();
+ }
+ });
+
+ socket.on('study_react', function(data) {
+ if (!gpCurrentGroup || data.groupId !== gpCurrentGroup.id) return;
+ var msg = gpMessages.find(function(m){ return m.id === data.msgId; });
+ if (msg) { msg.reactions = data.reactions; if (gpActiveTab === 'chat') renderGroupMessages(); }
+ });
+
+ socket.on('study_typing', function(data) {
+ var el = document.getElementById('gpTypingIndicator');
+ var txt = document.getElementById('gpTypingText');
+ if (el && txt) {
+ txt.textContent = (data.name || 'شخص ما') + ' يكتب...';
+ el.classList.remove('hidden');
+ clearTimeout(gpTypingTimeout);
+ gpTypingTimeout = setTimeout(function(){ el.classList.add('hidden'); }, 2500);
+ }
+ });
+
+ socket.on('study_join', function(data) {
+ if (gpCurrentGroup && data.id === gpCurrentGroup.id) {
+ gpCurrentGroup.members = data.members;
+ document.getElementById('gpMeta').textContent = data.members.length + ' عضو • ' + (gpCurrentGroup.level || 'عام');
+ if (gpActiveTab === 'members') renderGroupMembers(gpCurrentGroup);
+ }
+ });
+}
+
+/* ================================================================
+ 🎙️ GROUP VOICE / VIDEO CALLS (WebRTC)
+ ================================================================ */
+function startGroupVoiceCall() {
+ if (!gpCurrentGroup) return;
+ gpCallIsGroup = true;
+ initiateGroupCall('voice');
+}
+
+function startGroupVideoCall() {
+ if (!gpCurrentGroup) return;
+ gpCallIsGroup = true;
+ initiateGroupCall('video');
+}
+
+function initiateGroupCall(type) {
+ gpCallType = type;
+ var constraints = type === 'video' ? { audio: true, video: { facingMode: 'user' } } : { audio: true, video: false };
+
+ navigator.mediaDevices.getUserMedia(constraints)
+ .then(function(stream) {
+ gpLocalStream = stream;
+ gpCallActive = true;
+
+ // Show call banner
+ var banner = document.getElementById('gpCallBanner');
+ if (banner) banner.classList.remove('hidden');
+ document.getElementById('gpCallType').textContent = type === 'video' ? '📹 مكالمة مرئية جارية' : '🎙️ مكالمة صوتية جارية';
+
+ // Show video grid if video call
+ if (type === 'video') {
+ var grid = document.getElementById('gpVideoGrid');
+ if (grid) grid.classList.remove('hidden');
+ var localVid = document.getElementById('localVideo');
+ if (localVid) localVid.srcObject = stream;
+ }
+
+ // Start call timer
+ gpCallSeconds = 0;
+ gpCallTimer = setInterval(function() {
+ gpCallSeconds++;
+ var el = document.getElementById('gpCallTime');
+ if (el) el.textContent = Math.floor(gpCallSeconds/60) + ':' + ('0'+gpCallSeconds%60).slice(-2);
+ }, 1000);
+
+ // Notify group via socket
+ if (socket) {
+ socket.emit('call_request', {
+ groupId: gpCurrentGroup.id,
+ from: socket.id,
+ fromName: localStorage.getItem('nabdh_name') || 'عضو',
+ type: type
+ });
+ }
+
+ showToast(type === 'video' ? '📹 بدأت مكالمة مرئية' : '🎙️ بدأت مكالمة صوتية', 'success');
+ })
+ .catch(function(err) {
+ showToast('لا يمكن الوصول إلى الكاميرا/المايكروفون', 'error');
+ console.warn('Media error:', err);
+ });
+}
+
+function toggleMuteCall() {
+ if (!gpLocalStream) return;
+ var audioTracks = gpLocalStream.getAudioTracks();
+ if (!audioTracks.length) return;
+ audioTracks[0].enabled = !audioTracks[0].enabled;
+ var btn = document.getElementById('btnMute');
+ if (btn) btn.textContent = audioTracks[0].enabled ? '🎙️' : '🔇';
+}
+
+function toggleVideoCall() {
+ if (!gpLocalStream) return;
+ var videoTracks = gpLocalStream.getVideoTracks();
+ if (!videoTracks.length) return;
+ videoTracks[0].enabled = !videoTracks[0].enabled;
+ var btn = document.getElementById('btnCam');
+ if (btn) btn.textContent = videoTracks[0].enabled ? '📹' : '📷';
+}
+
+function endGroupCall() {
+ if (gpLocalStream) {
+ gpLocalStream.getTracks().forEach(function(t){ t.stop(); });
+ gpLocalStream = null;
+ }
+ Object.values(gpPeerConnections).forEach(function(pc){ try{ pc.close(); }catch(e){} });
+ gpPeerConnections = {};
+ gpCallActive = false;
+ clearInterval(gpCallTimer);
+
+ var banner = document.getElementById('gpCallBanner');
+ if (banner) banner.classList.add('hidden');
+ var grid = document.getElementById('gpVideoGrid');
+ if (grid) grid.classList.add('hidden');
+
+ // Clear remote videos
+ var remoteVids = document.getElementById('remoteVideos');
+ if (remoteVids) remoteVids.innerHTML = '';
+
+ if (socket && gpCurrentGroup) socket.emit('call_end', { groupId: gpCurrentGroup.id });
+ showToast('انتهت المكالمة', 'info');
+}
+
+// Handle incoming group call
+function handleIncomingGroupCall(data) {
+ var incoming = document.getElementById('gpIncomingCall');
+ var callerName = document.getElementById('gpCallerName');
+ var subtitle = document.getElementById('gpCallSubtitle');
+
+ if (incoming && callerName) {
+ callerName.textContent = data.fromName || 'عضو في المجموعة';
+ if (subtitle) subtitle.textContent = data.type === 'video' ? '📹 مكالمة مرئية' : '🎙️ مكالمة صوتية';
+ incoming._callData = data;
+ incoming.classList.remove('hidden');
+ }
+}
+
+function acceptCall() {
+ var incoming = document.getElementById('gpIncomingCall');
+ if (!incoming) return;
+ var data = incoming._callData;
+ incoming.classList.add('hidden');
+ if (data) initiateGroupCall(data.type || 'voice');
+}
+
+function rejectCall() {
+ var incoming = document.getElementById('gpIncomingCall');
+ if (incoming) incoming.classList.add('hidden');
+ if (socket && incoming && incoming._callData) {
+ socket.emit('call_reject', { to: incoming._callData.from });
+ }
+}
+
+/* ================================================================
+ 🔔 INCOMING CALL SOCKET LISTENER
+ ================================================================ */
+if (socket) {
+ socket.on('call_request', function(data) {
+ // If we're on the group page for this group, show incoming call
+ if (gpCurrentGroup && data.groupId === gpCurrentGroup.id) {
+ handleIncomingGroupCall(data);
+ } else if (!data.groupId && document.getElementById('dmChatPage') && !document.getElementById('dmChatPage').classList.contains('hidden')) {
+ // DM incoming call
+ handleIncomingDMCall(data);
+ }
+ });
+
+ socket.on('call_end', function() {
+ endGroupCall();
+ endDMCall();
+ });
+}
+
+/* ================================================================
+ 💬 ADVANCED DM CHAT PAGE
+ ================================================================ */
+function openDMChatPage(userId, userName) {
+ dmCurrentUser = { id: userId, name: userName };
+ dmReplyingTo = null;
+
+ document.getElementById('dmChatAvatar').textContent = '👤';
+ document.getElementById('dmChatName').textContent = userName || 'محادثة';
+ document.getElementById('dmChatStatus').textContent = 'عضو في نبض';
+
+ var page = document.getElementById('dmChatPage');
+ page.classList.remove('hidden');
+ page.style.transform = 'translateX(100%)';
+ setTimeout(function(){ page.style.transform = 'translateX(0)'; }, 10);
+
+ history.pushState({ page: 'dm', userId: userId }, '', '#dm/' + userId);
+
+ // Load DM messages
+ loadDMMessages(userId);
+}
+
+function closeDMChatPage() {
+ var page = document.getElementById('dmChatPage');
+ page.style.transform = 'translateX(100%)';
+ setTimeout(function(){ page.classList.add('hidden'); }, 300);
+
+ if (dmCallActive) endDMCall();
+ dmCurrentUser = null;
+
+ if (history.state && history.state.page === 'dm') history.back();
+}
+
+function loadDMMessages(userId) {
+ var container = document.getElementById('dmChatMessages');
+ if (!container) return;
+
+ // Use existing conversations from localStorage cache
+ var convId = [localStorage.getItem('nabdh_uid'), userId].sort().join('_');
+ var conv = (window._conversations || []).find(function(c){ return c.id === convId; });
+
+ if (!conv || !conv.messages || !conv.messages.length) {
+ container.innerHTML = '';
+ return;
+ }
+
+ var uid = localStorage.getItem('nabdh_uid') || '';
+ var html = '';
+ conv.messages.forEach(function(m) {
+ var isMine = m.from === uid;
+ html += '';
+ if (!isMine) html += '
' + escHtml(dmCurrentUser ? dmCurrentUser.name : 'عضو') + '
';
+ if (m.mediaType === 'image' && m.mediaData) {
+ html += '
';
+ } else if (m.mediaType === 'video' && m.mediaData) {
+ html += '
';
+ } else if (m.mediaType === 'audio' && m.mediaData) {
+ html += '
';
+ } else if (m.mediaType === 'file' && m.mediaData) {
+ html += '
';
+ }
+ if (m.text) html += '
' + escHtml(m.text) + '
';
+ html += '';
+ html += '
';
+ });
+
+ container.innerHTML = html;
+ container.scrollTop = container.scrollHeight;
+}
+
+function sendDMFromPage() {
+ if (!dmCurrentUser) return;
+ var input = document.getElementById('dmMsgInput');
+ var text = (input.value || '').trim();
+ if (!text) return;
+
+ var uid = localStorage.getItem('nabdh_uid') || '';
+ var uname = localStorage.getItem('nabdh_name') || 'عضو';
+
+ if (socket) {
+ socket.emit('dm_send', {
+ toUserId: dmCurrentUser.id,
+ fromUserId: uid,
+ senderName: uname,
+ text: text
+ });
+ }
+
+ // Optimistically add to UI
+ var container = document.getElementById('dmChatMessages');
+ if (container) {
+ var emptyEl = container.querySelector('.gp-empty-chat');
+ if (emptyEl) container.innerHTML = '';
+ var msgEl = document.createElement('div');
+ msgEl.className = 'gp-msg gpm-mine';
+ msgEl.innerHTML = '' + escHtml(text) + '
';
+ container.appendChild(msgEl);
+ container.scrollTop = container.scrollHeight;
+ }
+
+ input.value = '';
+ input.style.height = '';
+}
+
+/* ── triggerDMMedia — DM ──────────────────────────────────── */
+function triggerDMMedia(type) {
+ var map = { image:'dmImageInput', camera:'dmCameraInput', video:'dmVideoInput', audio:'dmAudioInput', file:'dmFileInput' };
+ var el = document.getElementById(map[type] || 'dmFileInput');
+ if (el) { el.value = ''; el.click(); }
+ else showToast('عنصر الرفع غير موجود', 'error');
+}
+
+function uploadDMMedia(input, type) {
+ var file = input.files[0];
+ if (!file) return;
+
+ // Size limits
+ var limits = { image:5, video:50, audio:20, file:20 };
+ var maxMB = limits[type] || 20;
+ if (file.size > maxMB * 1024 * 1024) {
+ showToast('الملف كبير جداً (الحد ' + maxMB + 'MB)', 'error');
+ input.value = '';
+ return;
+ }
+
+ var icons = { image:'🖼️', video:'🎬', audio:'🎵', file:'📄' };
+ showToast(icons[type] + ' جاري الرفع...', 'info');
+
+ var reader = new FileReader();
+ reader.onload = function(e) {
+ if (!dmCurrentUser || !socket) { showToast('الاتصال غير متاح', 'error'); return; }
+ var uid = localStorage.getItem('nabdh_uid') || '';
+ var uname = localStorage.getItem('nabdh_name') || 'عضو';
+ var label = { image:'📷 صورة', video:'🎬 فيديو', audio:'🎵 صوت', file:'📄 ملف: ' + file.name };
+ socket.emit('dm_send', {
+ toUserId: dmCurrentUser.id,
+ fromUserId: uid,
+ senderName: uname,
+ text: label[type] || '📎 مرفق',
+ mediaType: type,
+ mediaData: e.target.result,
+ mediaName: file.name
+ });
+ showToast('تم الإرسال ✓', 'success');
+ input.value = '';
+ };
+ reader.readAsDataURL(file);
+}
+
+function onDMTyping() {
+ if (!dmCurrentUser || !socket) return;
+ socket.emit('dm_typing', { toUserId: dmCurrentUser.id });
+}
+
+// DM Voice Record
+var dmVoiceRecorder = null, dmVoiceChunks = [], dmVoiceTimer = null, dmVoiceSeconds = 0;
+
+function startDMVoiceRecord() {
+ if (!navigator.mediaDevices) { showToast('التسجيل غير مدعوم', 'error'); return; }
+ navigator.mediaDevices.getUserMedia({ audio: true })
+ .then(function(stream) {
+ dmVoiceChunks = [];
+ dmVoiceSeconds = 0;
+ var opts = {};
+ try { opts = { mimeType: 'audio/webm;codecs=opus' }; } catch(e) {}
+ dmVoiceRecorder = new MediaRecorder(stream, opts);
+ dmVoiceRecorder.ondataavailable = function(e){ if(e.data.size>0) dmVoiceChunks.push(e.data); };
+ dmVoiceRecorder.onstop = function() {
+ stream.getTracks().forEach(function(t){ t.stop(); });
+ finishDMVoiceRecord();
+ };
+ dmVoiceRecorder.start();
+ var bar = document.getElementById('dmVoiceRecordingBar');
+ if (bar) bar.classList.remove('hidden');
+ dmVoiceTimer = setInterval(function() {
+ dmVoiceSeconds++;
+ var el = document.getElementById('dmVoiceRecTime');
+ if (el) el.textContent = Math.floor(dmVoiceSeconds/60) + ':' + ('0'+dmVoiceSeconds%60).slice(-2);
+ if (dmVoiceSeconds >= 120) stopDMVoiceRecord();
+ }, 1000);
+ })
+ .catch(function(){ showToast('لا يمكن الوصول للمايكروفون', 'error'); });
+}
+
+function stopDMVoiceRecord() {
+ if (dmVoiceRecorder && dmVoiceRecorder.state !== 'inactive') dmVoiceRecorder.stop();
+ clearInterval(dmVoiceTimer);
+ var bar = document.getElementById('dmVoiceRecordingBar');
+ if (bar) bar.classList.add('hidden');
+}
+
+function cancelDMVoiceRecord() {
+ if (dmVoiceRecorder && dmVoiceRecorder.state !== 'inactive') dmVoiceRecorder.stop();
+ clearInterval(dmVoiceTimer);
+ dmVoiceChunks = [];
+ var bar = document.getElementById('dmVoiceRecordingBar');
+ if (bar) bar.classList.add('hidden');
+}
+
+function finishDMVoiceRecord() {
+ if (!dmVoiceChunks.length || !dmCurrentUser) return;
+ var blob = new Blob(dmVoiceChunks, { type: 'audio/webm' });
+ var reader = new FileReader();
+ reader.onload = function(e) {
+ if (!socket) return;
+ socket.emit('dm_send', {
+ toUserId: dmCurrentUser.id,
+ fromUserId: localStorage.getItem('nabdh_uid') || '',
+ senderName: localStorage.getItem('nabdh_name') || 'عضو',
+ text: '🎵 رسالة صوتية',
+ mediaType: 'audio',
+ mediaData: e.target.result
+ });
+ showToast('تم إرسال الرسالة الصوتية ✓', 'success');
+ };
+ reader.readAsDataURL(blob);
+}
+
+/* ================================================================
+ 🌐 PUBLIC CHAT — MEDIA (toggleMenu, trigger, upload, voice)
+ ================================================================ */
+var pubVoiceRecorder = null, pubVoiceChunks = [], pubVoiceTimer = null, pubVoiceSeconds = 0;
+
+function togglePubMediaMenu() {
+ var menu = document.getElementById('pubMediaMenu');
+ var btn = document.getElementById('pubAttachBtn');
+ if (!menu) return;
+ var isOpen = !menu.classList.contains('hidden');
+ menu.classList.toggle('hidden', isOpen);
+ if (btn) btn.classList.toggle('gpi-attach-open', !isOpen);
+ if (!isOpen) {
+ setTimeout(function() {
+ document.addEventListener('click', function _closePub(e) {
+ var wrap = document.getElementById('pubMediaWrap');
+ if (wrap && !wrap.contains(e.target)) {
+ menu.classList.add('hidden');
+ if (btn) btn.classList.remove('gpi-attach-open');
+ }
+ document.removeEventListener('click', _closePub);
+ });
+ }, 10);
+ }
+}
+function closePubMediaMenu() {
+ var menu = document.getElementById('pubMediaMenu');
+ var btn = document.getElementById('pubAttachBtn');
+ if (menu) menu.classList.add('hidden');
+ if (btn) btn.classList.remove('gpi-attach-open');
+}
+function triggerPubMedia(type) {
+ var map = { image:'pubImageInput', camera:'pubCameraInput', video:'pubVideoInput', audio:'pubAudioInput', file:'pubFileInput' };
+ var el = document.getElementById(map[type] || 'pubFileInput');
+ if (el) { el.value = ''; el.click(); }
+ else showToast('عنصر الرفع غير موجود', 'error');
+}
+function uploadPubMedia(input, type) {
+ var file = input.files[0];
+ if (!file) return;
+ var limits = { image:5, video:50, audio:20, file:20 };
+ var maxMB = limits[type] || 20;
+ if (file.size > maxMB * 1024 * 1024) {
+ showToast('الملف كبير جداً (الحد ' + maxMB + 'MB)', 'error');
+ input.value = ''; return;
+ }
+ var icons = { image:'🖼️', video:'🎬', audio:'🎵', file:'📄' };
+ showToast((icons[type]||'📎') + ' جاري الإرسال...', 'info');
+ var reader = new FileReader();
+ reader.onload = function(e) {
+ var name = myName || 'أنت';
+ var label = { image:'📷 صورة', video:'🎬 فيديو', audio:'🎵 صوت', file:'📄 ملف: ' + file.name };
+ var msg = {
+ id: Date.now() + '', sender: name, senderArea: userLocationName,
+ text: label[type] || '📎 مرفق',
+ mediaType: type, mediaData: e.target.result, mediaName: file.name,
+ time: Date.now()
+ };
+ appendChatMsg(msg, true);
+ try {
+ fetch('/api/chat/' + chatRoom, {
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ text: msg.text, sender: name, senderArea: userLocationName,
+ mediaType: type, mediaData: e.target.result, mediaName: file.name
+ })
+ });
+ } catch(err) {}
+ input.value = '';
+ showToast('تم الإرسال ✓', 'success');
+ };
+ reader.readAsDataURL(file);
+}
+function startPubVoiceRecord() {
+ if (!navigator.mediaDevices) { showToast('المتصفح لا يدعم التسجيل', 'error'); return; }
+ navigator.mediaDevices.getUserMedia({ audio: true })
+ .then(function(stream) {
+ pubVoiceChunks = []; pubVoiceSeconds = 0;
+ var opts = {};
+ try { opts = { mimeType: 'audio/webm;codecs=opus' }; } catch(e) {}
+ pubVoiceRecorder = new MediaRecorder(stream, opts);
+ pubVoiceRecorder.ondataavailable = function(e) { if (e.data.size > 0) pubVoiceChunks.push(e.data); };
+ pubVoiceRecorder.onstop = function() {
+ stream.getTracks().forEach(function(t) { t.stop(); });
+ finishPubVoiceRecord();
+ };
+ pubVoiceRecorder.start();
+ var bar = document.getElementById('pubVoiceRecordingBar');
+ if (bar) bar.classList.remove('hidden');
+ pubVoiceTimer = setInterval(function() {
+ pubVoiceSeconds++;
+ var el = document.getElementById('pubVoiceRecTime');
+ if (el) el.textContent = Math.floor(pubVoiceSeconds/60) + ':' + ('0'+pubVoiceSeconds%60).slice(-2);
+ if (pubVoiceSeconds >= 120) stopPubVoiceRecord();
+ }, 1000);
+ })
+ .catch(function() { showToast('لا يمكن الوصول إلى المايكروفون', 'error'); });
+}
+function stopPubVoiceRecord() {
+ if (pubVoiceRecorder && pubVoiceRecorder.state !== 'inactive') pubVoiceRecorder.stop();
+ clearInterval(pubVoiceTimer);
+ var bar = document.getElementById('pubVoiceRecordingBar');
+ if (bar) bar.classList.add('hidden');
+}
+function cancelPubVoiceRecord() {
+ if (pubVoiceRecorder && pubVoiceRecorder.state !== 'inactive') pubVoiceRecorder.stop();
+ clearInterval(pubVoiceTimer);
+ pubVoiceChunks = [];
+ var bar = document.getElementById('pubVoiceRecordingBar');
+ if (bar) bar.classList.add('hidden');
+}
+function finishPubVoiceRecord() {
+ if (!pubVoiceChunks.length) return;
+ var blob = new Blob(pubVoiceChunks, { type: 'audio/webm' });
+ if (pubVoiceSeconds < 1) { showToast('التسجيل قصير جداً', 'error'); return; }
+ var reader = new FileReader();
+ reader.onload = function(e) {
+ var name = myName || 'أنت';
+ var durStr = Math.floor(pubVoiceSeconds/60) + ':' + ('0'+pubVoiceSeconds%60).slice(-2);
+ var msg = {
+ id: Date.now() + '', sender: name, senderArea: userLocationName,
+ text: '🎵 رسالة صوتية (' + durStr + ')',
+ mediaType: 'audio', mediaData: e.target.result,
+ time: Date.now()
+ };
+ appendChatMsg(msg, true);
+ try {
+ fetch('/api/chat/' + chatRoom, {
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ text: msg.text, sender: name, senderArea: userLocationName,
+ mediaType: 'audio', mediaData: e.target.result })
+ });
+ } catch(err) {}
+ showToast('تم إرسال الرسالة الصوتية ✓', 'success');
+ };
+ reader.readAsDataURL(blob);
+}
+
+/* ================================================================
+ 📚 STUDY CHAT — MEDIA (toggleMenu, trigger, upload, voice)
+ ================================================================ */
+var studyVoiceRecorder = null, studyVoiceChunks = [], studyVoiceTimer = null, studyVoiceSeconds = 0;
+
+function toggleStudyMediaMenu() {
+ var menu = document.getElementById('studyMediaMenu');
+ var btn = document.getElementById('studyAttachBtn');
+ if (!menu) return;
+ var isOpen = !menu.classList.contains('hidden');
+ menu.classList.toggle('hidden', isOpen);
+ if (btn) btn.classList.toggle('gpi-attach-open', !isOpen);
+ if (!isOpen) {
+ setTimeout(function() {
+ document.addEventListener('click', function _closeStudy(e) {
+ var wrap = document.getElementById('studyMediaWrap');
+ if (wrap && !wrap.contains(e.target)) {
+ menu.classList.add('hidden');
+ if (btn) btn.classList.remove('gpi-attach-open');
+ }
+ document.removeEventListener('click', _closeStudy);
+ });
+ }, 10);
+ }
+}
+function closeStudyMediaMenu() {
+ var menu = document.getElementById('studyMediaMenu');
+ var btn = document.getElementById('studyAttachBtn');
+ if (menu) menu.classList.add('hidden');
+ if (btn) btn.classList.remove('gpi-attach-open');
+}
+function triggerStudyMedia(type) {
+ var map = { image:'studyImageInput', camera:'studyCameraInput', video:'studyVideoInput', audio:'studyAudioInput', file:'studyFileInput' };
+ var el = document.getElementById(map[type] || 'studyFileInput');
+ if (el) { el.value = ''; el.click(); }
+ else showToast('عنصر الرفع غير موجود', 'error');
+}
+function uploadStudyMedia(input, type) {
+ if (!_activeStudyGroup) { showToast('لم يتم تحديد مجموعة', 'error'); return; }
+ var file = input.files[0];
+ if (!file) return;
+ var limits = { image:5, video:50, audio:20, file:20 };
+ var maxMB = limits[type] || 20;
+ if (file.size > maxMB * 1024 * 1024) {
+ showToast('الملف كبير جداً (الحد ' + maxMB + 'MB)', 'error');
+ input.value = ''; return;
+ }
+ var icons = { image:'🖼️', video:'🎬', audio:'🎵', file:'📄' };
+ showToast((icons[type]||'📎') + ' جاري الإرسال...', 'info');
+ var reader = new FileReader();
+ reader.onload = function(e) {
+ fetch('/api/study/' + _activeStudyGroup + '/msg', {
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ text: '',
+ userId: myUserId || localStorage.getItem('nabdh_uid') || '',
+ name: myName || 'عضو',
+ mediaType: type, mediaData: e.target.result, mediaName: file.name
+ })
+ })
+ .then(function(r) { return r.json(); })
+ .then(function() { input.value = ''; loadStudyChatMessages(_activeStudyGroup); showToast('تم الإرسال ✓', 'success'); })
+ .catch(function() { showToast('فشل إرسال الوسيط', 'error'); });
+ };
+ reader.readAsDataURL(file);
+}
+function startStudyVoiceRecord() {
+ if (!navigator.mediaDevices) { showToast('المتصفح لا يدعم التسجيل', 'error'); return; }
+ navigator.mediaDevices.getUserMedia({ audio: true })
+ .then(function(stream) {
+ studyVoiceChunks = []; studyVoiceSeconds = 0;
+ var opts = {};
+ try { opts = { mimeType: 'audio/webm;codecs=opus' }; } catch(e) {}
+ studyVoiceRecorder = new MediaRecorder(stream, opts);
+ studyVoiceRecorder.ondataavailable = function(e) { if (e.data.size > 0) studyVoiceChunks.push(e.data); };
+ studyVoiceRecorder.onstop = function() {
+ stream.getTracks().forEach(function(t) { t.stop(); });
+ finishStudyVoiceRecord();
+ };
+ studyVoiceRecorder.start();
+ var bar = document.getElementById('studyVoiceRecBar');
+ if (bar) bar.classList.remove('hidden');
+ studyVoiceTimer = setInterval(function() {
+ studyVoiceSeconds++;
+ var el = document.getElementById('studyVoiceRecTime');
+ if (el) el.textContent = Math.floor(studyVoiceSeconds/60) + ':' + ('0'+studyVoiceSeconds%60).slice(-2);
+ if (studyVoiceSeconds >= 120) stopStudyVoiceRecord();
+ }, 1000);
+ })
+ .catch(function() { showToast('لا يمكن الوصول إلى المايكروفون', 'error'); });
+}
+function stopStudyVoiceRecord() {
+ if (studyVoiceRecorder && studyVoiceRecorder.state !== 'inactive') studyVoiceRecorder.stop();
+ clearInterval(studyVoiceTimer);
+ var bar = document.getElementById('studyVoiceRecBar');
+ if (bar) bar.classList.add('hidden');
+}
+function cancelStudyVoiceRecord() {
+ if (studyVoiceRecorder && studyVoiceRecorder.state !== 'inactive') studyVoiceRecorder.stop();
+ clearInterval(studyVoiceTimer);
+ studyVoiceChunks = [];
+ var bar = document.getElementById('studyVoiceRecBar');
+ if (bar) bar.classList.add('hidden');
+}
+function finishStudyVoiceRecord() {
+ if (!studyVoiceChunks.length || !_activeStudyGroup) return;
+ var blob = new Blob(studyVoiceChunks, { type: 'audio/webm' });
+ if (studyVoiceSeconds < 1) { showToast('التسجيل قصير جداً', 'error'); return; }
+ var reader = new FileReader();
+ reader.onload = function(e) {
+ var durStr = Math.floor(studyVoiceSeconds/60) + ':' + ('0'+studyVoiceSeconds%60).slice(-2);
+ fetch('/api/study/' + _activeStudyGroup + '/msg', {
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ text: '🎵 رسالة صوتية (' + durStr + ')',
+ userId: myUserId || localStorage.getItem('nabdh_uid') || '',
+ name: myName || 'عضو',
+ mediaType: 'audio', mediaData: e.target.result
+ })
+ })
+ .then(function(r) { return r.json(); })
+ .then(function() { loadStudyChatMessages(_activeStudyGroup); showToast('تم إرسال الرسالة الصوتية ✓', 'success'); })
+ .catch(function() {});
+ };
+ reader.readAsDataURL(blob);
+}
+
+/* ================================================================
+ 📞 DM VOICE / VIDEO CALLS
+ ================================================================ */
+function startDMVoiceCall() {
+ initiateDMCall('voice');
+}
+
+function startDMVideoCall() {
+ initiateDMCall('video');
+}
+
+function initiateDMCall(type) {
+ if (!dmCurrentUser) return;
+ dmCallType = type;
+ var constraints = type === 'video' ? { audio: true, video: { facingMode: 'user' } } : { audio: true, video: false };
+
+ navigator.mediaDevices.getUserMedia(constraints)
+ .then(function(stream) {
+ dmLocalStream = stream;
+ dmCallActive = true;
+
+ var banner = document.getElementById('dmCallBanner');
+ if (banner) banner.classList.remove('hidden');
+ document.getElementById('dmCallType').textContent = type === 'video' ? '📹 مكالمة مرئية' : '🎙️ مكالمة صوتية';
+
+ if (type === 'video') {
+ var grid = document.getElementById('dmVideoGrid');
+ if (grid) grid.classList.remove('hidden');
+ var localVid = document.getElementById('dmLocalVideo');
+ if (localVid) localVid.srcObject = stream;
+ }
+
+ dmCallSeconds = 0;
+ dmCallTimer = setInterval(function() {
+ dmCallSeconds++;
+ var el = document.getElementById('dmCallTime');
+ if (el) el.textContent = Math.floor(dmCallSeconds/60) + ':' + ('0'+dmCallSeconds%60).slice(-2);
+ }, 1000);
+
+ // Create WebRTC peer connection
+ dmPeer = new RTCPeerConnection(ICE_SERVERS);
+ stream.getTracks().forEach(function(t){ dmPeer.addTrack(t, stream); });
+
+ dmPeer.ontrack = function(e) {
+ var remoteVid = document.getElementById('dmRemoteVideo');
+ if (remoteVid) remoteVid.srcObject = e.streams[0];
+ };
+
+ dmPeer.onicecandidate = function(e) {
+ if (e.candidate && socket && dmCurrentUser.socketId) {
+ socket.emit('webrtc_ice', { to: dmCurrentUser.socketId, candidate: e.candidate });
+ }
+ };
+
+ dmPeer.createOffer().then(function(offer) {
+ return dmPeer.setLocalDescription(offer).then(function(){ return offer; });
+ }).then(function(offer) {
+ if (socket && dmCurrentUser.socketId) {
+ socket.emit('webrtc_offer', { to: dmCurrentUser.socketId, offer: offer });
+ }
+ }).catch(function(e){ console.warn('WebRTC offer error:', e); });
+
+ // Notify via socket
+ if (socket) {
+ socket.emit('call_request', {
+ to: dmCurrentUser.socketId,
+ from: socket.id,
+ fromName: localStorage.getItem('nabdh_name') || 'عضو',
+ type: type
+ });
+ }
+
+ showToast(type === 'video' ? '📹 جاري الاتصال...' : '🎙️ جاري الاتصال...', 'info');
+ })
+ .catch(function(err) {
+ showToast('لا يمكن الوصول للكاميرا/المايكروفون', 'error');
+ });
+}
+
+function toggleDMMute() {
+ if (!dmLocalStream) return;
+ var tracks = dmLocalStream.getAudioTracks();
+ if (!tracks.length) return;
+ tracks[0].enabled = !tracks[0].enabled;
+ var btn = document.getElementById('dmBtnMute');
+ if (btn) btn.textContent = tracks[0].enabled ? '🎙️' : '🔇';
+}
+
+function toggleDMVideo() {
+ if (!dmLocalStream) return;
+ var tracks = dmLocalStream.getVideoTracks();
+ if (!tracks.length) return;
+ tracks[0].enabled = !tracks[0].enabled;
+ var btn = document.getElementById('dmBtnCam');
+ if (btn) btn.textContent = tracks[0].enabled ? '📹' : '📷';
+}
+
+function endDMCall() {
+ if (dmLocalStream) { dmLocalStream.getTracks().forEach(function(t){ t.stop(); }); dmLocalStream = null; }
+ if (dmPeer) { try { dmPeer.close(); } catch(e){} dmPeer = null; }
+ dmCallActive = false;
+ clearInterval(dmCallTimer);
+
+ var banner = document.getElementById('dmCallBanner');
+ if (banner) banner.classList.add('hidden');
+ var grid = document.getElementById('dmVideoGrid');
+ if (grid) grid.classList.add('hidden');
+
+ var remoteVid = document.getElementById('dmRemoteVideo');
+ if (remoteVid) remoteVid.srcObject = null;
+
+ if (socket && dmCurrentUser && dmCurrentUser.socketId) {
+ socket.emit('call_end', { to: dmCurrentUser.socketId });
+ }
+}
+
+function handleIncomingDMCall(data) {
+ // Show call notification
+ showToast('📞 مكالمة واردة من ' + (data.fromName || 'عضو') + ' - ' + (data.type === 'video' ? '📹 مرئية' : '🎙️ صوتية'), 'info');
+}
+
+/* ================================================================
+ START DM WITH MEMBER (from group members list)
+ ================================================================ */
+function startDMWithMember(userId) {
+ closeGroupPage();
+ setTimeout(function() {
+ openDMChatPage(userId, 'عضو في المجموعة');
+ }, 350);
+}
+
+/* ================================================================
+ HANDLE INVITE JOIN (URL param ?join=token)
+ ================================================================ */
+(function checkInviteJoin() {
+ var params = new URLSearchParams(window.location.search);
+ var token = params.get('join');
+ if (!token) return;
+
+ var uid = localStorage.getItem('nabdh_uid') || ('u_' + Math.random().toString(36).slice(2));
+ var uname = localStorage.getItem('nabdh_name') || 'عضو';
+ localStorage.setItem('nabdh_uid', uid);
+
+ fetch('/api/study/join-invite/' + token, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ userId: uid, author: uname })
+ })
+ .then(function(r){ return r.json(); })
+ .then(function(res) {
+ if (res.success) {
+ showToast('مرحباً! انضممت إلى مجموعة "' + res.group.name + '"', 'success');
+ setTimeout(function(){ openGroupPage(res.group.id); }, 1000);
+ } else {
+ showToast(res.error || 'رابط الدعوة غير صالح', 'error');
+ }
+ })
+ .catch(function(){});
+})();
+
+/* ================================================================
+ BACK BUTTON FIX - Fix popstate to handle group page
+ ================================================================ */
+window.removeEventListener('popstate', window._popstateHandler);
+window._popstateHandler = function(e) {
+ // Close any open emoji menus
+ var erm = document.getElementById('emojiReactMenu');
+ if (erm) { erm.remove(); return; }
+
+ // Close group emoji picker
+ var gpPicker = document.getElementById('gpEmojiPicker');
+ if (gpPicker && !gpPicker.classList.contains('hidden')) { gpPicker.classList.add('hidden'); return; }
+
+ // Close DM emoji picker
+ var dmPicker = document.getElementById('dmEmojiPicker');
+ if (dmPicker && !dmPicker.classList.contains('hidden')) { dmPicker.classList.add('hidden'); return; }
+
+ // Close invite modal
+ var invModal = document.getElementById('gpInviteModal');
+ if (invModal && !invModal.classList.contains('hidden')) { invModal.classList.add('hidden'); return; }
+
+ // Close DM page
+ var dmPage = document.getElementById('dmChatPage');
+ if (dmPage && !dmPage.classList.contains('hidden')) {
+ dmPage.style.transform = 'translateX(100%)';
+ setTimeout(function(){ dmPage.classList.add('hidden'); }, 300);
+ if (dmCallActive) endDMCall();
+ dmCurrentUser = null;
+ return;
+ }
+
+ // Close group page
+ var grpPage = document.getElementById('groupPage');
+ if (grpPage && !grpPage.classList.contains('hidden')) {
+ grpPage.style.transform = 'translateX(100%)';
+ setTimeout(function(){ grpPage.classList.add('hidden'); }, 300);
+ if (gpCurrentGroup && socket) socket.emit('leave_study', gpCurrentGroup.id);
+ if (gpCallActive) endGroupCall();
+ gpCurrentGroup = null;
+ return;
+ }
+
+ // 1. Close group page if open
+ var gp = document.getElementById('groupPage');
+ if (gp && !gp.classList.contains('hidden')) {
+ gp.style.transform = 'translateX(100%)';
+ setTimeout(function(){ gp.classList.add('hidden'); }, 300);
+ if (typeof gpCallActive !== 'undefined' && gpCallActive && typeof endGroupCall === 'function') endGroupCall();
+ if (socket && typeof gpCurrentGroup !== 'undefined' && gpCurrentGroup) socket.emit('leave_study', gpCurrentGroup.id);
+ gpCurrentGroup = null;
+ return;
+ }
+
+ // 2. Close DM chat page if open
+ var dmPage = document.getElementById('dmChatPage');
+ if (dmPage && !dmPage.classList.contains('hidden')) {
+ dmPage.classList.add('hidden');
+ document.body.style.overflow = '';
+ if (typeof dmCallActive !== 'undefined' && dmCallActive && typeof endDMCall === 'function') endDMCall();
+ return;
+ }
+
+ // 3. Close side menu
+ var menu = document.getElementById('sideMenu');
+ if (menu && !menu.classList.contains('hidden')) { menu.classList.add('hidden'); return; }
+
+ // 4. Close any open modal
+ var openModal = document.querySelector('.modal:not(.hidden), .overlay-modal:not(.hidden)');
+ if (openModal) { openModal.classList.add('hidden'); return; }
+
+ // 5. Navigate back in section history
+ var state = e.state;
+ if (state && state.section) {
+ goSection(state.section);
+ } else if (currentSection !== 'home') {
+ goSection('home');
+ }
+ // If already at home, do nothing (don't exit app)
+};
+window.addEventListener('popstate', window._popstateHandler);
+
+/* ================================================================
+ UPDATE renderStudyGroups to use openGroupPage
+ ================================================================ */
+function renderStudyGroupsNew(groups) {
+ var el = document.getElementById('studyList');
+ if (!el) return;
+ if (!groups || !groups.length) {
+ el.innerHTML = '🎓
لا توجد مجموعات بعد
كن أول من ينشئ مجموعة تعليمية!
';
+ return;
+ }
+
+ var uid = localStorage.getItem('nabdh_uid') || '';
+ var levelColors = { ابتدائي:'#27ae60', متوسط:'#2980b9', ثانوي:'#8e44ad', جامعي:'#e67e22', مهني:'#e74c3c', عام:'#1abc9c' };
+ var levelEmojis = { ابتدائي:'🌱', متوسط:'📗', ثانوي:'📘', جامعي:'🎓', مهني:'⚙️', عام:'🌐' };
+
+ el.innerHTML = groups.map(function(g) {
+ var color = levelColors[g.level] || '#1abc9c';
+ var emoji = g.avatar || levelEmojis[g.level] || '🎓';
+ var isMember = Array.isArray(g.members) && g.members.includes(uid);
+ var isFull = Array.isArray(g.members) && g.members.length >= (g.maxMembers || 20);
+ var membersCount = Array.isArray(g.members) ? g.members.length : 0;
+
+ return '' +
+ '' +
+ '
' +
+ '👥 ' + membersCount + '/' + (g.maxMembers||20) + ' ' +
+ (g.area ? '📍 ' + escHtml(g.area) + ' ' : '') +
+ (g.schedule ? '📅 ' + escHtml(g.schedule) + ' ' : '') +
+ '
' +
+ '
' +
+ '📖 فتح المجموعة ' +
+ (!isMember && !isFull ? '✋ انضم ' : '') +
+ (isMember ? '✅ أنت عضو ' : '') +
+ (isFull && !isMember ? '🔒 ممتلئة ' : '') +
+ '
' +
+ '
';
+ }).join('');
+}
+
+// Override the existing renderStudyGroups
+if (typeof renderStudyGroups === 'function') {
+ var _origRenderStudyGroups = renderStudyGroups;
+}
+renderStudyGroups = renderStudyGroupsNew;
+
+/* ================================================================
+ GROUP PAGE CSS TRANSITION
+ ================================================================ */
+(function() {
+ var gpPage = document.getElementById('groupPage');
+ var dmPage = document.getElementById('dmChatPage');
+ if (gpPage) gpPage.style.transition = 'transform 0.3s cubic-bezier(0.4,0,0.2,1)';
+ if (dmPage) dmPage.style.transition = 'transform 0.3s cubic-bezier(0.4,0,0.2,1)';
+})();
+
+
+/* ================================================================
+ 🎨 GROUP AVATAR PICKER
+================================================================ */
+function selectGroupAvatar(emoji, btn) {
+ document.querySelectorAll('.avatar-pick').forEach(function(b){ b.classList.remove('active-avatar'); });
+ btn.classList.add('active-avatar');
+ var inp = document.getElementById('studyAvatar');
+ if (inp) inp.value = emoji;
+}
+
+/* ================================================================
+ 🔥 VIRAL FEATURES - ميزات الانتشار بسرعة البرق
+ نظام النقاط | المتصدرون | التحديات | الطوارئ | المشاركة الفورية
+================================================================ */
+
+/* ──────────────────────────────────────────────
+ 🏆 POINTS & GAMIFICATION SYSTEM
+ نظام النقاط والمكافآت
+──────────────────────────────────────────────── */
+var POINT_ACTIONS = {
+ report: { pts: 10, label: 'نشر بلاغ' },
+ vote_up: { pts: 2, label: 'تقييم إيجابي' },
+ sos: { pts: 20, label: 'نداء استغاثة' },
+ help_offer: { pts: 15, label: 'عرض مساعدة' },
+ donate_blood:{ pts: 25, label: 'تسجيل تبرع دم' },
+ share: { pts: 5, label: 'مشاركة حدث' },
+ daily_login: { pts: 3, label: 'دخول يومي' },
+ study_msg: { pts: 2, label: 'رسالة مجموعة' },
+ join_study: { pts: 8, label: 'انضمام لمجموعة' },
+ market_post: { pts: 5, label: 'نشر في السوق' },
+ news_post: { pts: 12, label: 'نشر خبر' },
+ challenge: { pts: 30, label: 'تحدي يومي' }
+};
+
+var BADGES = [
+ { id:'first_report', icon:'🚨', title:'أول بلاغ', desc:'نشرت أول بلاغ لك', pts:10 },
+ { id:'helper', icon:'🤝', title:'مساعد', desc:'قدّمت 3 عروض مساعدة', pts:45 },
+ { id:'hero', icon:'🦸', title:'بطل المجتمع', desc:'أنقذت 5 أرواح', pts:100 },
+ { id:'reporter', icon:'📰', title:'مراسل ميداني', desc:'نشرت 10 أخبار', pts:120 },
+ { id:'blooddonor', icon:'🩸', title:'واهب الحياة', desc:'تبرعت بالدم مرتين', pts:50 },
+ { id:'connector', icon:'🔗', title:'الرابط', desc:'دعوت 5 أشخاص', pts:50 },
+ { id:'vigilant', icon:'👁️', title:'اليقظ', desc:'صوّت على 20 بلاغ', pts:40 },
+ { id:'scholar', icon:'🎓', title:'العالم', desc:'أنشأت مجموعة دراسية', pts:30 },
+ { id:'streaker', icon:'🔥', title:'المتواصل', desc:'دخلت 7 أيام متتالية', pts:70 },
+ { id:'legend', icon:'⭐', title:'أسطورة نبض', desc:'تجاوزت 500 نقطة', pts:500 }
+];
+
+var DAILY_CHALLENGES = [
+ { id:'dc1', title:'بلاغ عاجل', desc:'شارك بلاغاً حقيقياً من منطقتك اليوم', action:'report', target:1, reward:'10 نقطة + شارة 🚨' },
+ { id:'dc2', title:'واهب الدم', desc:'سجّل كمتبرع بالدم أو شارك طلب دم', action:'blood', target:1, reward:'25 نقطة + شارة 🩸' },
+ { id:'dc3', title:'ناشر الخير', desc:'شارك التطبيق مع 3 أشخاص من معارفك', action:'share', target:3, reward:'15 نقطة' },
+ { id:'dc4', title:'الطالب النشيط', desc:'أرسل 5 رسائل في مجموعة دراسية', action:'study_msg', target:5, reward:'20 نقطة + شارة 🎓' },
+ { id:'dc5', title:'يد العون', desc:'ردّ على طلب مساعدة في منطقتك', action:'help_offer', target:1, reward:'30 نقطة + شارة 🤝' },
+ { id:'dc6', title:'الناخب', desc:'صوّت على 5 بلاغات مختلفة', action:'vote_up', target:5, reward:'10 نقطة' },
+ { id:'dc7', title:'السوق النشط', desc:'انشر منتجاً في سوق P2P', action:'market_post', target:1, reward:'15 نقطة 🛒' }
+];
+
+// State
+var myPoints = 0;
+var myBadges = [];
+var myStreak = 0;
+var leaderboardData = [];
+var todayChallenge = null;
+var challengeProgress = 0;
+
+// ── Load points from localStorage ──────────────────────────────
+function loadPoints() {
+ myPoints = parseInt(localStorage.getItem('nabdh_pts') || '0');
+ myBadges = JSON.parse(localStorage.getItem('nabdh_badges') || '[]');
+ myStreak = parseInt(localStorage.getItem('nabdh_streak') || '0');
+ checkDailyLogin();
+ updatePointsUI();
+}
+
+function savePoints() {
+ localStorage.setItem('nabdh_pts', myPoints);
+ localStorage.setItem('nabdh_badges', JSON.stringify(myBadges));
+ localStorage.setItem('nabdh_streak', myStreak);
+}
+
+function addPoints(action) {
+ var def = POINT_ACTIONS[action];
+ if (!def) return;
+ myPoints += def.pts;
+ savePoints();
+ updatePointsUI();
+ showPointsPopup('+' + def.pts + ' نقطة', def.label);
+ checkBadges();
+ // Broadcast to server with area info for city leaderboard
+ var area = (typeof myProfile !== 'undefined' && myProfile && myProfile.area) ? myProfile.area :
+ (localStorage.getItem('nabdh_area') || '');
+ fetch('/api/points/add', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ userId: myUserId, action: action, pts: def.pts, name: myName || 'عضو', area: area })
+ }).catch(function(){});
+}
+
+function showPointsPopup(pts, label) {
+ var el = document.createElement('div');
+ el.className = 'points-popup';
+ el.innerHTML = '' + pts + ' ' + label + ' ';
+ el.style.cssText = 'position:fixed;bottom:80px;left:50%;transform:translateX(-50%);z-index:99999;background:linear-gradient(135deg,#f39c12,#e67e22);color:#fff;padding:.5rem 1.2rem;border-radius:30px;font-weight:700;font-size:.9rem;animation:pointsFloat 2s ease forwards;pointer-events:none;white-space:nowrap;box-shadow:0 4px 16px rgba(243,156,18,.4)';
+ document.body.appendChild(el);
+ setTimeout(function(){ el.remove(); }, 2000);
+}
+
+function updatePointsUI() {
+ // Update profile points display
+ var els = document.querySelectorAll('.my-points-display');
+ els.forEach(function(e){ e.textContent = myPoints + ' نقطة'; });
+ // Update profile level
+ var level = getPointLevel(myPoints);
+ var lvlEls = document.querySelectorAll('.my-level-display');
+ lvlEls.forEach(function(e){ e.textContent = level.icon + ' ' + level.title; });
+}
+
+function getPointLevel(pts) {
+ if (pts >= 1000) return { icon:'💎', title:'أسطورة', color:'#9b59b6' };
+ if (pts >= 500) return { icon:'⭐', title:'نجم', color:'#f39c12' };
+ if (pts >= 200) return { icon:'🥇', title:'متقدم', color:'#e67e22' };
+ if (pts >= 80) return { icon:'🥈', title:'نشيط', color:'#3498db' };
+ if (pts >= 20) return { icon:'🥉', title:'مبتدئ', color:'#1abc9c' };
+ return { icon:'🌱', title:'جديد', color:'#95a5a6' };
+}
+
+/* Full XP-level system with thresholds for the profile page */
+function getProfileLevel(pts) {
+ const levels = [
+ { level:1, name:'جديد', icon:'🌱', min:0, max:20 },
+ { level:2, name:'مبتدئ', icon:'🥉', min:20, max:80 },
+ { level:3, name:'نشيط', icon:'🥈', min:80, max:200 },
+ { level:4, name:'متقدم', icon:'🥇', min:200, max:500 },
+ { level:5, name:'نجم', icon:'⭐', min:500, max:1000 },
+ { level:6, name:'أسطورة', icon:'💎', min:1000, max:2000 },
+ ];
+ for (let i = levels.length - 1; i >= 0; i--) {
+ if (pts >= levels[i].min) return levels[i];
+ }
+ return levels[0];
+}
+
+function checkBadges() {
+ BADGES.forEach(function(b) {
+ if (myBadges.includes(b.id)) return;
+ var earned = false;
+ if (b.id === 'legend' && myPoints >= 500) earned = true;
+ if (b.id === 'streaker' && myStreak >= 7) earned = true;
+ // more checks via server
+ if (earned) awardBadge(b);
+ });
+}
+
+function awardBadge(badge) {
+ if (myBadges.includes(badge.id)) return;
+ myBadges.push(badge.id);
+ savePoints();
+ showAchievementToast(badge.icon, badge.title, badge.desc);
+}
+
+function showAchievementToast(icon, title, sub) {
+ var el = document.getElementById('achievementToast');
+ if (!el) return;
+ document.getElementById('atIcon').textContent = icon;
+ document.getElementById('atTitle').textContent = '🏅 ' + title;
+ document.getElementById('atSub').textContent = sub;
+ el.classList.remove('hidden');
+ el.style.animation = 'none';
+ setTimeout(function(){ el.style.animation = ''; }, 10);
+ setTimeout(function(){ el.classList.add('hidden'); }, 4000);
+}
+
+function checkDailyLogin() {
+ var today = new Date().toDateString();
+ var lastLogin = localStorage.getItem('nabdh_last_login');
+ if (lastLogin !== today) {
+ localStorage.setItem('nabdh_last_login', today);
+ var yesterday = new Date(Date.now() - 86400000).toDateString();
+ if (lastLogin === yesterday) {
+ myStreak++;
+ savePoints();
+ if (myStreak >= 3) showAchievementToast('🔥', 'متواصل ' + myStreak + ' أيام', 'تفتح هذا التطبيق كل يوم!');
+ } else if (lastLogin !== today) {
+ myStreak = 1;
+ savePoints();
+ }
+ addPoints('daily_login');
+ }
+}
+
+/* ──────────────────────────────────────────────
+ 🏆 LEADERBOARD
+──────────────────────────────────────────────── */
+function goSection_leaderboard() { openLeaderboard(); }
+
+function openLeaderboard() {
+ var page = document.getElementById('leaderboardPage');
+ if (!page) return;
+ page.classList.remove('hidden');
+ document.body.style.overflow = 'hidden';
+ history.pushState({ page: 'leaderboard' }, '', '#leaderboard');
+ loadLeaderboard('weekly');
+}
+
+function closeLeaderboard() {
+ var page = document.getElementById('leaderboardPage');
+ if (page) page.classList.add('hidden');
+ document.body.style.overflow = '';
+ if (history.state && history.state.page === 'leaderboard') history.back();
+}
+
+function switchLbTab(tab, btn) {
+ document.querySelectorAll('.lb-tab').forEach(function(b){ b.classList.remove('active-lb-tab'); });
+ if (btn) btn.classList.add('active-lb-tab');
+ loadLeaderboard(tab);
+}
+
+function loadLeaderboard(tab) {
+ fetch('/api/leaderboard?tab=' + (tab||'weekly'))
+ .then(function(r){ return r.json(); })
+ .then(function(data) {
+ renderLeaderboard(data.list || [], data.myRank);
+ })
+ .catch(function() {
+ // Fallback: show local data
+ var list = document.getElementById('lbList');
+ if (list) list.innerHTML = 'لا يوجد بيانات بعد - كن أول المتصدرين! 🏆
';
+ });
+}
+
+function renderLeaderboard(list, myRank) {
+ var el = document.getElementById('lbList');
+ if (!el) return;
+ if (!list.length) {
+ el.innerHTML = '🏆 لا يوجد متصدرون بعدابدأ بنشر البلاغات واكسب النقاط!
';
+ } else {
+ var medals = ['🥇','🥈','🥉'];
+ el.innerHTML = list.slice(0, 20).map(function(u, i) {
+ var isMe = u.userId === myUserId;
+ var level = getPointLevel(u.pts || 0);
+ return '' +
+ '
' + (medals[i] || (i+1)) + '
' +
+ '
' + (u.avatar || level.icon) + '
' +
+ '
' +
+ '
' + escHtml(u.name || 'مستخدم') + (isMe ? ' (أنت)' : '') + '
' +
+ '
' + escHtml(u.area || '') + ' • ' + level.title + '
' +
+ '
' +
+ '
' +
+ '
' + (u.pts || 0) + '
' +
+ '
نقطة
' +
+ '
' +
+ '
';
+ }).join('');
+ }
+ // My rank
+ if (myRank) {
+ document.getElementById('myRankNum').textContent = '#' + myRank.rank;
+ document.getElementById('myRankPts').textContent = myRank.pts + ' نقطة';
+ } else {
+ document.getElementById('myRankNum').textContent = '#' + (list.length + 1);
+ document.getElementById('myRankPts').textContent = myPoints + ' نقطة';
+ }
+}
+
+// Mini leaderboard for home page
+function loadHomeLeaderboard() {
+ fetch('/api/leaderboard?tab=weekly&limit=3')
+ .then(function(r){ return r.json(); })
+ .then(function(data) {
+ var el = document.getElementById('homeLeaderboard');
+ if (!el) return;
+ var list = data.list || [];
+ if (!list.length) {
+ el.innerHTML = 'لا يوجد متصدرون بعد - كن الأول! 🏆
';
+ return;
+ }
+ var medals = ['🥇','🥈','🥉'];
+ el.innerHTML = list.slice(0,3).map(function(u, i) {
+ var isMe = u.userId === myUserId;
+ return '' +
+ '' + (medals[i]||'') + ' ' +
+ '' + escHtml(u.name||'مستخدم') + ' ' +
+ '' + (u.pts||0) + ' نقطة ' +
+ '
';
+ }).join('') +
+ 'عرض الكل 🏆 ';
+ })
+ .catch(function(){
+ var el = document.getElementById('homeLeaderboard');
+ if (el) el.innerHTML = 'ابدأ بالبلاغات واكسب نقاط 🏆
';
+ });
+}
+
+/* ──────────────────────────────────────────────
+ 🎯 DAILY CHALLENGE
+──────────────────────────────────────────────── */
+function loadDailyChallenge() {
+ // Pick challenge based on day of week
+ var dayIdx = new Date().getDay();
+ todayChallenge = DAILY_CHALLENGES[dayIdx % DAILY_CHALLENGES.length];
+ challengeProgress = parseInt(localStorage.getItem('dc_progress_' + todayChallenge.id + '_' + new Date().toDateString()) || '0');
+
+ document.getElementById('dcTitle').textContent = todayChallenge.title;
+ document.getElementById('dcDesc').textContent = todayChallenge.desc;
+ document.getElementById('dcReward').textContent = todayChallenge.reward;
+ var pct = Math.min(100, Math.round((challengeProgress / todayChallenge.target) * 100));
+ document.getElementById('dcProgress').style.width = pct + '%';
+ document.getElementById('dcProgressText').textContent = challengeProgress + ' / ' + todayChallenge.target + (pct >= 100 ? ' ✅ مكتمل!' : ' مشارك');
+}
+
+function openDailyChallenge() {
+ if (!todayChallenge) return;
+ var modal = document.getElementById('dailyChallengeModal');
+ if (!modal) return;
+ var isDone = challengeProgress >= todayChallenge.target;
+ var pct = Math.min(100, Math.round((challengeProgress / todayChallenge.target) * 100));
+ document.getElementById('dcModalContent').innerHTML =
+ '🎯
' +
+ '' + todayChallenge.title + '
' +
+ '' + todayChallenge.desc + '
' +
+ '' +
+ '
' +
+ '
' + challengeProgress + ' / ' + todayChallenge.target + '
' +
+ '
' +
+ '🏅 المكافأة: ' + escHtml(todayChallenge.reward) + '
' +
+ '🔥 سلسلتك الحالية: ' + myStreak + ' يوم
';
+
+ var btn = document.getElementById('dcJoinBtn');
+ if (isDone) { btn.textContent = '✅ تم الإنجاز! احصل على النقاط'; btn.style.background = '#27ae60'; }
+ else { btn.textContent = '▶ ابدأ التحدي'; btn.style.background = ''; }
+ modal.classList.remove('hidden');
+}
+
+function closeDailyChallenge() {
+ var m = document.getElementById('dailyChallengeModal');
+ if (m) m.classList.add('hidden');
+}
+
+function joinDailyChallenge() {
+ if (!todayChallenge) return;
+ var isDone = challengeProgress >= todayChallenge.target;
+ if (isDone) {
+ var key = 'dc_rewarded_' + todayChallenge.id + '_' + new Date().toDateString();
+ if (!localStorage.getItem(key)) {
+ localStorage.setItem(key, '1');
+ addPoints('challenge');
+ showAchievementToast('🎯', 'تحدي اليوم مكتمل!', todayChallenge.reward);
+ } else {
+ showToast('لقد حصلت على مكافأتك اليوم ✅', 'info');
+ }
+ } else {
+ // Navigate to the relevant section
+ var sectionMap = { report:'report', blood:'blood', share:null, study_msg:'study', help_offer:'help', vote_up:'map', market_post:'market' };
+ var sec = sectionMap[todayChallenge.action];
+ closeDailyChallenge();
+ if (sec) goSection(sec);
+ else shareApp();
+ }
+}
+
+function incrementChallengeProgress(action) {
+ if (!todayChallenge || todayChallenge.action !== action) return;
+ var key = 'dc_progress_' + todayChallenge.id + '_' + new Date().toDateString();
+ challengeProgress = Math.min(todayChallenge.target, challengeProgress + 1);
+ localStorage.setItem(key, challengeProgress);
+ loadDailyChallenge();
+ if (challengeProgress >= todayChallenge.target) {
+ showToast('🎉 أكملت تحدي اليوم! افتح التحدي للحصول على المكافأة', 'success');
+ }
+}
+
+/* ──────────────────────────────────────────────
+ 🔥 VIRAL ALERTS - الأحداث الأكثر انتشاراً
+──────────────────────────────────────────────── */
+function loadViralAlerts() {
+ var el = document.getElementById('viralAlerts');
+ if (!el) return;
+ fetch('/api/alerts/viral')
+ .then(function(r){ return r.json(); })
+ .then(function(list) {
+ if (!list || !list.length) {
+ el.innerHTML = 'لا توجد أحداث انتشرت بعد - كن الأول! 🔥
';
+ return;
+ }
+ el.innerHTML = list.slice(0,5).map(function(a) {
+ var heat = (a.votes||0) + (a.shares||0)*3 + (a.views||0)*0.5;
+ var heatBar = Math.min(100, Math.round(heat / 2));
+ var typeIcon = { danger:'🔴', warning:'🟡', info:'🔵' };
+ return '' +
+ '
' + (typeIcon[a.type]||'📌') + '
' +
+ '
' +
+ '
' + escHtml((a.msg||'').slice(0,70)) + '
' +
+ '
📍 ' + escHtml(a.area||'غير محدد') + ' • ' + timeAgo(a.time||a.ts) + '
' +
+ '
' +
+ '
' +
+ '
' +
+ '👍 ' + (a.votes||0) + ' ' +
+ '🔁 ' + (a.shares||0) + ' ' +
+ '
' +
+ '
📲 ' +
+ '
';
+ }).join('');
+ })
+ .catch(function(){
+ if (el) el.innerHTML = 'لا توجد بيانات حالياً
';
+ });
+}
+
+/* ──────────────────────────────────────────────
+ 📡 EMERGENCY MODE - وضع الطوارئ الجماعي
+──────────────────────────────────────────────── */
+var emergencyModeActive = false;
+var emergencyThreshold = 5; // عدد البلاغات لتفعيل وضع الطوارئ
+
+function checkEmergencyMode(alertCount) {
+ if (alertCount >= emergencyThreshold && !emergencyModeActive) {
+ activateEmergencyMode(alertCount);
+ } else if (alertCount < emergencyThreshold && emergencyModeActive) {
+ deactivateEmergencyMode();
+ }
+}
+
+function activateEmergencyMode(alertCount) {
+ emergencyModeActive = true;
+ var el = document.getElementById('emergencyMode');
+ if (!el) return;
+ document.getElementById('emAlertCount').textContent = alertCount;
+ document.getElementById('emSOSCount').textContent = Math.floor(alertCount * 0.3);
+ document.getElementById('emHelpers').textContent = Math.floor(alertCount * 0.8);
+ document.getElementById('emSubtitle').textContent = 'تم رصد ' + alertCount + ' بلاغ في آخر ساعة';
+ el.classList.remove('hidden');
+ // Change app theme
+ document.body.classList.add('emergency-theme');
+ // Sound/vibration
+ if (navigator.vibrate) navigator.vibrate([200, 100, 200]);
+ showToast('🚨 تم تفعيل وضع الطوارئ - ' + alertCount + ' بلاغ نشط', 'error');
+}
+
+function deactivateEmergencyMode() {
+ emergencyModeActive = false;
+ document.body.classList.remove('emergency-theme');
+ closeEmergencyMode();
+}
+
+function closeEmergencyMode() {
+ var el = document.getElementById('emergencyMode');
+ if (el) el.classList.add('hidden');
+}
+
+function offerEmergencyHelp() {
+ closeEmergencyMode();
+ addPoints('help_offer');
+ incrementChallengeProgress('help_offer');
+ goSection('help');
+ showToast('شكراً على تبرعك! انشر عرض المساعدة 🤝', 'success');
+}
+
+/* ──────────────────────────────────────────────
+ 📲 QUICK SHARE - المشاركة الفورية للأحداث
+──────────────────────────────────────────────── */
+var _qsItem = null;
+
+function quickShareAlert(alert) {
+ _qsItem = alert;
+ var overlay = document.getElementById('quickShareOverlay');
+ if (!overlay) return;
+
+ var typeIcon = { danger:'🔴 خطر', warning:'⚠️ تحذير', info:'🔵 معلومة' };
+ var preview = document.getElementById('qsPreview');
+ if (preview) {
+ preview.innerHTML =
+ '' + (typeIcon[alert.type]||'📌') + '
' +
+ '' + escHtml((alert.msg||'').slice(0,100)) + '
' +
+ '📍 ' + escHtml(alert.area||'') + ' • ' + timeAgo(alert.time) + '
' +
+ 'عبر تطبيق نبض 💓
';
+ }
+
+ // Update view/share counts
+ document.getElementById('qsViews').textContent = alert.views || 0;
+ document.getElementById('qsShares').textContent = alert.shares || 0;
+ document.getElementById('qsHeat').textContent = Math.max(1, (alert.votes||0) + (alert.shares||0));
+
+ overlay.classList.remove('hidden');
+
+ // Increment view count
+ fetch('/api/alerts/' + alert.id + '/view', { method:'POST' }).catch(function(){});
+}
+
+function closeQuickShare() {
+ var el = document.getElementById('quickShareOverlay');
+ if (el) el.classList.add('hidden');
+ _qsItem = null;
+}
+
+function qsShareTo(platform) {
+ if (!_qsItem) return;
+ var url = window.location.origin + '/#alert/' + _qsItem.id;
+ var text = '🔴 ' + (_qsItem.msg||'') + '\n📍 ' + (_qsItem.area||'') + '\n\nعبر تطبيق نبض 💓\n' + url;
+
+ var urls = {
+ whatsapp: 'https://wa.me/?text=' + encodeURIComponent(text),
+ telegram: 'https://t.me/share/url?url=' + encodeURIComponent(url) + '&text=' + encodeURIComponent(_qsItem.msg||''),
+ twitter: 'https://twitter.com/intent/tweet?text=' + encodeURIComponent(text),
+ facebook: 'https://www.facebook.com/sharer/sharer.php?u=' + encodeURIComponent(url),
+ copy: null,
+ native: null
+ };
+
+ if (platform === 'copy') {
+ navigator.clipboard.writeText(url).then(function(){ showToast('✅ تم نسخ الرابط', 'success'); }).catch(function(){});
+ } else if (platform === 'native') {
+ if (navigator.share) navigator.share({ title:'نبض - حدث عاجل', text: _qsItem.msg||'', url: url }).catch(function(){});
+ else { navigator.clipboard.writeText(url).then(function(){ showToast('تم نسخ الرابط', 'success'); }); }
+ } else if (urls[platform]) {
+ window.open(urls[platform], '_blank');
+ }
+
+ // Increment share count + add points
+ fetch('/api/alerts/' + _qsItem.id + '/share', { method:'POST' }).catch(function(){});
+ addPoints('share');
+ incrementChallengeProgress('share');
+
+ var shareStat = document.getElementById('qsShares');
+ if (shareStat) shareStat.textContent = parseInt(shareStat.textContent||'0') + 1;
+ closeQuickShare();
+}
+
+/* ──────────────────────────────────────────────
+ 📊 LIVE MOMENTUM - شريط الزخم الحي
+──────────────────────────────────────────────── */
+function updateMomentumBar() {
+ fetch('/api/stats/live')
+ .then(function(r){ return r.json(); })
+ .then(function(d) {
+ var online = document.getElementById('onlineCountBig');
+ var today = document.getElementById('todayReports');
+ var zones = document.getElementById('activeZonesCount');
+ var trend = document.getElementById('trendingTopic');
+ if (online) animateCount('onlineCountBig', d.online || d.users || 0);
+ if (today) animateCount('todayReports', d.todayReports || 0);
+ if (zones) animateCount('activeZonesCount', d.activeZones || d.cities || 0);
+ if (trend && d.trending) trend.textContent = d.trending;
+ // Check emergency mode
+ checkEmergencyMode(d.activeAlerts || 0);
+ })
+ .catch(function(){});
+}
+
+/* ──────────────────────────────────────────────
+ 🔗 REFERRAL SYSTEM - نظام الإحالة
+──────────────────────────────────────────────── */
+function generateReferralLink() {
+ var ref = myUserId ? myUserId.slice(0,8) : 'nabdh';
+ return window.location.origin + '/?ref=' + ref;
+}
+
+function shareReferral() {
+ var link = generateReferralLink();
+ var text = '💓 جرّب تطبيق نبض - اعرف ما يحدث حولك في الوقت الحقيقي!\n\n🗺️ خريطة حية للأحداث\n💵 سعر الصرف اللحظي\n🩸 بنك الدم\n📢 صوت المجتمع\n\n' + link;
+ if (navigator.share) {
+ navigator.share({ title: 'نبض - صوت مدينتك الحي', text, url: link }).catch(function(){});
+ } else {
+ navigator.clipboard.writeText(text).then(function(){ showToast('✅ تم نسخ رابط الإحالة', 'success'); });
+ }
+ addPoints('share');
+}
+
+/* ──────────────────────────────────────────────
+ 🚀 INIT VIRAL FEATURES
+──────────────────────────────────────────────── */
+(function initViral() {
+ // Load after DOM is ready
+ setTimeout(function() {
+ loadPoints();
+ loadDailyChallenge();
+ loadHomeLeaderboard();
+ loadViralAlerts();
+ updateMomentumBar();
+
+ // Check referral
+ var params = new URLSearchParams(window.location.search);
+ var ref = params.get('ref');
+ if (ref && ref !== (myUserId||'').slice(0,8)) {
+ showToast('مرحباً! تم تسجيلك عبر رابط إحالة 🎉', 'success');
+ // Reward referrer
+ fetch('/api/referral', {
+ method:'POST',
+ headers:{'Content-Type':'application/json'},
+ body: JSON.stringify({ ref, newUser: myUserId })
+ }).catch(function(){});
+ }
+
+ // Refresh momentum every 30s
+ setInterval(updateMomentumBar, 30000);
+ // Refresh viral alerts every 5 min
+ setInterval(loadViralAlerts, 300000);
+ // Refresh leaderboard every 2 min if on home
+ setInterval(function(){
+ if (currentSection === 'home') loadHomeLeaderboard();
+ }, 120000);
+ }, 2000);
+})();
+
+// goSection leaderboard hook - safe inline check (no override needed)
+// leaderboard is handled directly inside goSection via openLeaderboard()
+
+// Hook into existing functions to award points
+// Override vote to add points
+var _origVote = typeof vote === 'function' ? vote : null;
+if (_origVote) {
+ window.vote = function(id) {
+ _origVote(id);
+ addPoints('vote_up');
+ incrementChallengeProgress('vote_up');
+ };
+}
+
+
+/* ================================================================
+ 🏆 POINTS HOOKS - ربط نقاط المكافآت بالأفعال الحقيقية
+ يضمن حصول المستخدم على نقاط عند كل فعل مهم
+================================================================ */
+(function hookPointsToActions() {
+ setTimeout(function() {
+
+ // ── Hook: submit alert / report ─────────────────────────────
+ var _origSubmitAlert = typeof submitAlert === 'function' ? submitAlert : null;
+ if (_origSubmitAlert && !_origSubmitAlert._nabdhHooked) {
+ window.submitAlert = function() {
+ _origSubmitAlert.apply(this, arguments);
+ setTimeout(function() {
+ addPoints('report');
+ incrementChallengeProgress('report');
+ }, 1500);
+ };
+ window.submitAlert._nabdhHooked = true;
+ }
+
+ // ── Hook: submit blood donor ────────────────────────────────
+ var _origSubmitDonor = typeof submitDonor === 'function' ? submitDonor : null;
+ if (_origSubmitDonor && !_origSubmitDonor._nabdhHooked) {
+ window.submitDonor = function() {
+ _origSubmitDonor.apply(this, arguments);
+ setTimeout(function() {
+ addPoints('donate_blood');
+ incrementChallengeProgress('blood');
+ }, 1500);
+ };
+ window.submitDonor._nabdhHooked = true;
+ }
+
+ // ── Hook: submit blood request ──────────────────────────────
+ var _origSubmitBlood = typeof submitBloodRequest === 'function' ? submitBloodRequest : null;
+ if (_origSubmitBlood && !_origSubmitBlood._nabdhHooked) {
+ window.submitBloodRequest = function() {
+ _origSubmitBlood.apply(this, arguments);
+ setTimeout(function() {
+ addPoints('donate_blood');
+ incrementChallengeProgress('blood');
+ }, 1500);
+ };
+ window.submitBloodRequest._nabdhHooked = true;
+ }
+
+ // ── Hook: submit help offer ─────────────────────────────────
+ var _origOfferHelp = typeof offerHelp === 'function' ? offerHelp : null;
+ if (_origOfferHelp && !_origOfferHelp._nabdhHooked) {
+ window.offerHelp = function(id) {
+ _origOfferHelp(id);
+ setTimeout(function() {
+ addPoints('help_offer');
+ incrementChallengeProgress('help_offer');
+ }, 500);
+ };
+ window.offerHelp._nabdhHooked = true;
+ }
+
+ // ── Hook: submit market post ────────────────────────────────
+ var _origSubmitMarket = typeof submitMarket === 'function' ? submitMarket : null;
+ if (_origSubmitMarket && !_origSubmitMarket._nabdhHooked) {
+ window.submitMarket = function() {
+ _origSubmitMarket.apply(this, arguments);
+ setTimeout(function() {
+ addPoints('market_post');
+ incrementChallengeProgress('market_post');
+ }, 1500);
+ };
+ window.submitMarket._nabdhHooked = true;
+ }
+
+ // ── Hook: submit news ───────────────────────────────────────
+ var _origSubmitNews = typeof submitNews === 'function' ? submitNews : null;
+ if (_origSubmitNews && !_origSubmitNews._nabdhHooked) {
+ window.submitNews = function() {
+ _origSubmitNews.apply(this, arguments);
+ setTimeout(function() { addPoints('news_post'); }, 1500);
+ };
+ window.submitNews._nabdhHooked = true;
+ }
+
+ // ── Hook: join study group ──────────────────────────────────
+ var _origJoinStudy = typeof joinStudyGroup === 'function' ? joinStudyGroup : null;
+ if (_origJoinStudy && !_origJoinStudy._nabdhHooked) {
+ window.joinStudyGroup = function(id) {
+ _origJoinStudy(id);
+ setTimeout(function() { addPoints('join_study'); }, 500);
+ };
+ window.joinStudyGroup._nabdhHooked = true;
+ }
+
+ // ── Hook: vote on alert ─────────────────────────────────────
+ var _origVoteAlert = typeof voteAlert === 'function' ? voteAlert : null;
+ if (_origVoteAlert && !_origVoteAlert._nabdhHooked) {
+ window.voteAlert = function(id, type) {
+ _origVoteAlert(id, type);
+ addPoints('vote_up');
+ incrementChallengeProgress('vote_up');
+ };
+ window.voteAlert._nabdhHooked = true;
+ }
+
+ // ── Hook: SOS send ──────────────────────────────────────────
+ var _origSendSOS = typeof sendSOS === 'function' ? sendSOS : null;
+ if (_origSendSOS && !_origSendSOS._nabdhHooked) {
+ window.sendSOS = function() {
+ _origSendSOS.apply(this, arguments);
+ setTimeout(function() { addPoints('sos'); }, 500);
+ };
+ window.sendSOS._nabdhHooked = true;
+ }
+
+ }, 3000); // Wait for all functions to be defined
+})();
+
+/* ================================================================
+ 📊 PROFILE POINTS DISPLAY - عرض النقاط في الملف الشخصي
+================================================================ */
+(function enhanceProfileWithPoints() {
+ // Poll until profile section exists
+ var attempts = 0;
+ var timer = setInterval(function() {
+ attempts++;
+ if (attempts > 20) { clearInterval(timer); return; }
+ var profileSection = document.getElementById('sec-profile');
+ if (!profileSection) return;
+ clearInterval(timer);
+
+ // Add points/badges display after profile loads
+ var origLoadProfile = typeof loadMyProfile === 'function' ? loadMyProfile : null;
+ if (origLoadProfile && !origLoadProfile._pointsHooked) {
+ window.loadMyProfile = function() {
+ origLoadProfile.apply(this, arguments);
+ setTimeout(injectProfilePointsCard, 800);
+ };
+ window.loadMyProfile._pointsHooked = true;
+ }
+ }, 500);
+})();
+
+function injectProfilePointsCard() {
+ var existing = document.getElementById('profilePointsCard');
+ if (existing) { updateProfilePointsCard(existing); return; }
+
+ var profileSection = document.getElementById('sec-profile');
+ if (!profileSection) return;
+
+ var firstPad = profileSection.querySelector('.section-pad');
+ if (!firstPad) return;
+
+ var card = document.createElement('div');
+ card.id = 'profilePointsCard';
+ card.className = 'section-pad';
+ card.innerHTML = buildProfilePointsHTML();
+ profileSection.insertBefore(card, firstPad.nextSibling);
+}
+
+function updateProfilePointsCard(card) {
+ card.innerHTML = buildProfilePointsHTML();
+}
+
+function buildProfilePointsHTML() {
+ var level = getPointLevel(myPoints);
+ var badgeIcons = BADGES.filter(function(b) { return myBadges.includes(b.id); }).map(function(b) {
+ return '' + b.icon + ' ';
+ }).join('');
+
+ return '' +
+ '
' +
+ '
' + level.icon + '
' +
+ '
' +
+ '
' + level.title + '
' +
+ '
' + myPoints + ' نقطة
' +
+ '
' +
+ '
🏆 المتصدرون ' +
+ '
' +
+ (myStreak > 1 ? '
🔥 سلسلة ' + myStreak + ' يوم متتالي
' : '') +
+ (badgeIcons ? '
' + badgeIcons + '
' : '') +
+ '
' +
+ '🔗 دعوة صديق +20 نقطة ' +
+ '🎯 تحدي اليوم ' +
+ '
' +
+ '
';
+}
+
+
+/* ============================================================
+ 📷 PROFILE PHOTO + EMOJI AVATAR SYSTEM
+ Camera / Gallery / Emoji picker buttons
+ ============================================================ */
+(function() {
+ 'use strict';
+
+ // ── Temp storage ──────────────────────────────────────────
+ var _profilePhotoBase64 = null;
+
+ /* ---- triggerProfilePhotoUpload ---- */
+ window.triggerProfilePhotoUpload = function() {
+ var inp = document.getElementById('profilePhotoInput');
+ if (inp) { inp.value = ''; inp.click(); }
+ else showToast && showToast('⚠️ عنصر رفع الصورة غير موجود', 'error');
+ };
+
+ /* ---- triggerProfilePhotoCapture ---- */
+ window.triggerProfilePhotoCapture = function() {
+ var inp = document.getElementById('profileCameraInput');
+ if (inp) { inp.value = ''; inp.click(); }
+ else window.triggerProfilePhotoUpload(); // Fallback for desktop
+ };
+
+ /* ---- onProfilePhotoSelected ---- */
+ window.onProfilePhotoSelected = function(input, fromCamera) {
+ var file = input && input.files && input.files[0];
+ if (!file) return;
+ if (file.size > 3 * 1024 * 1024) {
+ if (typeof showToast === 'function') showToast('❌ حجم الصورة يجب أن لا يتجاوز 3 ميجابايت', 'error');
+ return;
+ }
+ var reader = new FileReader();
+ reader.onload = function(e) {
+ var b64 = e.target.result;
+ _profilePhotoBase64 = b64;
+
+ // Update preview in edit form
+ var photoImg = document.getElementById('pePhotoImg');
+ var avatarPrev = document.getElementById('peAvatarPreview');
+ if (photoImg) { photoImg.src = b64; photoImg.classList.remove('hidden'); photoImg.style.display = 'block'; }
+ if (avatarPrev) { avatarPrev.style.display = 'none'; }
+
+ // Also update main profile avatar if visible
+ var profilePhotoDisplay = document.getElementById('profilePhotoDisplay');
+ var profileAvatarBig = document.getElementById('profileAvatarBig');
+ if (profilePhotoDisplay) { profilePhotoDisplay.src = b64; profilePhotoDisplay.classList.remove('hidden'); profilePhotoDisplay.style.display = 'block'; }
+ if (profileAvatarBig) profileAvatarBig.style.display = 'none';
+
+ if (typeof showToast === 'function') showToast(fromCamera ? '📷 تم التقاط الصورة، احفظ الملف لرفعها' : '🖼️ تم اختيار الصورة، احفظ الملف لرفعها', 'success');
+ };
+ reader.readAsDataURL(file);
+ };
+
+ /* ---- uploadProfilePhoto (called by saveProfile) ---- */
+ window.uploadProfilePhoto = async function(b64) {
+ try {
+ var res = await fetch('/api/upload/image', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ imageData: b64, type: 'profile', userId: window.myUserId || 'unknown' })
+ });
+ var data = await res.json();
+ return data.url || data.imageUrl || null;
+ } catch(e) {
+ console.error('uploadProfilePhoto error:', e);
+ return null;
+ }
+ };
+
+ /* ---- updateProfilePhotoDisplay ---- */
+ window.updateProfilePhotoDisplay = function(photoUrl) {
+ var ids = ['profilePhotoDisplay', 'pePhotoImg'];
+ ids.forEach(function(id) {
+ var el = document.getElementById(id);
+ if (!el) return;
+ if (photoUrl) { el.src = photoUrl; el.classList.remove('hidden'); el.style.display = 'block'; }
+ else { el.src = ''; el.classList.add('hidden'); el.style.display = 'none'; }
+ });
+ // Show/hide emoji avatar element
+ var bigAvatar = document.getElementById('profileAvatarBig');
+ var prevAvatar = document.getElementById('peAvatarPreview');
+ if (bigAvatar) bigAvatar.style.display = photoUrl ? 'none' : '';
+ if (prevAvatar) prevAvatar.style.display = photoUrl ? 'none' : '';
+ };
+
+ // ── Patch saveProfile to upload photo first ───────────────
+ var _originalSaveProfile = window.saveProfile;
+ window.saveProfile = async function() {
+ if (_profilePhotoBase64) {
+ if (typeof showToast === 'function') showToast('⏳ جاري رفع الصورة...', 'info');
+ try {
+ var photoUrl = await window.uploadProfilePhoto(_profilePhotoBase64);
+ if (photoUrl) {
+ if (!window.myProfile) window.myProfile = {};
+ window.myProfile.profileImage = photoUrl;
+ _profilePhotoBase64 = null;
+ window.updateProfilePhotoDisplay(photoUrl);
+ } else {
+ if (typeof showToast === 'function') showToast('⚠️ فشل رفع الصورة، سيتم الحفظ بدونها', 'warning');
+ _profilePhotoBase64 = null;
+ }
+ } catch(e) {
+ _profilePhotoBase64 = null;
+ }
+ }
+ if (typeof _originalSaveProfile === 'function') return _originalSaveProfile();
+ };
+
+ // ── Emoji Avatar Picker ───────────────────────────────────
+ var AVATAR_EMOJIS = [
+ '👤','👦','👧','👨','👩','🧑','👴','👵','🧒',
+ '🦸','🦹','🧙','🧝','🧛','🧟','🧞','🧜','🧚',
+ '👮','💂','🕵️','👷','🤴','👸','🤶','🎅','🧑⚕️',
+ '🧑🏫','🧑🌾','🧑🍳','🧑🔧','🧑🏭','🧑💼','🧑🔬','🧑🎨','🧑✈️',
+ '🦊','🐺','🦁','🐯','🐻','🐼','🐨','🦝','🦔',
+ '🤖','👾','👻','💀','☠️','🎭','🌟','⚡','🔥',
+ '🌊','🌈','🎯','🏆','💎','🦅','🦋','🌺','🌙',
+ 'م','ن','ب','ع','خ','ا','س','ح','ي','ف','ق','ك','ل','ج','و','ر','ز','ص','ط','ت','ث','ذ','ظ','ض','غ','ش'
+ ];
+
+ window.openEmojiAvatarPicker = function() {
+ var picker = document.getElementById('emojiAvatarPicker');
+ var grid = document.getElementById('eapGrid');
+ if (!picker) return;
+ if (grid) {
+ grid.innerHTML = '';
+ AVATAR_EMOJIS.forEach(function(em) {
+ var btn = document.createElement('button');
+ btn.type = 'button';
+ btn.textContent = em;
+ btn.className = 'eap-btn';
+ btn.style.cssText = 'font-size:1.6rem;background:var(--card,#1a2332);border:1px solid var(--border,rgba(255,255,255,.08));border-radius:8px;padding:.4rem .5rem;cursor:pointer;transition:transform .15s';
+ btn.onclick = function() { window.selectEmojiAvatar(em); };
+ grid.appendChild(btn);
+ });
+ }
+ picker.classList.remove('hidden');
+ picker.style.display = 'block';
+ picker.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
+ };
+
+ window.closeEmojiAvatarPicker = function() {
+ var picker = document.getElementById('emojiAvatarPicker');
+ if (picker) { picker.classList.add('hidden'); picker.style.display = 'none'; }
+ };
+
+ window.selectEmojiAvatar = function(emoji) {
+ if (!window.myProfile) window.myProfile = {};
+ window.myProfile.avatar = emoji;
+ window.myProfile.profileImage = null;
+ _profilePhotoBase64 = null;
+
+ // Update all avatar elements
+ ['profileAvatarBig','peAvatarPreview','menuAvatar'].forEach(function(id) {
+ var el = document.getElementById(id);
+ if (el) { el.textContent = emoji; el.style.display = ''; }
+ });
+ // Hide photo img elements
+ ['profilePhotoDisplay','pePhotoImg'].forEach(function(id) {
+ var el = document.getElementById(id);
+ if (el) { el.classList.add('hidden'); el.style.display = 'none'; }
+ });
+
+ window.closeEmojiAvatarPicker();
+
+ // Persist immediately
+ try { localStorage.setItem('nabdh_profile', JSON.stringify(window.myProfile)); } catch(e) {}
+ if (typeof showToast === 'function') showToast('✅ تم اختيار ' + emoji + ' كرمز لملفك', 'success');
+ };
+
+ // ── Sync photo display when profile section opens ─────────
+ var _profileSectionObserver = (function() {
+ var sec = document.getElementById('sec-profile');
+ if (!sec) return;
+ var obs = new MutationObserver(function(mutations) {
+ mutations.forEach(function(m) {
+ if (m.type === 'attributes' && m.attributeName === 'class') {
+ if (sec.classList.contains('active-sec')) {
+ var img = (window.myProfile && window.myProfile.profileImage) || null;
+ if (img) window.updateProfilePhotoDisplay(img);
+ }
+ }
+ });
+ });
+ obs.observe(sec, { attributes: true });
+ })();
+
+ console.log('✅ Profile photo & emoji system loaded');
+})();
+/* ============================================================
+ END PROFILE PHOTO + EMOJI SYSTEM
+ ============================================================ */
+
+/* ── refreshProfilePointsCard — v4 points card sync ── */
+function refreshProfilePointsCard() {
+ var points = myPoints || 0;
+ var streak = myStreak || 0;
+ var level = getPointLevel(points);
+
+ // Update all level/points display elements
+ var ids = {
+ 'ppcLevelIcon': function(e){ e.textContent = level.icon; },
+ 'ppcLevelTitle': function(e){ e.textContent = level.title; },
+ 'ppcPtsText': function(e){ e.textContent = points + ' نقطة'; },
+ 'pv4LevelIcon': function(e){ e.textContent = level.icon; },
+ 'pv4LevelText': function(e){ e.textContent = level.title; },
+ 'profileLevelBadgeInline': function(e){ e.textContent = level.icon + ' ' + level.title; },
+ 'profileLevelIcon': function(e){ e.textContent = level.icon; },
+ 'profileLevelText': function(e){ e.textContent = level.title; },
+ };
+ Object.keys(ids).forEach(function(id) {
+ var el = document.getElementById(id);
+ if (el) ids[id](el);
+ });
+
+ // Streak
+ var streakEl = document.getElementById('ppcStreak');
+ if (streakEl) {
+ if (streak > 1) { streakEl.textContent = '🔥 سلسلة ' + streak + ' يوم'; streakEl.classList.remove('hidden'); }
+ else streakEl.classList.add('hidden');
+ }
+
+ // Badges chips in points card
+ var badgesEl = document.getElementById('ppcBadgesRow');
+ if (badgesEl && myBadges && myBadges.length > 0) {
+ var BADGE_MAP = {};
+ if (typeof BADGES !== 'undefined') BADGES.forEach(function(b){ BADGE_MAP[b.id] = b; });
+ var html = myBadges.map(function(bid) {
+ var b = BADGE_MAP[bid];
+ if (!b) return '';
+ return '' + b.icon + ' ' + b.title + ' ';
+ }).filter(Boolean).join('');
+ if (html) badgesEl.innerHTML = html;
+ }
+
+ // Animated points counter in stats row
+ var psPts = document.getElementById('ps-points');
+ if (psPts) {
+ var cur = parseInt(psPts.textContent) || 0;
+ if (cur !== points) animateCounter('ps-points', cur, points, 600);
+ }
+
+ // Refresh activity bars
+ refreshActivityBars();
+}
+
+/* ================================================================
+ PROFILE v5 — Missing Functions (shareProfile, QR, phone, location)
+================================================================ */
+
+/* ── Share Profile ─────────────────────────────────────────── */
+function shareProfile() {
+ var name = (myProfile && myProfile.name) || myName || 'مستخدم نبض';
+ var pubPhone = (myProfile && myProfile.publicPhone) || '';
+ var company = (myProfile && myProfile.company) || '';
+ var area = (myProfile && myProfile.area) || userLocationName || '';
+ var url = window.location.origin + '/#profile';
+ var text = '👤 ' + name + ' على تطبيق نبض';
+ if (area) text += '\n📍 ' + area;
+ if (company) text += '\n🏢 ' + company;
+ if (pubPhone) text += '\n📞 ' + pubPhone;
+ text += '\n🔗 ' + url;
+
+ if (navigator.share) {
+ navigator.share({ title: 'ملف ' + name + ' على نبض', text: text, url: url })
+ .catch(function(){});
+ } else {
+ try {
+ navigator.clipboard.writeText(text);
+ showToast('📋 تم نسخ بيانات الملف الشخصي', 'success');
+ } catch(e) {
+ showToast(text.substring(0, 60) + '…', 'info');
+ }
+ }
+}
+
+/* ── Show Profile QR Modal ─────────────────────────────────── */
+function showProfileQRModal() {
+ var modal = document.getElementById('profileQrModal');
+ if (!modal) return;
+ var name = (myProfile && myProfile.name) || myName || 'مستخدم نبض';
+ var url = window.location.origin + '/#profile';
+
+ // Update name labels
+ var nameEl = document.getElementById('pqmName');
+ var subEl = document.getElementById('pqmSub');
+ if (nameEl) nameEl.textContent = name;
+ if (subEl) subEl.textContent = 'امسح الكود للتواصل معي';
+
+ // Generate QR
+ var canvas = document.getElementById('profileQrCanvas');
+ if (canvas && typeof QRCode !== 'undefined') {
+ QRCode.toCanvas(canvas, url, {
+ width: 200, margin: 1,
+ color: { dark: '#1abc9c', light: '#0e151e' }
+ }, function(err) { if (err) console.warn('QR error:', err); });
+ }
+
+ modal.classList.remove('hidden');
+ history.pushState({ section: currentSection, modal: 'qr' }, '', '#profile');
+}
+
+function closeProfileQrModal() {
+ var modal = document.getElementById('profileQrModal');
+ if (modal) modal.classList.add('hidden');
+}
+
+/* ── Copy Profile Link ─────────────────────────────────────── */
+function copyProfileLink() {
+ var url = window.location.origin + '/#profile';
+ try {
+ navigator.clipboard.writeText(url).then(function() {
+ showToast('📋 تم نسخ رابط ملفك الشخصي!', 'success');
+ });
+ } catch(e) {
+ showToast('🔗 الرابط: ' + url, 'info');
+ }
+}
+
+/* ── Call Profile Phone (private) ─────────────────────────── */
+function callProfilePhone() {
+ var ph = myProfile && myProfile.phone;
+ if (ph) window.open('tel:' + ph);
+ else showToast('❌ لم تُضف رقم هاتف', 'error');
+}
+
+/* ── Use Current Location for Profile ─────────────────────── */
+function useCurrentLocationForProfile() {
+ if (!navigator.geolocation) {
+ showToast('❌ المتصفح لا يدعم الموقع', 'error');
+ return;
+ }
+ showToast('⏳ جارٍ تحديد موقعك...', 'info');
+ navigator.geolocation.getCurrentPosition(
+ function(pos) {
+ userLat = pos.coords.latitude;
+ userLng = pos.coords.longitude;
+
+ // Reverse geocode
+ fetch('https://nominatim.openstreetmap.org/reverse?lat=' + userLat + '&lon=' + userLng + '&format=json&accept-language=ar')
+ .then(function(r){ return r.json(); })
+ .then(function(d) {
+ var city = d.address && (d.address.city || d.address.town || d.address.county || d.address.state) || '';
+ var state = d.address && d.address.state || '';
+ userLocationName = city || state || 'موقعي الحالي';
+
+ var areaInput = document.getElementById('pe-area');
+ if (areaInput) areaInput.value = userLocationName;
+ showToast('📍 تم تحديد موقعك: ' + userLocationName, 'success');
+ })
+ .catch(function() {
+ userLocationName = 'موقعي الحالي';
+ var areaInput = document.getElementById('pe-area');
+ if (areaInput) areaInput.value = userLocationName;
+ showToast('📍 تم تحديد الإحداثيات', 'success');
+ });
+ },
+ function(err) {
+ var msgs = { 1: 'رُفض الإذن', 2: 'الموقع غير متاح', 3: 'انتهت المهلة' };
+ showToast('❌ ' + (msgs[err.code] || 'خطأ في الموقع'), 'error');
+ },
+ { enableHighAccuracy: true, timeout: 10000 }
+ );
+}
+
+/* ============================================================
+ 🚀 PERFORMANCE & UX ENHANCEMENTS v7.0
+ - Skeleton loaders
+ - Debounce / Throttle
+ - Offline cache (localStorage)
+ - Pull-to-refresh
+ - IntersectionObserver lazy images
+ - Virtual scroll hint
+ - Prefetch on idle
+ - Connection quality detection
+============================================================ */
+
+/* ── Debounce & Throttle ────────────────────────────────── */
+function debounce(fn, ms) {
+ let t; return function(...a) { clearTimeout(t); t = setTimeout(() => fn.apply(this, a), ms); };
+}
+function throttle(fn, ms) {
+ let last = 0; return function(...a) { const now = Date.now(); if (now - last >= ms) { last = now; fn.apply(this, a); } };
+}
+
+/* ── Skeleton HTML generator ────────────────────────────── */
+function skeletonCards(n, type) {
+ if (type === 'alert') {
+ return Array(n).fill(0).map(() =>
+ ''
+ ).join('');
+ }
+ if (type === 'market') {
+ return Array(n).fill(0).map(() =>
+ ''
+ ).join('');
+ }
+ if (type === 'person') {
+ return Array(n).fill(0).map(() =>
+ ''
+ ).join('');
+ }
+ return Array(n).fill(0).map(() =>
+ ''
+ ).join('');
+}
+
+/* ── Offline Cache helpers ──────────────────────────────── */
+const _cache = {
+ set(key, data, ttlMs) {
+ try { localStorage.setItem('nc_' + key, JSON.stringify({ data, exp: Date.now() + (ttlMs || 300000) })); } catch {}
+ },
+ get(key) {
+ try {
+ const raw = localStorage.getItem('nc_' + key);
+ if (!raw) return null;
+ const obj = JSON.parse(raw);
+ if (Date.now() > obj.exp) { localStorage.removeItem('nc_' + key); return null; }
+ return obj.data;
+ } catch { return null; }
+ },
+ clear(key) { try { localStorage.removeItem('nc_' + key); } catch {} }
+};
+
+/* ── Cached fetch wrapper ───────────────────────────────── */
+async function cachedFetch(url, ttlMs, forceRefresh) {
+ const key = url.replace(/[^a-z0-9]/gi, '_');
+ if (!forceRefresh) {
+ const cached = _cache.get(key);
+ if (cached !== null) return cached;
+ }
+ const data = await fetch(url).then(r => r.json());
+ _cache.set(key, data, ttlMs || 120000);
+ return data;
+}
+
+/* ── Connection quality detection ───────────────────────── */
+let _connQuality = 'good'; // 'good' | 'slow' | 'offline'
+function detectConnection() {
+ if (!navigator.onLine) { _connQuality = 'offline'; return; }
+ const conn = navigator.connection || navigator.mozConnection || navigator.webkitConnection;
+ if (conn) {
+ const type = conn.effectiveType;
+ _connQuality = (type === '2g' || type === 'slow-2g') ? 'slow' : 'good';
+ }
+}
+window.addEventListener('online', () => { _connQuality = 'good'; showToast('✅ عاد الاتصال بالإنترنت', 'success'); const offEl = document.getElementById('offlineIndicator'); if (offEl) offEl.classList.add('hidden'); });
+window.addEventListener('offline', () => { _connQuality = 'offline'; showToast('⚠️ انقطع الاتصال - وضع offline', 'warning'); const offEl = document.getElementById('offlineIndicator'); if (offEl) offEl.classList.remove('hidden'); });
+detectConnection();
+
+/* ── Pull-to-refresh ────────────────────────────────────── */
+(function initPullToRefresh() {
+ let startY = 0, pulling = false;
+ const threshold = 80;
+ let indicator = null;
+ function getIndicator() {
+ if (!indicator) {
+ indicator = document.createElement('div');
+ indicator.id = 'pullRefreshIndicator';
+ indicator.innerHTML = '🔄 اسحب للتحديث ';
+ indicator.style.cssText = 'position:fixed;top:-50px;left:50%;transform:translateX(-50%);background:var(--dark3);color:var(--text);padding:.5rem 1.2rem;border-radius:2rem;font-size:.85rem;z-index:9000;transition:top .2s ease;display:flex;align-items:center;gap:.4rem;box-shadow:0 4px 16px rgba(0,0,0,.4)';
+ document.body.appendChild(indicator);
+ }
+ return indicator;
+ }
+ document.addEventListener('touchstart', e => {
+ const mc = document.getElementById('mainContent');
+ if (mc && mc.scrollTop === 0) { startY = e.touches[0].clientY; pulling = true; }
+ }, { passive: true });
+ document.addEventListener('touchmove', e => {
+ if (!pulling) return;
+ const dy = e.touches[0].clientY - startY;
+ if (dy > 10 && dy < threshold + 40) {
+ const ind = getIndicator();
+ ind.style.top = Math.min(dy - 40, 20) + 'px';
+ ind.querySelector('.ptr-text').textContent = dy >= threshold ? '↑ أفلت للتحديث' : '↓ اسحب للتحديث';
+ }
+ }, { passive: true });
+ document.addEventListener('touchend', e => {
+ if (!pulling) return;
+ const dy = e.changedTouches[0].clientY - startY;
+ const ind = getIndicator();
+ ind.style.top = '-50px';
+ if (dy >= threshold) {
+ showToast('🔄 جاري التحديث...', 'info');
+ // Refresh current section
+ if (currentSection === 'home') { loadAlerts(); loadStats(); }
+ else if (currentSection === 'market') loadMarket();
+ else if (currentSection === 'exchange') loadExchange();
+ else if (currentSection === 'news') loadNews();
+ else if (currentSection === 'map') { loadNearbyAlerts(); loadNearbyPeople(); }
+ else if (currentSection === 'dashboard') loadDashboard();
+ else if (currentSection === 'study') loadStudyGroups();
+ else if (currentSection === 'help') loadHelpRequests();
+ else if (currentSection === 'polls') loadPolls();
+ }
+ pulling = false;
+ }, { passive: true });
+})();
+
+/* ── Lazy image loading via IntersectionObserver ────────── */
+function lazyLoadImages() {
+ if (!('IntersectionObserver' in window)) return;
+ const imgs = document.querySelectorAll('img[data-src]');
+ const io = new IntersectionObserver((entries, obs) => {
+ entries.forEach(entry => {
+ if (entry.isIntersecting) {
+ const img = entry.target;
+ img.src = img.dataset.src;
+ img.removeAttribute('data-src');
+ obs.unobserve(img);
+ }
+ });
+ }, { rootMargin: '200px' });
+ imgs.forEach(img => io.observe(img));
+}
+
+/* ── Idle prefetch (background data loading) ─────────────── */
+function scheduleIdlePrefetch() {
+ if ('requestIdleCallback' in window) {
+ requestIdleCallback(() => {
+ fetch('/api/prefetch').then(r => r.json()).then(d => {
+ if (d.alerts && d.alerts.length) { _cache.set('_api_alerts', d.alerts, 180000); }
+ if (d.exchange) { _cache.set('_api_exchange', d.exchange, 180000); }
+ if (d.market) { _cache.set('_api_market', d.market, 180000); }
+ }).catch(() => {});
+ }, { timeout: 5000 });
+ }
+}
+
+/* ── Enhanced loadAlerts with skeleton + cache ──────────── */
+window._loadAlertsEnhanced = async function(forceRefresh) {
+ const el = document.getElementById('homeAlerts');
+ if (el && !allAlerts.length) el.innerHTML = skeletonCards(4, 'alert');
+ try {
+ allAlerts = await cachedFetch('/api/alerts', 60000, forceRefresh);
+ renderHomeAlerts(); updateTicker();
+ } catch {
+ try { allAlerts = await fetch('/api/alerts').then(r => r.json()); } catch { allAlerts = []; }
+ renderHomeAlerts(); updateTicker();
+ }
+};
+
+/* ── Enhanced loadMarket with skeleton + cache ──────────── */
+window._loadMarketEnhanced = async function(forceRefresh) {
+ const el = document.getElementById('marketList');
+ if (el && !allMarket.length) el.innerHTML = skeletonCards(4, 'market');
+ try {
+ allMarket = await cachedFetch('/api/market', 60000, forceRefresh);
+ renderMarket();
+ } catch {
+ try { allMarket = await fetch('/api/market').then(r => r.json()); } catch { allMarket = []; }
+ renderMarket();
+ }
+};
+
+/* ── Enhanced loadStats with cache ─────────────────────── */
+window._loadStatsEnhanced = async function(forceRefresh) {
+ try {
+ const s = await cachedFetch('/api/stats', 30000, forceRefresh);
+ updateStats(s);
+ } catch {
+ try { const s = await fetch('/api/stats').then(r=>r.json()); updateStats(s); } catch {}
+ }
+};
+
+/* ── Format file size ───────────────────────────────────── */
+function fmtSize(bytes) {
+ if (!bytes) return '';
+ if (bytes < 1024) return bytes + ' B';
+ if (bytes < 1048576) return (bytes / 1024).toFixed(1) + ' KB';
+ return (bytes / 1048576).toFixed(1) + ' MB';
+}
+
+/* ── Copy text to clipboard ─────────────────────────────── */
+function copyText(text, msg) {
+ try {
+ navigator.clipboard.writeText(text).then(() => showToast(msg || '✅ تم النسخ', 'success'));
+ } catch {
+ const ta = document.createElement('textarea');
+ ta.value = text; ta.style.position = 'fixed'; ta.style.opacity = '0';
+ document.body.appendChild(ta); ta.select(); document.execCommand('copy');
+ document.body.removeChild(ta);
+ showToast(msg || '✅ تم النسخ', 'success');
+ }
+}
+
+/* ── Trending section on home ───────────────────────────── */
+async function loadTrending() {
+ try {
+ const items = await cachedFetch('/api/trending', 60000);
+ const el = document.getElementById('trendingList');
+ if (!el || !items.length) return;
+ el.innerHTML = items.slice(0, 5).map((item, i) =>
+ '' +
+ '
' + (i + 1) + ' ' +
+ '
' +
+ '' + escHtml((item.text || '').substring(0, 60)) + ' ' +
+ '' + { alert: '🚨', news: '📰', voice: '🔊' }[item.type] + ' ' +
+ '
' +
+ '
'
+ ).join('');
+ } catch {}
+}
+
+/* ── Smart search with debounce ────────────────────────── */
+const _debouncedPeopleSearch = debounce(function(q) {
+ const el = document.getElementById('nearbyPeopleList');
+ if (!el) return;
+ if (!q) { loadNearbyPeople(); return; }
+ const q2 = q.toLowerCase();
+ const filtered = nearbyUsers.filter(u =>
+ (u.name || '').toLowerCase().includes(q2) ||
+ (u.area || '').toLowerCase().includes(q2) ||
+ (u.jobTitle || '').toLowerCase().includes(q2) ||
+ (u.company || '').toLowerCase().includes(q2)
+ );
+ if (!filtered.length) {
+ el.innerHTML = emptyState('🔍', 'لا توجد نتائج', 'جرّب بحثاً آخر', '');
+ } else {
+ el.innerHTML = filtered.map(u => renderPersonCard(u)).join('');
+ }
+}, 300);
+
+/* ── Animated counter with easing (improved) ───────────── */
+function animateCount2(id, target, duration) {
+ const el = document.getElementById(id);
+ if (!el) return;
+ const start = parseInt(el.textContent.replace(/[^\d]/g, '')) || 0;
+ if (start === target) return;
+ const dur = duration || 1000;
+ const t0 = performance.now();
+ (function loop(now) {
+ const p = Math.min((now - t0) / dur, 1);
+ const ease = p < .5 ? 4 * p * p * p : 1 - Math.pow(-2 * p + 2, 3) / 2;
+ el.textContent = Math.round(start + (target - start) * ease).toLocaleString('ar');
+ if (p < 1) requestAnimationFrame(loop);
+ })(t0);
+}
+
+/* ── Scroll to top button ───────────────────────────────── */
+function initScrollTop() {
+ const btn = document.getElementById('scrollTopBtn');
+ if (!btn) return;
+ const mc = document.getElementById('mainContent');
+ if (!mc) return;
+ mc.addEventListener('scroll', throttle(() => {
+ if (mc.scrollTop > 300) btn.classList.remove('hidden');
+ else btn.classList.add('hidden');
+ }, 200));
+ btn.onclick = () => mc.scrollTo({ top: 0, behavior: 'smooth' });
+}
+
+/* ── Touch feedback for buttons ─────────────────────────── */
+function addTouchFeedback() {
+ document.addEventListener('touchstart', e => {
+ const btn = e.target.closest('button, .btn, .bnav, .alert-item, .market-card, .person-card');
+ if (btn) btn.classList.add('touch-active');
+ }, { passive: true });
+ document.addEventListener('touchend', e => {
+ document.querySelectorAll('.touch-active').forEach(el => el.classList.remove('touch-active'));
+ }, { passive: true });
+}
+
+/* ── Time formatter (extended) ──────────────────────────── */
+function timeAgoFull(ts) {
+ if (!ts) return '—';
+ const t = typeof ts === 'number' ? ts : new Date(ts).getTime();
+ const s = Math.floor((Date.now() - t) / 1000);
+ if (s < 5) return 'الآن';
+ if (s < 60) return 'منذ ' + s + ' ثانية';
+ const m = Math.floor(s / 60);
+ if (m < 60) return 'منذ ' + m + ' دقيقة';
+ const h = Math.floor(m / 60);
+ if (h < 24) return 'منذ ' + h + ' ساعة';
+ const d = Math.floor(h / 24);
+ if (d < 7) return 'منذ ' + d + ' يوم';
+ if (d < 30) return 'منذ ' + Math.floor(d / 7) + ' أسبوع';
+ return new Date(t).toLocaleDateString('ar-SA');
+}
+
+/* ── Page visibility - pause/resume ─────────────────────── */
+document.addEventListener('visibilitychange', () => {
+ if (document.visibilityState === 'visible') {
+ // Refresh when user returns
+ setTimeout(() => { loadStats(true); if (currentSection === 'home') loadAlerts(); }, 200);
+ }
+});
+
+/* ── Register enhanced Service Worker ───────────────────── */
+if ('serviceWorker' in navigator) {
+ window.addEventListener('load', () => {
+ navigator.serviceWorker.register('/sw.js', { scope: '/' })
+ .then(reg => {
+ // Check for updates every 5 min
+ setInterval(() => reg.update(), 5 * 60 * 1000);
+ })
+ .catch(() => {});
+ });
+}
+
+/* ── Init all enhancements after DOMContentLoaded ───────── */
+document.addEventListener('DOMContentLoaded', () => {
+ addTouchFeedback();
+ initScrollTop();
+ scheduleIdlePrefetch();
+ loadTrending();
+ // Init lazy images on any dynamic content update
+ const mainContent = document.getElementById('mainContent');
+ if (mainContent) {
+ const mo = new MutationObserver(() => lazyLoadImages());
+ mo.observe(mainContent, { childList: true, subtree: true });
+ }
+});
+
+
+/* ============================================================
+ 🔍 GLOBAL SEARCH v7.0
+============================================================ */
+let _globalSearchTimer = null;
+
+function openGlobalSearch() {
+ let modal = document.getElementById('globalSearchModal');
+ if (!modal) {
+ modal = document.createElement('div');
+ modal.id = 'globalSearchModal';
+ modal.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,.85);z-index:9990;display:flex;flex-direction:column;padding:1rem';
+ modal.innerHTML =
+ '' +
+ ' ' +
+ '✕ ' +
+ '
' +
+ '
';
+ document.body.appendChild(modal);
+ const inp = modal.querySelector('#globalSearchInp');
+ inp.addEventListener('input', () => {
+ clearTimeout(_globalSearchTimer);
+ _globalSearchTimer = setTimeout(() => performGlobalSearch(inp.value), 350);
+ });
+ inp.focus();
+ } else {
+ modal.style.display = 'flex';
+ setTimeout(() => { const i = document.getElementById('globalSearchInp'); if (i) { i.value = ''; i.focus(); } }, 50);
+ document.getElementById('globalSearchResults').innerHTML = '';
+ }
+}
+
+function closeGlobalSearch() {
+ const m = document.getElementById('globalSearchModal');
+ if (m) m.style.display = 'none';
+}
+
+async function performGlobalSearch(q) {
+ const el = document.getElementById('globalSearchResults');
+ if (!el) return;
+ if (!q || q.trim().length < 2) { el.innerHTML = 'ابحث عن أي شيء...
'; return; }
+ el.innerHTML = '
';
+ try {
+ const data = await fetch('/api/search?q=' + encodeURIComponent(q.trim())).then(r => r.json());
+ if (!data.total) { el.innerHTML = '🔍 لا توجد نتائج لـ "' + escHtml(q) + '"
'; return; }
+ let html = '';
+ const sections = [
+ { key: 'alerts', label: '🚨 تنبيهات', goTo: 'map' },
+ { key: 'market', label: '🛒 سوق', goTo: 'market' },
+ { key: 'news', label: '📰 أخبار', goTo: 'news' },
+ { key: 'people', label: '👤 أشخاص', goTo: 'people' }
+ ];
+ sections.forEach(s => {
+ const items = data[s.key] || [];
+ if (!items.length) return;
+ html += '' + s.label + '
';
+ items.forEach(item => {
+ html += '
' +
+ '
' + (item.icon || '•') + ' ' +
+ '
' +
+ '
' + escHtml(item.title || '') + '
' +
+ (item.sub ? '
' + escHtml(item.sub) + '
' : '') +
+ '
' +
+ '
';
+ });
+ html += '
';
+ });
+ el.innerHTML = html;
+ } catch {
+ el.innerHTML = '❌ خطأ في البحث
';
+ }
+}
+
+/* ============================================================
+ 📊 LIVE STATS UPDATER (topbar quick stats)
+============================================================ */
+async function loadQuickStats() {
+ try {
+ const s = await fetch('/api/stats/quick').then(r => r.json());
+ // Update topbar
+ const lr = document.getElementById('liveRate');
+ if (lr && s.usdRate) lr.textContent = s.usdRate.toLocaleString('ar');
+ const lu = document.getElementById('liveUsers');
+ if (lu) animateCount('liveUsers', s.users || 0);
+ const lrep = document.getElementById('liveReports');
+ if (lrep) animateCount('liveReports', s.reports || 0);
+ // Today reports
+ const tr = document.getElementById('todayReports');
+ if (tr) tr.textContent = s.todayReports || 0;
+ // Online count big
+ const ocb = document.getElementById('onlineCountBig');
+ if (ocb) ocb.textContent = s.users || 0;
+ } catch {}
+}
+
+/* ============================================================
+ 🏷️ APP VERSION CHECKER
+============================================================ */
+async function checkAppVersion() {
+ try {
+ const v = await fetch('/api/version').then(r => r.json());
+ const savedV = localStorage.getItem('nabdh_version');
+ if (savedV && savedV !== v.version) {
+ showToast('🚀 إصدار جديد متاح! v' + v.version, 'info');
+ // Tell service worker to update
+ if ('serviceWorker' in navigator && navigator.serviceWorker.controller) {
+ navigator.serviceWorker.controller.postMessage('skipWaiting');
+ }
+ }
+ localStorage.setItem('nabdh_version', v.version);
+ } catch {}
+}
+
+/* ============================================================
+ 📋 ALERT DETAILS MODAL
+============================================================ */
+function openAlertDetails(alertId) {
+ const alert = allAlerts.find(a => a.id === alertId);
+ if (!alert) return;
+ let modal = document.getElementById('alertDetailsModal');
+ if (!modal) {
+ modal = document.createElement('div');
+ modal.id = 'alertDetailsModal';
+ modal.className = 'modal-overlay';
+ modal.onclick = e => { if (e.target === modal) modal.classList.add('hidden'); };
+ const box = document.createElement('div');
+ box.className = 'modal-box';
+ box.innerHTML = '✕
';
+ modal.appendChild(box);
+ document.body.appendChild(modal);
+ }
+ const img = alert.imageId ? ' ' : '';
+ document.getElementById('alertDetailsContent').innerHTML =
+ '' + (alert.icon || '🔴') + '
' +
+ '' + escHtml(alert.msg) + '
' +
+ img +
+ '' +
+ '📍 ' + escHtml(alert.area || '—') + ' ' +
+ '🕐 ' + timeAgo(alert.time) + ' ' +
+ '👁️ ' + (alert.views || 0) + ' مشاهدة ' +
+ '👍 ' + (alert.votes || 0) + ' تأييد ' +
+ '
' +
+ '' +
+ '👍 أؤيد ' +
+ '🔗 مشاركة ' +
+ '
';
+ modal.classList.remove('hidden');
+}
+
+/* ============================================================
+ 📈 TOPBAR LIVE RATE UPDATE
+============================================================ */
+// Update live USD rate from exchange rates
+function updateTopbarRate() {
+ const usdRate = allRates.find(r => (r.currency || '').includes('دولار') || (r.currency || '').toLowerCase().includes('usd'));
+ if (usdRate) {
+ const el = document.getElementById('liveRate');
+ if (el) el.textContent = (usdRate.buy || usdRate.rate || '---').toLocaleString('ar');
+ }
+}
+
+// Hook into existing data updates
+const _origRenderExchange = typeof renderExchange === 'function' ? renderExchange : null;
+
+/* ============================================================
+ 🛡️ INPUT SANITIZATION (XSS prevention)
+============================================================ */
+function sanitizeInput(str, maxLen) {
+ if (!str) return '';
+ return String(str).replace(/[<>"'&]/g, c => ({ '<':'<', '>':'>', '"':'"', "'":"'", '&':'&' }[c])).substring(0, maxLen || 500);
+}
+
+/* ============================================================
+ 📱 HAPTIC FEEDBACK (vibration for mobile)
+============================================================ */
+function haptic(p) {
+ if (!navigator.vibrate) return;
+ if (typeof p === 'string') {
+ const patterns = { light:[10], medium:[20], heavy:[30,10,30], success:[10,5,10], error:[50,10,50] };
+ navigator.vibrate(patterns[p] || [10]);
+ } else {
+ navigator.vibrate(p || [50]);
+ }
+}
+
+/* ============================================================
+ 🔄 AUTO-REFRESH INTERVALS (enhanced)
+============================================================ */
+// Override initApp intervals to be smarter
+document.addEventListener('DOMContentLoaded', () => {
+ // Initial quick stats load
+ setTimeout(loadQuickStats, 1500);
+ // Check version after 3 seconds
+ setTimeout(checkAppVersion, 3000);
+ // Periodic quick stats (every 20 seconds)
+ setInterval(loadQuickStats, 20000);
+ // Update topbar rate when exchange data changes
+ setInterval(updateTopbarRate, 15000);
+});
+
+/* ============================================================
+ ⌨️ KEYBOARD SHORTCUTS
+============================================================ */
+document.addEventListener('keydown', e => {
+ // Escape to close modals
+ if (e.key === 'Escape') {
+ closeGlobalSearch();
+ const modals = document.querySelectorAll('.modal-overlay:not(.hidden)');
+ modals.forEach(m => m.classList.add('hidden'));
+ }
+ // / or Ctrl+F to open search
+ if ((e.key === '/' || (e.ctrlKey && e.key === 'f')) && !e.target.matches('input, textarea')) {
+ e.preventDefault();
+ openGlobalSearch();
+ }
+});
+
+
+/* ============================================================
+ 🚀 NABDH v7.1 — COMPLETE FEATURES ENGINE
+ تحسينات شاملة: أداء + UX + ميزات متقدمة
+ ============================================================ */
+
+/* ── Enhanced Skeleton Loaders ─────────────────────────────── */
+function showSkeleton(containerId, type = 'generic', count = 4) {
+ const el = document.getElementById(containerId);
+ if (!el) return;
+ el.innerHTML = skeletonCards(count, type);
+ el.classList.add('skeleton-loading');
+}
+function hideSkeleton(containerId) {
+ const el = document.getElementById(containerId);
+ if (el) el.classList.remove('skeleton-loading');
+}
+function skeletonList(n) {
+ return Array(n).fill(0).map(() =>
+ ``
+ ).join('');
+}
+function skeletonStats(n) {
+ return Array(n).fill(0).map(() =>
+ ``
+ ).join('');
+}
+
+/* ── Virtual Scroll for Long Lists ────────────────────────── */
+class VirtualScroll {
+ constructor(container, items, renderFn, itemHeight = 80) {
+ this.container = typeof container === 'string' ? document.getElementById(container) : container;
+ this.items = items;
+ this.renderFn = renderFn;
+ this.itemHeight = itemHeight;
+ this.visibleCount = Math.ceil((this.container?.clientHeight || 600) / itemHeight) + 5;
+ this.startIndex = 0;
+ this._bound = this._onScroll.bind(this);
+ this.init();
+ }
+ init() {
+ if (!this.container) return;
+ this.container.style.overflowY = 'auto';
+ this.spacer = document.createElement('div');
+ this.spacer.style.height = (this.items.length * this.itemHeight) + 'px';
+ this.container.appendChild(this.spacer);
+ this.viewport = document.createElement('div');
+ this.viewport.className = 'vs-viewport';
+ this.container.appendChild(this.viewport);
+ this.container.addEventListener('scroll', this._bound);
+ this.render();
+ }
+ _onScroll() {
+ const scrollTop = this.container.scrollTop;
+ const newStart = Math.max(0, Math.floor(scrollTop / this.itemHeight) - 2);
+ if (newStart !== this.startIndex) {
+ this.startIndex = newStart;
+ this.render();
+ }
+ }
+ render() {
+ if (!this.viewport) return;
+ const end = Math.min(this.startIndex + this.visibleCount, this.items.length);
+ const slice = this.items.slice(this.startIndex, end);
+ this.viewport.style.transform = `translateY(${this.startIndex * this.itemHeight}px)`;
+ this.viewport.innerHTML = slice.map(item => this.renderFn(item)).join('');
+ }
+ update(items) {
+ this.items = items;
+ if (this.spacer) this.spacer.style.height = (items.length * this.itemHeight) + 'px';
+ this.render();
+ }
+ destroy() {
+ if (this.container) this.container.removeEventListener('scroll', this._bound);
+ }
+}
+
+/* ── Advanced Offline Cache ────────────────────────────────── */
+const NabdhCache = {
+ _store: {},
+ set(key, value, ttl = 300000) {
+ this._store[key] = { value, expires: Date.now() + ttl };
+ try { localStorage.setItem('nabdh_cache_' + key, JSON.stringify({ value, expires: Date.now() + ttl })); } catch(e) {}
+ },
+ get(key) {
+ // Memory first
+ if (this._store[key] && Date.now() < this._store[key].expires) return this._store[key].value;
+ // LocalStorage fallback
+ try {
+ const raw = localStorage.getItem('nabdh_cache_' + key);
+ if (raw) {
+ const item = JSON.parse(raw);
+ if (Date.now() < item.expires) { this._store[key] = item; return item.value; }
+ localStorage.removeItem('nabdh_cache_' + key);
+ }
+ } catch(e) {}
+ return null;
+ },
+ del(key) {
+ delete this._store[key];
+ try { localStorage.removeItem('nabdh_cache_' + key); } catch(e) {}
+ },
+ clear() {
+ this._store = {};
+ try {
+ Object.keys(localStorage).filter(k => k.startsWith('nabdh_cache_')).forEach(k => localStorage.removeItem(k));
+ } catch(e) {}
+ }
+};
+
+/* ── Smart Fetch with Cache ────────────────────────────────── */
+async function smartFetch(url, opts = {}) {
+ const cacheKey = url;
+ const ttl = opts.ttl || 30000;
+ const forceRefresh = opts.forceRefresh || false;
+ if (!forceRefresh) {
+ const cached = NabdhCache.get(cacheKey);
+ if (cached) return cached;
+ }
+ try {
+ const res = await fetch(url, { signal: AbortSignal.timeout(8000), ...opts });
+ if (!res.ok) throw new Error('HTTP ' + res.status);
+ const data = await res.json();
+ NabdhCache.set(cacheKey, data, ttl);
+ return data;
+ } catch(e) {
+ const stale = NabdhCache.get(cacheKey + '_stale');
+ if (stale) return stale;
+ throw e;
+ }
+}
+
+/* ── Pull to Refresh ───────────────────────────────────────── */
+function initPullToRefresh(containerId, onRefresh) {
+ const el = document.getElementById(containerId) || document.getElementById('content');
+ if (!el) return;
+ let startY = 0, pulling = false, indicator = null;
+
+ const getIndicator = () => {
+ if (!indicator) {
+ indicator = document.createElement('div');
+ indicator.className = 'ptr-indicator';
+ indicator.innerHTML = '↓ اسحب للتحديث ';
+ el.parentNode.insertBefore(indicator, el);
+ }
+ return indicator;
+ };
+
+ el.addEventListener('touchstart', e => {
+ if (el.scrollTop === 0) { startY = e.touches[0].clientY; pulling = true; }
+ }, { passive: true });
+
+ el.addEventListener('touchmove', e => {
+ if (!pulling) return;
+ const dy = e.touches[0].clientY - startY;
+ if (dy > 20) {
+ const ind = getIndicator();
+ ind.style.height = Math.min(dy, 70) + 'px';
+ ind.style.opacity = Math.min(dy / 70, 1);
+ if (dy > 60) {
+ ind.querySelector('.ptr-text').textContent = 'ارفع للتحديث';
+ ind.querySelector('.ptr-icon').textContent = '↑';
+ }
+ }
+ }, { passive: true });
+
+ el.addEventListener('touchend', async e => {
+ if (!pulling || !indicator) return;
+ pulling = false;
+ const dy = e.changedTouches[0].clientY - startY;
+ if (dy > 60) {
+ indicator.querySelector('.ptr-text').textContent = 'جاري التحديث...';
+ indicator.querySelector('.ptr-icon').innerHTML = '
';
+ try { await onRefresh(); } catch(e) {}
+ }
+ if (indicator) { indicator.style.height = '0'; indicator.style.opacity = '0'; }
+ setTimeout(() => { if (indicator) { indicator.remove(); indicator = null; } }, 300);
+ }, { passive: true });
+}
+
+/* ── Intersection Observer for Lazy Loading ────────────────── */
+const lazyObserver = new IntersectionObserver((entries) => {
+ entries.forEach(entry => {
+ if (entry.isIntersecting) {
+ const el = entry.target;
+ if (el.dataset.src) {
+ el.src = el.dataset.src;
+ el.removeAttribute('data-src');
+ el.classList.add('lazy-loaded');
+ lazyObserver.unobserve(el);
+ }
+ if (el.dataset.bg) {
+ el.style.backgroundImage = `url(${el.dataset.bg})`;
+ el.removeAttribute('data-bg');
+ lazyObserver.unobserve(el);
+ }
+ }
+ });
+}, { rootMargin: '200px', threshold: 0.01 });
+
+function observeLazy(container) {
+ const imgs = (container || document).querySelectorAll('[data-src],[data-bg]');
+ imgs.forEach(img => lazyObserver.observe(img));
+}
+
+/* ── Animated Number Counter ───────────────────────────────── */
+function animateNumber(el, target, duration = 1200, prefix = '', suffix = '') {
+ if (!el) return;
+ const start = parseInt(el.textContent.replace(/\D/g, '')) || 0;
+ const diff = target - start;
+ if (diff === 0) return;
+ const startTime = performance.now();
+ const step = (now) => {
+ const elapsed = now - startTime;
+ const progress = Math.min(elapsed / duration, 1);
+ const eased = 1 - Math.pow(1 - progress, 3); // ease-out cubic
+ const current = Math.round(start + diff * eased);
+ el.textContent = prefix + current.toLocaleString('ar-EG') + suffix;
+ if (progress < 1) requestAnimationFrame(step);
+ };
+ requestAnimationFrame(step);
+}
+
+/* ── Enhanced Toast Notifications ─────────────────────────── */
+const ToastQueue = [];
+let toastTimer = null;
+function showEnhancedToast(msg, type = 'info', duration = 3500) {
+ const types = {
+ success: { icon: '✅', class: 'toast-success' },
+ error: { icon: '❌', class: 'toast-error' },
+ warning: { icon: '⚠️', class: 'toast-warning' },
+ info: { icon: 'ℹ️', class: 'toast-info' }
+ };
+ const t = types[type] || types.info;
+ const toast = document.createElement('div');
+ toast.className = `enhanced-toast ${t.class}`;
+ toast.innerHTML = `${t.icon} ${escHtml(msg)} `;
+ toast.onclick = () => toast.remove();
+
+ const container = document.getElementById('toastContainer') || document.body;
+ container.appendChild(toast);
+ requestAnimationFrame(() => toast.classList.add('show'));
+ setTimeout(() => {
+ toast.classList.remove('show');
+ setTimeout(() => toast.remove(), 300);
+ }, duration);
+}
+// Override default showToast
+window.showToast = showEnhancedToast;
+
+/* ── Connection Quality Monitor ────────────────────────────── */
+const ConnectionMonitor = {
+ quality: 'good',
+ _handlers: [],
+ init() {
+ if (navigator.connection) {
+ const update = () => {
+ const c = navigator.connection;
+ const rtt = c.rtt || 100;
+ this.quality = rtt < 100 ? 'excellent' : rtt < 300 ? 'good' : rtt < 700 ? 'fair' : 'poor';
+ this._handlers.forEach(h => h(this.quality));
+ const badge = document.getElementById('connectionBadge');
+ if (badge) {
+ badge.className = `conn-badge conn-${this.quality}`;
+ badge.title = `جودة الاتصال: ${this.quality}`;
+ }
+ };
+ navigator.connection.addEventListener('change', update);
+ update();
+ }
+ window.addEventListener('online', () => {
+ this.quality = 'good';
+ showEnhancedToast('🌐 عاد الاتصال بالإنترنت', 'success');
+ loadStats(); loadAlerts();
+ });
+ window.addEventListener('offline', () => {
+ this.quality = 'offline';
+ showEnhancedToast('📴 لا يوجد اتصال بالإنترنت', 'warning', 5000);
+ });
+ },
+ onChange(fn) { this._handlers.push(fn); }
+};
+
+/* ── Smart Search with Debounce ────────────────────────────── */
+let _searchTimer = null;
+function smartSearch(query, callback, delay = 400) {
+ clearTimeout(_searchTimer);
+ if (!query || query.trim().length < 2) { callback(null); return; }
+ _searchTimer = setTimeout(async () => {
+ try {
+ const data = await smartFetch(`/api/search?q=${encodeURIComponent(query.trim())}`, { ttl: 15000 });
+ callback(data);
+ } catch(e) { callback(null); }
+ }, delay);
+}
+
+/* ── Scroll to Top Button ──────────────────────────────────── */
+function initScrollToTop() {
+ const btn = document.getElementById('scrollTopBtn');
+ if (!btn) return;
+ const content = document.getElementById('content');
+ if (!content) return;
+ const handleScroll = throttle(() => {
+ if (content.scrollTop > 300) btn.classList.add('show');
+ else btn.classList.remove('show');
+ }, 100);
+ content.addEventListener('scroll', handleScroll, { passive: true });
+ btn.addEventListener('click', () => {
+ content.scrollTo({ top: 0, behavior: 'smooth' });
+ haptic('light');
+ });
+}
+
+/* ── Enhanced App Stats ────────────────────────────────────── */
+async function loadEnhancedStats() {
+ try {
+ const data = await smartFetch('/api/dashboard/full', { ttl: 30000 });
+ if (data.stats) {
+ const s = data.stats;
+ animateNumber(document.getElementById('hUsers'), s.online || 0);
+ animateNumber(document.getElementById('hReports'), s.reports || 0);
+ animateNumber(document.getElementById('hLives'), s.lives_saved || 0);
+ animateNumber(document.getElementById('hCities'), s.cities || 0);
+
+ // Top bar quick stats
+ const liveEl = document.getElementById('liveUsers');
+ const repEl = document.getElementById('liveReports');
+ if (liveEl) liveEl.textContent = (s.online || 0).toLocaleString('ar-EG');
+ if (repEl) repEl.textContent = (s.reports || 0).toLocaleString('ar-EG');
+
+ // Update online count
+ const onlineBig = document.getElementById('onlineCountBig');
+ if (onlineBig) animateNumber(onlineBig, s.online || 0);
+ }
+ if (data.top_areas && data.top_areas.length) {
+ updateTopAreasBar(data.top_areas);
+ }
+ } catch(e) { /* silent fail */ }
+}
+
+function updateTopAreasBar(areas) {
+ const el = document.getElementById('topAreasList');
+ if (!el) return;
+ el.innerHTML = areas.map((a, i) =>
+ `
+ #${i+1}
+ ${escHtml(a.area)}
+ ${a.count}
+
`
+ ).join('');
+}
+
+/* ── Filter by Area ────────────────────────────────────────── */
+function filterByArea(area) {
+ const input = document.getElementById('alertFilterArea') || document.getElementById('filterArea');
+ if (input) { input.value = area; input.dispatchEvent(new Event('input')); }
+ goSection('home');
+ showEnhancedToast(`🗺️ تصفية حسب: ${area}`, 'info');
+}
+
+/* ── Weather Widget ────────────────────────────────────────── */
+async function loadWeatherWidget() {
+ const el = document.getElementById('weatherWidget');
+ if (!el) return;
+ const city = userCity || 'الخرطوم';
+ try {
+ const w = await smartFetch(`/api/weather/${encodeURIComponent(city)}`, { ttl: 300000 });
+ el.innerHTML = `
+
+ ${w.icon || '☀️'}
+ ${w.temp}°
+ ${escHtml(w.condition)}
+ ${escHtml(w.area)}
+
`;
+ el.style.display = 'block';
+ } catch(e) { el.style.display = 'none'; }
+}
+
+/* ── Prayer Times Widget ───────────────────────────────────── */
+async function loadPrayerWidget() {
+ const el = document.getElementById('prayerWidget');
+ if (!el) return;
+ try {
+ const lat = userLat || 15.5;
+ const lng = userLng || 32.5;
+ const p = await smartFetch(`/api/prayer/${lat}/${lng}`, { ttl: 3600000 });
+ if (!p.prayers) return;
+ const now = new Date();
+ const timeStr = `${String(now.getHours()).padStart(2,'0')}:${String(now.getMinutes()).padStart(2,'0')}`;
+ const nextPrayer = Object.entries(p.prayers).find(([, t]) => t > timeStr);
+ if (nextPrayer) {
+ el.innerHTML = `🕌 ${nextPrayer[0] === 'fajr' ? 'الفجر' : nextPrayer[0] === 'dhuhr' ? 'الظهر' : nextPrayer[0] === 'asr' ? 'العصر' : nextPrayer[0] === 'maghrib' ? 'المغرب' : 'العشاء'}: ${nextPrayer[1]}
`;
+ }
+ } catch(e) {}
+}
+
+/* ── Enhanced Leaderboard ──────────────────────────────────── */
+async function loadEnhancedLeaderboard() {
+ const el = document.getElementById('leaderboardList') || document.getElementById('weeklyLeaderboard');
+ if (!el) return;
+ el.innerHTML = skeletonList(5);
+ try {
+ const data = await smartFetch('/api/leaderboard', { ttl: 60000 });
+ if (!data.board || !data.board.length) {
+ el.innerHTML = emptyState('🏆 لا توجد بيانات بعد');
+ return;
+ }
+ el.innerHTML = data.board.slice(0, 10).map((u, i) => `
+
+
${i === 0 ? '🥇' : i === 1 ? '🥈' : i === 2 ? '🥉' : '#' + (i+1)}
+
${u.avatar || '👤'}
+
+
${escHtml(u.name)}
+
${escHtml(u.area || '')}
+
+
+ ${(u.points || 0).toLocaleString('ar-EG')} نقطة
+ ${(u.badges || []).join(' ')}
+
+
`).join('');
+ } catch(e) { el.innerHTML = emptyState('❌ تعذر التحميل'); }
+}
+
+/* ── Blood Bank Stats ──────────────────────────────────────── */
+async function loadBloodStats() {
+ const el = document.getElementById('bloodStats');
+ if (!el) return;
+ try {
+ const data = await smartFetch('/api/blood/stats', { ttl: 120000 });
+ if (!data.stats) return;
+ const bloodTypes = ['A+','A-','B+','B-','AB+','AB-','O+','O-'];
+ el.innerHTML = `${
+ bloodTypes.map(t => `
+
+ ${t}
+ ${data.stats[t] || 0}
+
`).join('')
+ }
إجمالي المتبرعين: ${data.total}
`;
+ } catch(e) {}
+}
+
+/* ── Trending Topics ───────────────────────────────────────── */
+async function loadTrendingTopics() {
+ const el = document.getElementById('trendingList');
+ if (!el) return;
+ try {
+ const data = await smartFetch('/api/trending', { ttl: 120000 });
+ if (!data.items || !data.items.length) {
+ el.innerHTML = emptyState('📊 لا توجد مواضيع رائجة');
+ return;
+ }
+ el.innerHTML = data.items.slice(0, 5).map((item, i) => `
+
+
#${i+1}
+
${escHtml(item.icon || '🔥')}
+
+
${escHtml((item.title || '').substring(0, 50))}
+
${escHtml(item.area || '')} • ${escHtml(timeAgo(item.time))}
+
+
${item.score || ''}
+
`).join('');
+ } catch(e) { el.innerHTML = emptyState('❌ تعذر التحميل'); }
+}
+
+function handleTrendingClick(type, id) {
+ if (type === 'alert') goSection('home');
+ else if (type === 'market') goSection('market');
+ else if (type === 'voice') goSection('voice');
+ else if (type === 'news') goSection('news');
+}
+
+/* ── Enhanced Exchange Rate Display ────────────────────────── */
+async function loadEnhancedExchange() {
+ const el = document.getElementById('exchangeList') || document.getElementById('homeRateNum');
+ try {
+ const data = await smartFetch('/api/exchange', { ttl: 60000 });
+ const rates = data.rates || data;
+ if (!rates || !rates.length) return;
+ const latest = rates[0];
+ if (el && el.id === 'homeRateNum') {
+ el.textContent = (latest.sdg || latest.rate || '---');
+ }
+ // Update top bar rate
+ const rateEl = document.getElementById('liveRate');
+ if (rateEl && latest) rateEl.textContent = (latest.sdg || latest.rate || '---') + ' ج.س';
+ } catch(e) {}
+}
+
+/* ── Polls Quick View ──────────────────────────────────────── */
+async function loadActivePolls() {
+ const el = document.getElementById('activePollsList');
+ if (!el) return;
+ el.innerHTML = skeletonList(3);
+ try {
+ const data = await smartFetch('/api/polls/active', { ttl: 60000 });
+ if (!data.polls || !data.polls.length) { el.innerHTML = emptyState('📊 لا توجد استطلاعات'); return; }
+ el.innerHTML = data.polls.slice(0, 3).map(p => `
+
+
${escHtml(p.question || p.text || '').substring(0, 60)}
+
${p.totalVotes || 0} صوت • ${escHtml(timeAgo(p.time))}
+
`).join('');
+ } catch(e) { el.innerHTML = ''; }
+}
+
+/* ── Help Requests Urgent Banner ───────────────────────────── */
+async function checkUrgentHelp() {
+ try {
+ const data = await smartFetch('/api/help/urgent', { ttl: 60000 });
+ if (data.count > 0) {
+ const banner = document.getElementById('urgentHelpBanner');
+ if (banner) {
+ banner.innerHTML = `⚠️ ${data.count} طلب مساعدة عاجل - عرض الآن `;
+ banner.style.display = 'block';
+ }
+ }
+ } catch(e) {}
+}
+
+/* ── Online Users Map ──────────────────────────────────────── */
+async function updateOnlineUsersOnMap() {
+ if (currentSection !== 'map') return;
+ try {
+ const data = await smartFetch('/api/users/map', { ttl: 15000 });
+ if (!data.users || !map) return;
+ // Already handled by socket nearby_users, but fallback
+ data.users.forEach(u => {
+ if (u.lat && u.lng) {
+ const icon = L.divIcon({ className: 'user-map-dot', html: '👤', iconSize: [20, 20] });
+ L.marker([u.lat, u.lng], { icon }).bindPopup(`${escHtml(u.name)} ${escHtml(u.area || '')}`).addTo(map);
+ }
+ });
+ } catch(e) {}
+}
+
+/* ── Service Worker Message Handler ────────────────────────── */
+if ('serviceWorker' in navigator) {
+ navigator.serviceWorker.addEventListener('message', event => {
+ const { type, payload } = event.data || {};
+ if (type === 'NEW_ALERT') {
+ showEnhancedToast(`🔔 ${payload?.msg || 'تنبيه جديد'}`, 'info');
+ haptic('medium');
+ }
+ if (type === 'SYNC_COMPLETE') {
+ NabdhCache.clear();
+ loadStats(); loadAlerts();
+ }
+ });
+}
+
+/* ── Page Visibility API ───────────────────────────────────── */
+document.addEventListener('visibilitychange', () => {
+ if (!document.hidden) {
+ // Refresh when user comes back
+ setTimeout(() => {
+ loadEnhancedStats();
+ loadAlerts();
+ if (currentSection === 'exchange') loadEnhancedExchange();
+ if (currentSection === 'dashboard') loadDashboard();
+ }, 500);
+ }
+});
+
+/* ── Idle Prefetch ─────────────────────────────────────────── */
+function prefetchOnIdle() {
+ const prefetch = () => {
+ smartFetch('/api/prefetch', { ttl: 300000 }).catch(() => {});
+ smartFetch('/api/stats/quick', { ttl: 30000 }).catch(() => {});
+ smartFetch('/api/trending', { ttl: 120000 }).catch(() => {});
+ };
+ if ('requestIdleCallback' in window) requestIdleCallback(prefetch, { timeout: 5000 });
+ else setTimeout(prefetch, 3000);
+}
+
+/* ── Share Content ─────────────────────────────────────────── */
+async function shareContent(title, text, url) {
+ const shareUrl = url || getAppUrl();
+ if (navigator.share) {
+ try { await navigator.share({ title, text, url: shareUrl }); haptic('success'); return true; }
+ catch(e) {}
+ }
+ try {
+ await navigator.clipboard.writeText(`${title}\n${text}\n${shareUrl}`);
+ showEnhancedToast('📋 تم نسخ الرابط', 'success');
+ return true;
+ } catch(e) {
+ showEnhancedToast('❌ تعذر المشاركة', 'error');
+ return false;
+ }
+}
+
+/* ── Copy to Clipboard ─────────────────────────────────────── */
+async function copyToClipboard(text) {
+ try {
+ await navigator.clipboard.writeText(text);
+ showEnhancedToast('✅ تم النسخ', 'success', 2000);
+ haptic('light');
+ } catch(e) {
+ showEnhancedToast('❌ تعذر النسخ', 'error');
+ }
+}
+
+/* ── Image Viewer ──────────────────────────────────────────── */
+function openImageViewer(src, caption = '') {
+ const existing = document.getElementById('imgViewerOverlay');
+ if (existing) existing.remove();
+ const overlay = document.createElement('div');
+ overlay.id = 'imgViewerOverlay';
+ overlay.className = 'img-viewer-overlay';
+ overlay.innerHTML = `
+ ✕
+
+ ${caption ? `${escHtml(caption)}
` : ''}
+ `;
+ document.body.appendChild(overlay);
+ overlay.addEventListener('click', e => { if (e.target === overlay) overlay.remove(); });
+ requestAnimationFrame(() => overlay.classList.add('show'));
+}
+
+/* ── Format File Size ──────────────────────────────────────── */
+function formatSize(bytes) {
+ if (bytes < 1024) return bytes + ' B';
+ if (bytes < 1048576) return (bytes / 1024).toFixed(1) + ' KB';
+ return (bytes / 1048576).toFixed(1) + ' MB';
+}
+
+/* ── Date Formatter ────────────────────────────────────────── */
+function formatDate(ts) {
+ const d = new Date(ts);
+ return d.toLocaleDateString('ar-EG', { year: 'numeric', month: 'long', day: 'numeric' });
+}
+function formatDateTime(ts) {
+ const d = new Date(ts);
+ return d.toLocaleString('ar-EG', { hour: '2-digit', minute: '2-digit', month: 'short', day: 'numeric' });
+}
+
+/* ── Quick Stats for Top Bar ───────────────────────────────── */
+async function loadTopBarStats() {
+ try {
+ const data = await smartFetch('/api/stats/quick', { ttl: 20000 });
+ const rateEl = document.getElementById('liveRate');
+ const usersEl = document.getElementById('liveUsers');
+ const reportsEl = document.getElementById('liveReports');
+ if (rateEl && data.rate) rateEl.textContent = data.rate;
+ if (usersEl && data.online !== undefined) usersEl.textContent = data.online.toLocaleString('ar-EG');
+ if (reportsEl && data.reports !== undefined) reportsEl.textContent = data.reports.toLocaleString('ar-EG');
+ } catch(e) {}
+}
+
+/* ── Keyboard Navigation ───────────────────────────────────── */
+function initKeyboardNav() {
+ document.addEventListener('keydown', e => {
+ if (e.target.matches('input, textarea, select')) return;
+ const shortcuts = {
+ 'h': () => goSection('home'),
+ 'm': () => goSection('map'),
+ 'p': () => goSection('people'),
+ 'e': () => goSection('exchange'),
+ 'r': () => goSection('report'),
+ };
+ if (!e.ctrlKey && !e.altKey && !e.metaKey && shortcuts[e.key]) {
+ shortcuts[e.key]();
+ }
+ });
+}
+
+/* ── Swipe Navigation ──────────────────────────────────────── */
+function initSwipeNav() {
+ let xStart = null;
+ const sections = ['home', 'map', 'report', 'people', 'messages'];
+ document.addEventListener('touchstart', e => { xStart = e.touches[0].clientX; }, { passive: true });
+ document.addEventListener('touchend', e => {
+ if (xStart === null) return;
+ const diff = xStart - e.changedTouches[0].clientX;
+ xStart = null;
+ if (Math.abs(diff) < 80) return;
+ const idx = sections.indexOf(currentSection);
+ if (diff > 0 && idx < sections.length - 1) goSection(sections[idx + 1]);
+ else if (diff < 0 && idx > 0) goSection(sections[idx - 1]);
+ }, { passive: true });
+}
+
+/* ── Adaptive Refresh Rate ─────────────────────────────────── */
+const AdaptiveRefresh = {
+ _intervals: {},
+ set(key, fn, baseMsec) {
+ this.clear(key);
+ const ms = ConnectionMonitor.quality === 'poor' ? baseMsec * 3 :
+ ConnectionMonitor.quality === 'fair' ? baseMsec * 1.5 : baseMsec;
+ this._intervals[key] = setInterval(fn, ms);
+ },
+ clear(key) {
+ if (this._intervals[key]) { clearInterval(this._intervals[key]); delete this._intervals[key]; }
+ }
+};
+
+/* ── Network Monitor Integration ───────────────────────────── */
+ConnectionMonitor.onChange(() => {
+ AdaptiveRefresh.set('stats', loadTopBarStats, 20000);
+ AdaptiveRefresh.set('alerts', loadAlerts, 30000);
+});
+
+/* ── Initialize All v7.1 Features ─────────────────────────── */
+document.addEventListener('DOMContentLoaded', () => {
+ // Initialize UI enhancements
+ setTimeout(() => {
+ initScrollToTop();
+ initKeyboardNav();
+ initSwipeNav();
+ ConnectionMonitor.init();
+ prefetchOnIdle();
+ loadTopBarStats();
+ loadEnhancedStats();
+ loadTrendingTopics();
+ loadWeatherWidget();
+ loadPrayerWidget();
+ checkUrgentHelp();
+ observeLazy(document);
+ }, 1000);
+
+ // Setup adaptive refresh
+ AdaptiveRefresh.set('stats', loadTopBarStats, 20000);
+ AdaptiveRefresh.set('enhanced_stats', loadEnhancedStats, 30000);
+ AdaptiveRefresh.set('trending', loadTrendingTopics, 120000);
+ AdaptiveRefresh.set('weather', loadWeatherWidget, 300000);
+
+ // PTR on main content
+ const content = document.getElementById('content');
+ if (content) {
+ initPullToRefresh('content', async () => {
+ await Promise.all([loadStats(), loadAlerts(), loadTopBarStats()]);
+ showEnhancedToast('✅ تم التحديث', 'success', 2000);
+ });
+ }
+});
+
+/* ============================================================
+ 🔧 MISSING FUNCTIONS — تعريفات الدوال الناقصة
+============================================================ */
+
+// showGroupInfo: shows group info tab inside group page
+function showGroupInfo() {
+ if (typeof switchGroupTab === 'function') { switchGroupTab('info'); return; }
+ var el = document.getElementById('gpTabInfo');
+ if (el) {
+ ['gpTabChat','gpTabMembers','gpTabMedia','gpTabInfo'].forEach(function(id){
+ var e2 = document.getElementById(id); if (e2) e2.classList.add('hidden');
+ });
+ el.classList.remove('hidden');
+ if (typeof renderGroupInfo === 'function') renderGroupInfo();
+ }
+}
+
+// cancelDMReply: cancels reply mode in DM chat
+function cancelDMReply() {
+ window.dmReplyingTo = null;
+ var banner = document.getElementById('dmReplyBanner') || document.querySelector('.grp-reply-preview');
+ if (banner) banner.classList.add('hidden');
+ var inp = document.getElementById('dmInput') || document.getElementById('dmMsgInput');
+ if (inp) inp.placeholder = 'اكتب رسالة...';
+}
+
+/* ═══════════════════════════════════════════════════════
+ ONBOARDING — يظهر مرة واحدة فقط عند أول زيارة
+ المفتاح في localStorage: nabdh_onboarding_done
+ ═══════════════════════════════════════════════════════ */
+(function() {
+ 'use strict';
+
+ var ONB_KEY = 'nabdh_onboarding_done';
+ var TOTAL = 6; // عدد الشرائح
+ var current = 0;
+
+ /* ── هل أكمل المستخدم الدليل من قبل؟ ── */
+ function isDone() {
+ try { return !!localStorage.getItem(ONB_KEY); } catch(e) { return false; }
+ }
+ function markDone() {
+ try { localStorage.setItem(ONB_KEY, '1'); } catch(e) {}
+ }
+
+ /* ── بناء نقاط المؤشر ── */
+ function buildDots() {
+ var container = document.getElementById('onbDots');
+ if (!container) return;
+ container.innerHTML = '';
+ for (var i = 0; i < TOTAL; i++) {
+ var d = document.createElement('span');
+ d.className = 'onb-dot' + (i === 0 ? ' active' : '');
+ d.dataset.idx = i;
+ (function(idx){ d.addEventListener('click', function(){ goSlide(idx); }); })(i);
+ container.appendChild(d);
+ }
+ }
+
+ /* ── تحديث شريط التقدم والنقاط والأزرار ── */
+ function updateUI() {
+ /* شريط التقدم */
+ var fill = document.getElementById('onbProgressFill');
+ if (fill) fill.style.width = (((current + 1) / TOTAL) * 100) + '%';
+
+ /* نقاط المؤشر */
+ document.querySelectorAll('.onb-dot').forEach(function(d, i) {
+ d.classList.toggle('active', parseInt(d.dataset.idx) === current);
+ });
+
+ /* زر السابق */
+ var prev = document.getElementById('onbPrevBtn');
+ if (prev) prev.style.visibility = current > 0 ? 'visible' : 'hidden';
+
+ /* زر التالي / ابدأ الآن */
+ var next = document.getElementById('onbNextBtn');
+ if (next) {
+ if (current === TOTAL - 1) {
+ next.textContent = '🚀 ابدأ الآن';
+ next.classList.add('onb-finish');
+ } else {
+ next.textContent = 'التالي →';
+ next.classList.remove('onb-finish');
+ }
+ }
+ }
+
+ /* ── الانتقال إلى شريحة معيّنة ── */
+ function goSlide(idx) {
+ var slides = document.querySelectorAll('.onb-slide');
+ if (!slides.length) return;
+ slides[current].classList.remove('active');
+ current = Math.max(0, Math.min(idx, TOTAL - 1));
+ slides[current].classList.add('active');
+ updateUI();
+ }
+
+ /* ── التالي ── */
+ window.onbNext = function() {
+ if (current < TOTAL - 1) {
+ goSlide(current + 1);
+ } else {
+ onbClose();
+ }
+ };
+
+ /* ── السابق ── */
+ window.onbPrev = function() {
+ if (current > 0) goSlide(current - 1);
+ };
+
+ /* ── تخطي / إغلاق نهائي ── */
+ window.onbSkip = function() {
+ onbClose();
+ };
+
+ function onbClose() {
+ markDone();
+ var overlay = document.getElementById('onboardingOverlay');
+ if (!overlay) return;
+ /* انيميشن خروج لطيف */
+ overlay.style.transition = 'opacity .35s ease';
+ overlay.style.opacity = '0';
+ setTimeout(function() {
+ overlay.classList.add('hidden');
+ overlay.style.opacity = '';
+ overlay.style.transition = '';
+ }, 360);
+ }
+
+ /* ── تشغيل الدليل بعد اختفاء splash ── */
+ function launchOnboarding() {
+ if (isDone()) return; // سبق وأنهاه → لا شيء
+ buildDots();
+ updateUI();
+ var overlay = document.getElementById('onboardingOverlay');
+ if (!overlay) return;
+ overlay.classList.remove('hidden');
+ }
+
+ /* ننتظر حتى تختفي شاشة التحميل (splash ~1.6 ثانية) ثم نعرض الدليل */
+ window.addEventListener('DOMContentLoaded', function() {
+ if (isDone()) return;
+ /* نؤخر الظهور قليلاً بعد انتهاء splash + initApp */
+ setTimeout(launchOnboarding, 1800);
+ });
+
+})();
+
+/* ============================================================
+ ⚙️ SETTINGS - صفحة الإعدادات
+ ============================================================ */
+var _nabdhSettings = JSON.parse(localStorage.getItem('nabdh_settings') || '{}');
+
+function openSettings() {
+ var modal = document.getElementById('settingsModal');
+ if (!modal) return;
+ // تحميل الإعدادات الحالية
+ _loadSettingsUI();
+ modal.classList.remove('hidden');
+}
+
+function closeSettings(e) {
+ if (e && e.target !== document.getElementById('settingsModal')) return;
+ var modal = document.getElementById('settingsModal');
+ if (modal) modal.classList.add('hidden');
+ if (!e) { modal && modal.classList.add('hidden'); }
+}
+
+function _loadSettingsUI() {
+ // حجم الخط
+ var size = _nabdhSettings.fontSize || 'medium';
+ ['small','medium','large'].forEach(function(s) {
+ var btn = document.getElementById('fsBtn' + s.charAt(0).toUpperCase() + s.slice(1));
+ if (btn) btn.classList.toggle('active-fs', s === size);
+ });
+ // الوضع الهادئ
+ var qm = document.getElementById('quietModeToggle');
+ if (qm) qm.checked = !!_nabdhSettings.quietMode;
+ // منبه الصلاة
+ var pt = document.getElementById('settingsPrayerToggle');
+ if (pt) pt.checked = !!_nabdhSettings.prayerAlarm;
+ // إشعارات أخرى
+ var at = document.getElementById('settingsAchievToggle');
+ if (at) at.checked = _nabdhSettings.notif_achiev !== false;
+ var nt = document.getElementById('settingsNearbyToggle');
+ if (nt) nt.checked = _nabdhSettings.notif_nearby !== false;
+ var dt = document.getElementById('settingsDmToggle');
+ if (dt) dt.checked = _nabdhSettings.notif_dm !== false;
+ // الصوت
+ var snd = document.getElementById('soundToggle');
+ if (snd) snd.checked = _nabdhSettings.soundEnabled !== false;
+ // إصدار
+ var sv = document.getElementById('settingsVersion');
+ if (sv) sv.textContent = 'v7.9';
+ // قائمة المحظورين
+ _renderBlockedList();
+}
+
+function setFontSize(size) {
+ _nabdhSettings.fontSize = size;
+ _saveSettings();
+ // تطبيق على الـ body
+ var root = document.documentElement;
+ if (size === 'small') root.style.fontSize = '14px';
+ if (size === 'medium') root.style.fontSize = '16px';
+ if (size === 'large') root.style.fontSize = '18px';
+ ['small','medium','large'].forEach(function(s) {
+ var btn = document.getElementById('fsBtn' + s.charAt(0).toUpperCase() + s.slice(1));
+ if (btn) btn.classList.toggle('active-fs', s === size);
+ });
+ showToast('✅ تم تغيير حجم الخط', 'success');
+}
+
+function toggleQuietMode(on) {
+ _nabdhSettings.quietMode = on;
+ _saveSettings();
+ showToast(on ? '🔕 الوضع الهادئ مفعّل' : '🔔 الإشعارات مفعّلة', on ? 'info' : 'success');
+}
+
+function saveSettingsBool(key, val) {
+ _nabdhSettings[key] = val;
+ _saveSettings();
+}
+
+function togglePrayerAlarmFromSettings(on) {
+ _nabdhSettings.prayerAlarm = on;
+ _saveSettings();
+ // تزامن مع قسم الصلاة
+ var pt = document.getElementById('prayerAlarmToggle');
+ if (pt) pt.checked = on;
+ var status = document.getElementById('prayerAlarmStatus');
+ if (status) status.textContent = on ? '⏰ مفعّل — 5 دقائق قبل كل صلاة' : 'غير مفعّل';
+ var detail = document.getElementById('prayerAlarmDetail');
+ if (detail) detail.classList.toggle('hidden', !on);
+ showToast(on ? '🔔 منبّه الصلاة مفعّل' : '🔕 منبّه الصلاة مُعطَّل', on ? 'success' : 'info');
+}
+
+function _saveSettings() {
+ localStorage.setItem('nabdh_settings', JSON.stringify(_nabdhSettings));
+}
+
+function clearAppCache() {
+ var keys = Object.keys(localStorage).filter(function(k){
+ return k.startsWith('nabdh_cache_') || k.startsWith('nabdh_tmp_');
+ });
+ keys.forEach(function(k){ localStorage.removeItem(k); });
+ if (caches) {
+ caches.keys().then(function(names){
+ names.filter(function(n){ return n !== 'nabdh-v9' && n !== 'nabdh-static-v9'; })
+ .forEach(function(n){ caches.delete(n); });
+ });
+ }
+ showToast('✅ تم مسح الذاكرة المؤقتة', 'success');
+}
+
+function exportMyData() {
+ var data = {
+ profile: JSON.parse(localStorage.getItem('nabdh_profile') || 'null'),
+ settings: _nabdhSettings,
+ exportedAt: new Date().toISOString()
+ };
+ var blob = new Blob([JSON.stringify(data, null, 2)], {type:'application/json'});
+ var url = URL.createObjectURL(blob);
+ var a = document.createElement('a');
+ a.href = url; a.download = 'nabdh_data.json'; a.click();
+ URL.revokeObjectURL(url);
+ showToast('📥 تم تصدير البيانات', 'success');
+}
+
+/* تطبيق حجم الخط عند التحميل */
+(function(){
+ var s = (JSON.parse(localStorage.getItem('nabdh_settings') || '{}')).fontSize;
+ if (s === 'small') document.documentElement.style.fontSize = '14px';
+ if (s === 'large') document.documentElement.style.fontSize = '18px';
+})();
+
+
+/* ============================================================
+ 🔔 PRAYER ALARM - منبّه أوقات الصلاة
+ ============================================================ */
+var _prayerAlarmTimeout = null;
+var _prayerAlarmTimes = null;
+var _prayerAlarmNotified = {};
+
+function togglePrayerAlarm(on) {
+ _nabdhSettings.prayerAlarm = on;
+ _saveSettings();
+ var status = document.getElementById('prayerAlarmStatus');
+ var detail = document.getElementById('prayerAlarmDetail');
+ if (status) status.textContent = on ? '⏰ مفعّل — 5 دقائق قبل كل صلاة' : 'غير مفعّل';
+ if (detail) detail.classList.toggle('hidden', !on);
+ // تزامن مع صفحة الإعدادات
+ var st = document.getElementById('settingsPrayerToggle');
+ if (st) st.checked = on;
+ if (on) {
+ _requestNotifPerm(function(granted){
+ if (granted) {
+ showToast('🔔 سيصلك إشعار 5 دقائق قبل كل صلاة', 'success');
+ if (_prayerAlarmTimes) _schedulePrayerAlarms(_prayerAlarmTimes);
+ } else {
+ showToast('⚠️ فعّل إشعارات المتصفح لتعمل المنبّهات', 'warning');
+ on = false;
+ _nabdhSettings.prayerAlarm = false;
+ _saveSettings();
+ var tog = document.getElementById('prayerAlarmToggle');
+ if (tog) tog.checked = false;
+ if (status) status.textContent = 'غير مفعّل';
+ if (detail) detail.classList.add('hidden');
+ }
+ });
+ } else {
+ if (_prayerAlarmTimeout) clearTimeout(_prayerAlarmTimeout);
+ showToast('🔕 منبّه الصلاة مُعطَّل', 'info');
+ }
+}
+
+function _requestNotifPerm(cb) {
+ if (!('Notification' in window)) return cb(false);
+ if (Notification.permission === 'granted') return cb(true);
+ if (Notification.permission === 'denied') return cb(false);
+ Notification.requestPermission().then(function(p){ cb(p === 'granted'); });
+}
+
+function _schedulePrayerAlarms(times) {
+ _prayerAlarmTimes = times;
+ if (!_nabdhSettings.prayerAlarm) return;
+ var prayers = [
+ {name:'الفجر', key:'fajr'},
+ {name:'الشروق', key:'sunrise'},
+ {name:'الظهر', key:'dhuhr'},
+ {name:'العصر', key:'asr'},
+ {name:'المغرب', key:'maghrib'},
+ {name:'العشاء', key:'isha'}
+ ];
+ var now = Date.now();
+ prayers.forEach(function(p){
+ var timeStr = times[p.key];
+ if (!timeStr || timeStr === '—') return;
+ var m = timeStr.match(/(\d+):(\d+)\s*(AM|PM)?/i);
+ if (!m) return;
+ var h = parseInt(m[1]), mn = parseInt(m[2]);
+ if (m[3]) {
+ if (m[3].toUpperCase() === 'PM' && h < 12) h += 12;
+ if (m[3].toUpperCase() === 'AM' && h === 12) h = 0;
+ }
+ var today = new Date();
+ var pTime = new Date(today.getFullYear(), today.getMonth(), today.getDate(), h, mn, 0);
+ var alertTime = pTime.getTime() - 5 * 60 * 1000; // 5 دقائق قبل
+ var delay = alertTime - now;
+ if (delay < 0) return; // فات الوقت
+ var key = p.key + '_' + today.toDateString();
+ if (_prayerAlarmNotified[key]) return; // بالفعل أُشعر
+ setTimeout(function(){
+ if (!_nabdhSettings.prayerAlarm) return;
+ _prayerAlarmNotified[key] = true;
+ // إشعار المتصفح
+ if (Notification.permission === 'granted') {
+ new Notification('🕌 نبض — أوقات الصلاة', {
+ body: '⏰ ' + p.name + ' بعد 5 دقائق',
+ icon: '/favicon.svg',
+ tag: 'prayer_' + p.key
+ });
+ }
+ // Toast داخل التطبيق
+ showToast('🕌 ' + p.name + ' بعد 5 دقائق', 'success');
+ }, delay);
+ });
+}
+
+/* hook في startPrayerCountdown لحفظ الأوقات وجدولة المنبّهات */
+var _origStartPrayerCountdown = typeof startPrayerCountdown === 'function' ? startPrayerCountdown : null;
+if (_origStartPrayerCountdown) {
+ startPrayerCountdown = function(times) {
+ _prayerAlarmTimes = times;
+ _origStartPrayerCountdown(times);
+ if (_nabdhSettings.prayerAlarm) _schedulePrayerAlarms(times);
+ };
+}
+
+/* تهيئة حالة المنبّه عند تحميل الصفحة */
+window.addEventListener('DOMContentLoaded', function(){
+ var saved = JSON.parse(localStorage.getItem('nabdh_settings') || '{}');
+ if (saved.prayerAlarm) {
+ var tog = document.getElementById('prayerAlarmToggle');
+ if (tog) tog.checked = true;
+ var status = document.getElementById('prayerAlarmStatus');
+ if (status) status.textContent = '⏰ مفعّل — 5 دقائق قبل كل صلاة';
+ var detail = document.getElementById('prayerAlarmDetail');
+ if (detail) detail.classList.remove('hidden');
+ }
+});
+
+
+/* ============================================================
+ 🧮 CURRENCY CALCULATOR - حاسبة الصرف
+ ============================================================ */
+/* نسب تقريبية بالنسبة لـ USD - تُحدَّث من سعر السوق للـ SDG */
+var _currencyRates = {
+ usd: 1,
+ eur: 0.92,
+ gbp: 0.79,
+ sar: 3.75,
+ aed: 3.67,
+ egp: 30.9,
+ sdg: null /* يُجلب من /api/exchange */
+};
+
+/* نجلب سعر الجنيه السوداني من API */
+(function fetchSDGRate(){
+ fetch('/api/exchange')
+ .then(function(r){ return r.json(); })
+ .then(function(data){
+ if (Array.isArray(data) && data.length) {
+ _currencyRates.sdg = data[0].rate;
+ } else {
+ _currencyRates.sdg = 600; // افتراضي
+ }
+ calcCurrency();
+ })
+ .catch(function(){ _currencyRates.sdg = 600; });
+})();
+
+function calcCurrency() {
+ var amountEl = document.getElementById('calcAmount');
+ var fromEl = document.getElementById('calcFrom');
+ var toEl = document.getElementById('calcTo');
+ var resultEl = document.getElementById('calcResult');
+ var noteEl = document.getElementById('calcNote');
+ if (!amountEl || !fromEl || !toEl || !resultEl) return;
+
+ var amount = parseFloat(amountEl.value);
+ var from = fromEl.value;
+ var to = toEl.value;
+
+ if (!amount || isNaN(amount) || amount <= 0) {
+ resultEl.textContent = '—';
+ return;
+ }
+
+ var rateFrom = _currencyRates[from];
+ var rateTo = _currencyRates[to];
+
+ if (!rateFrom || !rateTo) {
+ resultEl.textContent = '⏳';
+ return;
+ }
+
+ // تحويل للـ USD أولاً ثم للعملة المطلوبة
+ var inUSD = amount / rateFrom;
+ var result = inUSD * rateTo;
+
+ // تنسيق العدد
+ var formatted = result >= 1000
+ ? result.toLocaleString('ar-SA', {maximumFractionDigits: 0})
+ : result.toLocaleString('ar-SA', {maximumFractionDigits: 2});
+
+ resultEl.textContent = formatted;
+
+ if (noteEl) {
+ var note = from === 'sdg' || to === 'sdg'
+ ? 'يستخدم سعر السوق المُبلَّغ عنه مباشرةً' + (_currencyRates.sdg ? ' (' + _currencyRates.sdg + ' ج.س/$)' : '')
+ : 'أسعار تقريبية — للاسترشاد فقط';
+ noteEl.textContent = note;
+ }
+}
+
+function swapCalcCurrency() {
+ var fromEl = document.getElementById('calcFrom');
+ var toEl = document.getElementById('calcTo');
+ if (!fromEl || !toEl) return;
+ var tmp = fromEl.value;
+ fromEl.value = toEl.value;
+ toEl.value = tmp;
+ calcCurrency();
+}
+
+
+/* ============================================================
+ 🚫 BLOCK USER - حظر المستخدم
+ ============================================================ */
+var _blockedUsers = JSON.parse(localStorage.getItem('nabdh_blocked') || '[]');
+
+function _saveBlocked() {
+ localStorage.setItem('nabdh_blocked', JSON.stringify(_blockedUsers));
+}
+
+function isUserBlocked(userId) {
+ return _blockedUsers.some(function(u){ return u.id === userId; });
+}
+
+function blockUser(userId, userName) {
+ if (isUserBlocked(userId)) return;
+ _blockedUsers.push({ id: userId, name: userName || userId, blockedAt: Date.now() });
+ _saveBlocked();
+ _updateBlockBtn(userId);
+ _renderBlockedList();
+ showToast('🚫 تم حظر ' + (userName || 'المستخدم'), 'warning');
+}
+
+function unblockUser(userId) {
+ var name = (_blockedUsers.find(function(u){ return u.id === userId; }) || {}).name || 'المستخدم';
+ _blockedUsers = _blockedUsers.filter(function(u){ return u.id !== userId; });
+ _saveBlocked();
+ _renderBlockedList();
+ _updateBlockBtn(userId);
+ showToast('✅ تم رفع الحظر عن ' + name, 'success');
+}
+
+function toggleBlockUser() {
+ if (!dmCurrentUser) return;
+ var userId = dmCurrentUser.id;
+ var userName = dmCurrentUser.name;
+ if (isUserBlocked(userId)) {
+ // تأكيد رفع الحظر
+ if (confirm('رفع الحظر عن ' + userName + '؟')) {
+ unblockUser(userId);
+ }
+ } else {
+ // تأكيد الحظر
+ if (confirm('حظر ' + userName + '؟\nلن يتمكن من إرسال رسائل إليك.')) {
+ blockUser(userId, userName);
+ }
+ }
+}
+
+function _updateBlockBtn(userId) {
+ var btn = document.getElementById('dmBlockBtn');
+ if (!btn) return;
+ var blocked = isUserBlocked(userId);
+ btn.classList.toggle('blocked-active', blocked);
+ btn.title = blocked ? 'رفع الحظر' : 'حظر المستخدم';
+ btn.textContent = blocked ? '🔓' : '🚫';
+}
+
+/* نحدّث زر الحظر عند فتح المحادثة */
+var _origOpenDMChatPage = typeof openDMChatPage === 'function' ? openDMChatPage : null;
+if (_origOpenDMChatPage) {
+ openDMChatPage = function(userId, userName) {
+ _origOpenDMChatPage(userId, userName);
+ setTimeout(function(){ _updateBlockBtn(userId); }, 100);
+ };
+}
+
+/* منع تلقي رسائل من المحظورين في loadDMMessages */
+var _origLoadDMMessages = typeof loadDMMessages === 'function' ? loadDMMessages : null;
+if (_origLoadDMMessages) {
+ loadDMMessages = function(userId) {
+ if (isUserBlocked(userId)) {
+ var cont = document.getElementById('dmChatMessages');
+ if (cont) cont.innerHTML = '🚫
هذا المستخدم محظورلا يمكن إرسال رسائل أو استقبالها
🔓 رفع الحظر ';
+ return;
+ }
+ _origLoadDMMessages(userId);
+ };
+}
+
+function _renderBlockedList() {
+ var list = document.getElementById('blockedUsersList');
+ if (!list) return;
+ if (!_blockedUsers.length) {
+ list.innerHTML = 'لا يوجد مستخدمون محظورون
';
+ return;
+ }
+ list.innerHTML = _blockedUsers.map(function(u){
+ return '' +
+ '🚫 ' + escHtml(u.name) + ' ' +
+ 'رفع الحظر ' +
+ '
';
+ }).join('');
+}
+
+
+/* ============================================================
+ 🔊 SOUND NOTIFICATIONS — تنبيهات صوتية للأحداث الجديدة
+ ============================================================
+ يعمل عبر Web Audio API (بدون ملفات خارجية)
+ أنواع الأصوات:
+ • alert – تنبيه/حادثة جديدة
+ • sos – نداء استغاثة
+ • msg – رسالة جديدة
+ • market – منتج/إعلان جديد
+ • prayer – تنبيه صلاة
+ • nearby – إضافة من المنطقة المحيطة
+ يحترم: الوضع الهادئ + إعدادات الإشعارات
+ ============================================================ */
+
+var _audioCtx = null;
+
+function _getAudioCtx() {
+ if (!_audioCtx) {
+ try { _audioCtx = new (window.AudioContext || window.webkitAudioContext)(); } catch(e) {}
+ }
+ return _audioCtx;
+}
+
+/** تشغيل نغمة بسيطة عبر Web Audio API */
+function _playTone(freqList, durList, waveform) {
+ if (_nabdhSettings.quietMode) return; // الوضع الهادئ
+ var ctx = _getAudioCtx();
+ if (!ctx) return;
+ // استيقاظ السياق إذا كان معلقاً
+ if (ctx.state === 'suspended') { ctx.resume(); }
+
+ var now = ctx.currentTime;
+ var wave = waveform || 'sine';
+
+ freqList.forEach(function(freq, i) {
+ var osc = ctx.createOscillator();
+ var gain = ctx.createGain();
+ osc.connect(gain);
+ gain.connect(ctx.destination);
+
+ osc.type = wave;
+ osc.frequency.setValueAtTime(freq, now);
+
+ var start = now + (durList[i] ? durList.slice(0,i).reduce(function(a,b){return a+b;},0) : 0);
+ var dur = durList[i] || 0.15;
+
+ gain.gain.setValueAtTime(0, start);
+ gain.gain.linearRampToValueAtTime(0.25, start + 0.01);
+ gain.gain.exponentialRampToValueAtTime(0.001, start + dur);
+
+ osc.start(start);
+ osc.stop(start + dur + 0.05);
+ });
+}
+
+/** خزّان أصوات حسب النوع */
+var _soundDefs = {
+ alert: { freqs:[520, 420], durs:[0.12, 0.18], wave:'triangle' },
+ sos: { freqs:[880, 660, 880, 660, 880], durs:[0.1,0.08,0.1,0.08,0.2], wave:'sawtooth' },
+ msg: { freqs:[660, 880], durs:[0.1, 0.12], wave:'sine' },
+ market: { freqs:[440, 550, 660], durs:[0.1, 0.1, 0.15], wave:'sine' },
+ prayer: { freqs:[330, 392, 440, 494, 523], durs:[0.15,0.15,0.15,0.15,0.3], wave:'sine' },
+ nearby: { freqs:[480, 600], durs:[0.1, 0.15], wave:'triangle' },
+ dm: { freqs:[700, 900], durs:[0.08, 0.12], wave:'sine' },
+ blood: { freqs:[440, 330, 440], durs:[0.1, 0.08, 0.2], wave:'triangle' },
+ power: { freqs:[220, 330], durs:[0.15, 0.2], wave:'square' },
+ water: { freqs:[550, 660, 770], durs:[0.1, 0.1, 0.12], wave:'sine' },
+ achiev: { freqs:[523, 659, 784, 1047], durs:[0.12,0.12,0.12,0.25], wave:'sine' }
+};
+
+/**
+ * الدالة الرئيسية: شغّل صوت التنبيه
+ * @param {string} type - نوع التنبيه (alert|sos|msg|market|prayer|nearby|dm|blood|power|water|achiev)
+ * @param {string} [notifKey] - مفتاح الإعداد للتحقق (notif_nearby|notif_dm|notif_achiev)
+ */
+function playNotifSound(type, notifKey) {
+ // تحقق من الوضع الهادئ
+ if (_nabdhSettings.quietMode) return;
+ // تحقق من إعداد مخصص
+ if (notifKey && _nabdhSettings[notifKey] === false) return;
+ var def = _soundDefs[type] || _soundDefs.alert;
+ _playTone(def.freqs, def.durs, def.wave);
+}
+
+/* ── hook في onNewAlert لتشغيل الصوت ── */
+var _origOnNewAlert = typeof onNewAlert === 'function' ? onNewAlert : null;
+onNewAlert = function(alert) {
+ if (_origOnNewAlert) _origOnNewAlert(alert);
+ // تحقق من إعداد إشعارات التنبيهات القريبة
+ if (_nabdhSettings.notif_nearby === false) return;
+ // صوت حسب نوع الحدث
+ var type = 'alert';
+ if (alert.type === 'sos' || (alert.icon && alert.icon.includes('🆘'))) type = 'sos';
+ else if (alert.type === 'blood' || (alert.icon && alert.icon.includes('🩸'))) type = 'blood';
+ else if (alert.type === 'power' || alert.type === 'electricity') type = 'power';
+ else if (alert.type === 'water') type = 'water';
+ playNotifSound(type);
+};
+
+/* ── hook في socket new_market_item ── */
+var _origConnectSocket = typeof connectSocket === 'function' ? connectSocket : null;
+// نضيف الصوت مباشرة على socket events عبر مراقبة socket
+(function _patchSocketSounds(){
+ var _patchInterval = setInterval(function(){
+ if (typeof socket === 'undefined' || !socket) return;
+ clearInterval(_patchInterval);
+
+ // رسائل السوق
+ socket.on('new_market_item', function(item){
+ if (_nabdhSettings.notif_nearby !== false) playNotifSound('market');
+ });
+
+ // رسائل الدم
+ socket.on('new_blood_request', function(){
+ playNotifSound('blood');
+ });
+
+ // تنبيه SOS مباشر
+ socket.on('sos_alert', function(){
+ playNotifSound('sos');
+ });
+
+ // رسائل مباشرة
+ socket.on('dm_msg', function(){
+ if (_nabdhSettings.notif_dm !== false) playNotifSound('dm', 'notif_dm');
+ });
+
+ // تنبيهات قريبة عامة (nearby alerts update)
+ socket.on('new_alert', function(alert){
+ // الصوت يُشغَّل من onNewAlert المعدَّلة أعلاه
+ });
+
+ }, 500);
+})();
+
+/* ── hook في showToast لصوت الإنجازات ── */
+var _origShowToast = typeof showToast === 'function' ? showToast : null;
+if (_origShowToast) {
+ showToast = function(msg, type) {
+ _origShowToast(msg, type);
+ if (type === 'success' && _nabdhSettings.notif_achiev !== false) {
+ // صوت للإنجازات (النقاط / السويسترات)
+ if (typeof msg === 'string' && (msg.includes('نقطة') || msg.includes('🏆') || msg.includes('🥇') || msg.includes('مستوى'))) {
+ playNotifSound('achiev', 'notif_achiev');
+ }
+ }
+ };
+}
+
+/* ── تنبيه صوتي لصلاة الأذان (يُدمج مع المنبّه) ── */
+var _origSchedulePrayerAlarms = typeof _schedulePrayerAlarms === 'function' ? _schedulePrayerAlarms : null;
+/* نضيف الصوت داخل setTimeout الخاص بالمنبّه */
+/* يعتمد على _prayerAlarmTimes الموجود — نُعيد تعريف الدالة */
+(function _patchPrayerSound(){
+ var _patchInterval2 = setInterval(function(){
+ if (typeof _schedulePrayerAlarms !== 'function') return;
+ clearInterval(_patchInterval2);
+ var _origSched = _schedulePrayerAlarms;
+ _schedulePrayerAlarms = function(times) {
+ /* نُشغّل الأصل وبعده نُضيف الصوت */
+ _origSched(times);
+ /* نُجدول الصوت بنفس المنطق */
+ if (!_nabdhSettings.prayerAlarm) return;
+ var prayers2 = [
+ {key:'fajr'},{key:'sunrise'},{key:'dhuhr'},
+ {key:'asr'},{key:'maghrib'},{key:'isha'}
+ ];
+ var now2 = Date.now();
+ prayers2.forEach(function(p){
+ var timeStr = times[p.key];
+ if (!timeStr || timeStr === '—') return;
+ var m = timeStr.match(/(\d+):(\d+)\s*(AM|PM)?/i);
+ if (!m) return;
+ var h = parseInt(m[1]), mn = parseInt(m[2]);
+ if (m[3]) {
+ if (m[3].toUpperCase()==='PM' && h<12) h+=12;
+ if (m[3].toUpperCase()==='AM' && h===12) h=0;
+ }
+ var today2 = new Date();
+ var pTime2 = new Date(today2.getFullYear(), today2.getMonth(), today2.getDate(), h, mn, 0);
+ var alertTime2 = pTime2.getTime() - 5*60*1000;
+ var delay2 = alertTime2 - now2;
+ if (delay2 < 0) return;
+ setTimeout(function(){
+ if (!_nabdhSettings.prayerAlarm) return;
+ playNotifSound('prayer');
+ }, delay2 + 100); // 100ms بعد الإشعار
+ });
+ };
+ }, 800);
+})();
+
+/* ── إضافة زر تفعيل الصوت في الإعدادات ── */
+(function _addSoundToggleToSettings(){
+ window.addEventListener('DOMContentLoaded', function(){
+ // نضيف إعداد الصوت للـ _nabdhSettings إذا لم يكن موجوداً
+ if (_nabdhSettings.soundEnabled === undefined) {
+ _nabdhSettings.soundEnabled = true;
+ _saveSettings();
+ }
+ // نُعيّن حالة toggle الصوت من HTML إذا كان موجوداً
+ var st = document.getElementById('soundToggle');
+ if (st) st.checked = _nabdhSettings.soundEnabled !== false;
+ });
+})();
+
+/** تفعيل / إيقاف الصوت من إعدادات */
+function toggleSound(on) {
+ _nabdhSettings.soundEnabled = on;
+ _saveSettings();
+ // في حالة تفعيل — شغّل نغمة اختبار
+ if (on) {
+ // unlock AudioContext بلمسة المستخدم
+ setTimeout(function(){ playNotifSound('nearby'); }, 100);
+ showToast('🔊 الصوت مفعّل', 'success');
+ } else {
+ showToast('🔇 الصوت مُعطَّل', 'info');
+ }
+}
+
+/** اختبار الصوت من واجهة الإعدادات */
+function testNotifSound() {
+ var saved = _nabdhSettings.quietMode;
+ _nabdhSettings.quietMode = false; // override للاختبار
+ playNotifSound('nearby');
+ setTimeout(function(){ _nabdhSettings.quietMode = saved; }, 300);
+ showToast('🔊 تشغيل نغمة تجريبية...', 'info');
+}
+
+/* ── تنبيه صوتي للتنبيهات القريبة عند تحميل الصفحة ── */
+/* عند أول تحميل للتنبيهات — لا نُصدر صوت (لتفادي الإزعاج) */
+/* الصوت فقط عند الأحداث الجديدة الفعلية في الوقت الحي */
+
+/* ── إشعار مرئي + صوتي للإضافات الجديدة من المنطقة ── */
+/**
+ * nearbyNewAlert: يُستدعى عند وصول حدث جديد من منطقة المستخدم
+ * يُعرض إشعاراً خاصاً بالجزء العلوي مع صوت
+ */
+function nearbyNewAlert(alertObj) {
+ if (_nabdhSettings.notif_nearby === false) return;
+ // تشغيل الصوت
+ var sndType = 'nearby';
+ if (alertObj && alertObj.type === 'sos') sndType = 'sos';
+ else if (alertObj && alertObj.type === 'blood') sndType = 'blood';
+ playNotifSound(sndType);
+ // إشعار مرئي منبثق (banner) خاص بالمنطقة
+ var banner = document.createElement('div');
+ banner.className = 'nearby-banner';
+ var icon = (alertObj && alertObj.icon) || '📡';
+ var msg = (alertObj && alertObj.msg) ? alertObj.msg.substring(0, 60) : 'حدث جديد';
+ var area = (alertObj && alertObj.area) ? alertObj.area : 'منطقتك';
+ banner.innerHTML =
+ '' +
+ '
' + icon + ' ' +
+ '
' +
+ '📡 إضافة جديدة من ' + area + ' ' +
+ '' + msg + ' ' +
+ '
' +
+ '
✕ ' +
+ '
';
+ document.body.appendChild(banner);
+ setTimeout(function(){ if (banner.parentNode) banner.classList.add('nb-fadeout'); }, 6000);
+ setTimeout(function(){ if (banner.parentNode) banner.remove(); }, 7000);
+}
+
+/* نُدمج nearbyNewAlert مع onNewAlert */
+var _origOnNewAlert2 = onNewAlert;
+onNewAlert = function(alert) {
+ _origOnNewAlert2(alert);
+ // تحقق من القرب الجغرافي (50 كم)
+ if (typeof userLat !== 'undefined' && userLat && alert && alert.lat && alert.lng) {
+ var R = 6371;
+ var dLat = (alert.lat - userLat) * Math.PI / 180;
+ var dLng = (alert.lng - (typeof userLng !== 'undefined' ? userLng : 0)) * Math.PI / 180;
+ var a = Math.sin(dLat/2)*Math.sin(dLat/2) +
+ Math.cos(userLat*Math.PI/180)*Math.cos(alert.lat*Math.PI/180)*
+ Math.sin(dLng/2)*Math.sin(dLng/2);
+ var distance = R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
+ if (distance <= 50) {
+ nearbyNewAlert(alert);
+ return; // الصوت شُغِّل من nearbyNewAlert
+ }
+ }
+ // بعيد — صوت خفيف فقط
+ if (_nabdhSettings.notif_nearby !== false) playNotifSound('alert');
+};
+
+
+/* ============================================================
+ 🏘️ HOOD GROUPS - مجموعات الأحياء v1.0
+ ============================================================ */
+
+var _hoodGroups = [];
+var _hoodFilter = 'all';
+var _hoodSearch = '';
+var _hoodCityFilter = '';
+var _currentHoodGroup = null;
+var _myHoodGroups = JSON.parse(localStorage.getItem('_myHoodGroups') || '[]');
+
+/* ── تحميل المجموعات ──────────────────────────────────── */
+async function loadHoodGroups() {
+ const el = document.getElementById('hoodGroupsList');
+ if (el) el.innerHTML = '';
+ try {
+ const res = await fetch('/api/hood');
+ if (!res.ok) throw new Error('HTTP ' + res.status);
+ _hoodGroups = await res.json();
+ renderHoodGroups();
+ } catch(e) {
+ if (el) el.innerHTML = '🏘️
أنشئ أول مجموعة حي!
لا توجد مجموعات بعد في هذه المنطقة. اضغط على الزر أدناه لتبدأ.
';
+ }
+}
+
+function renderHoodGroups() {
+ const el = document.getElementById('hoodGroupsList');
+ if (!el) return;
+ let list = _hoodGroups.slice();
+ if (_hoodFilter !== 'all') list = list.filter(g => g.type === _hoodFilter);
+ if (_hoodCityFilter) {
+ const cf = _hoodCityFilter.toLowerCase();
+ list = list.filter(g => (g.area||'').toLowerCase().includes(cf));
+ }
+ if (_hoodSearch) {
+ const q = _hoodSearch.toLowerCase();
+ list = list.filter(g =>
+ (g.name||'').toLowerCase().includes(q) ||
+ (g.area||'').toLowerCase().includes(q) ||
+ (g.desc||'').toLowerCase().includes(q)
+ );
+ }
+ // populate city filter dropdown
+ _populateHoodCityFilter();
+ if (!list.length) {
+ const emptyMsg = _hoodSearch || _hoodFilter !== 'all' || _hoodCityFilter
+ ? '🔍
لا نتائج
جرّب تغيير الفلتر أو البحث بكلمة مختلفة
'
+ : '🏘️
أنشئ أول مجموعة حي!
لا توجد مجموعات بعد. اضغط على الزر أعلاه لتبدأ مجموعة في حيّك.
';
+ el.innerHTML = emptyMsg;
+ return;
+ }
+ el.innerHTML = list.map(g => hoodGroupCard(g)).join('');
+}
+
+function _populateHoodCityFilter() {
+ const sel = document.getElementById('hoodCityFilter');
+ if (!sel || sel.dataset.populated) return;
+ const areas = [...new Set(_hoodGroups.map(g => (g.area||'').split(/[،,]/)[0].trim()).filter(Boolean))].sort();
+ if (!areas.length) return;
+ sel.innerHTML = 'جميع المدن والأحياء ' +
+ areas.map(a => `${a} `).join('');
+ sel.dataset.populated = '1';
+}
+
+function filterHoodByCity(val) {
+ _hoodCityFilter = val || '';
+ const sel = document.getElementById('hoodCityFilter');
+ if (sel) delete sel.dataset.populated;
+ renderHoodGroups();
+}
+
+function hoodGroupCard(g) {
+ const TYPE_ICO = { نظافة:'🧹', اجتماعات:'📅', مبادرة:'💡', ترشيح:'⭐', عام:'💬' };
+ const ico = TYPE_ICO[g.type] || '🏘️';
+ const joined = _myHoodGroups.includes(g.id);
+ const membersCount = (g.members||[]).length;
+ const postsCount = (g.posts||[]).length;
+ const nomsCount = (g.nominations||[]).length;
+ // last activity from posts
+ const lastPost = (g.posts||[]).length ? (g.posts[g.posts.length-1].ts) : null;
+ const lastActivity = lastPost || g.createdAt;
+ // upcoming meetings
+ const upcoming = (g.posts||[]).filter(p => p.postType==='meeting' && p.meetingDate && new Date(p.meetingDate+' '+(p.meetingTime||'00:00')) > new Date());
+ const hasMeeting = upcoming.length > 0;
+ return `
+
+
${ico}
+
+
${g.name}
+
📍 ${g.area}
+
+ ${g.type}
+ ${hasMeeting ? '📅 اجتماع قادم ' : ''}
+ ${joined ? '✓ منضم ' : ''}
+
+
+
+ ${joined?'✓ منضم':'انضم'}
+
+
+ ${g.desc ? `
${g.desc}
` : ''}
+
+
`;
+}
+
+function searchHoodGroups() {
+ _hoodSearch = (document.getElementById('hoodSearchInp')||{}).value || '';
+ renderHoodGroups();
+}
+
+function filterHoodByType(type, btn) {
+ _hoodFilter = type;
+ document.querySelectorAll('.hood-type-filt').forEach(b => b.classList.remove('active'));
+ if (btn) btn.classList.add('active');
+ renderHoodGroups();
+}
+
+/* ── Quick join from card ─────────────────────────────── */
+async function quickJoinHood(id, btn) {
+ const uid = localStorage.getItem('_nabdh_uid') || _getOrCreateUID();
+ try {
+ await fetch('/api/hood/'+id+'/join', {
+ method:'POST', headers:{'Content-Type':'application/json'},
+ body: JSON.stringify({ userId: uid })
+ });
+ if (!_myHoodGroups.includes(id)) _myHoodGroups.push(id);
+ localStorage.setItem('_myHoodGroups', JSON.stringify(_myHoodGroups));
+ if (btn) { btn.textContent='✓ منضم'; btn.classList.add('joined'); }
+ showToast('✅ انضممت للمجموعة!', 'success');
+ // update local count
+ const g = _hoodGroups.find(x=>x.id===id);
+ if (g && !g.members.includes(uid)) g.members.push(uid);
+ playNotifSound && playNotifSound('achiev');
+ } catch(e) { showToast('تعذّر الانضمام', 'error'); }
+}
+
+/* ── Create group modal ───────────────────────────────── */
+function openCreateHoodGroup() {
+ document.getElementById('createHoodModal').classList.remove('hidden');
+}
+function closeCreateHoodGroup() {
+ document.getElementById('createHoodModal').classList.add('hidden');
+ ['hoodNewName','hoodNewArea','hoodNewDesc','hoodNewContact'].forEach(id => {
+ const el = document.getElementById(id); if(el) el.value='';
+ });
+}
+async function submitCreateHoodGroup() {
+ const name = (document.getElementById('hoodNewName')||{}).value?.trim();
+ const area = (document.getElementById('hoodNewArea')||{}).value?.trim();
+ const type = (document.getElementById('hoodNewType')||{}).value;
+ const desc = (document.getElementById('hoodNewDesc')||{}).value?.trim();
+ const contact = (document.getElementById('hoodNewContact')||{}).value?.trim();
+ const maxMembers = parseInt((document.getElementById('hoodNewMaxMembers')||{}).value)||0;
+ if (!name||!area) { showToast('اسم المجموعة والحي مطلوبان ❗', 'error'); return; }
+ const uid = localStorage.getItem('_nabdh_uid') || _getOrCreateUID();
+ const author = localStorage.getItem('_nabdh_name') || 'مجهول';
+ const submitBtn = document.querySelector('#createHoodModal .btn-submit');
+ if (submitBtn) { submitBtn.disabled=true; submitBtn.textContent='جارٍ الإنشاء...'; }
+ try {
+ const res = await fetch('/api/hood', {
+ method:'POST', headers:{'Content-Type':'application/json'},
+ body: JSON.stringify({ name, area, type, desc, contact, maxMembers, userId:uid, author })
+ });
+ const data = await res.json();
+ if (!data.success) throw new Error(data.error);
+ _hoodGroups.unshift(data.group);
+ if (!_myHoodGroups.includes(data.group.id)) _myHoodGroups.push(data.group.id);
+ localStorage.setItem('_myHoodGroups', JSON.stringify(_myHoodGroups));
+ // reset city filter cache
+ const sel=document.getElementById('hoodCityFilter'); if(sel) delete sel.dataset.populated;
+ closeCreateHoodGroup();
+ renderHoodGroups();
+ showToast('🏘️ تم إنشاء المجموعة بنجاح!', 'success');
+ playNotifSound && playNotifSound('achiev');
+ openHoodGroupPage(data.group.id);
+ } catch(e) { showToast('فشل الإنشاء: '+(e.message||''), 'error'); }
+ finally { if(submitBtn){ submitBtn.disabled=false; submitBtn.textContent='✅ إنشاء المجموعة'; } }
+}
+
+/* ── Hood Group Page ──────────────────────────────────── */
+function openHoodGroupPage(id) {
+ const g = _hoodGroups.find(x=>x.id===id);
+ if (!g) { loadHoodGroups().then(()=>openHoodGroupPage(id)); return; }
+ _currentHoodGroup = g;
+ const TYPE_ICO = { نظافة:'🧹', اجتماعات:'📅', مبادرة:'💡', ترشيح:'⭐', عام:'💬' };
+ document.getElementById('hgpAvatar').textContent = TYPE_ICO[g.type]||'🏘️';
+ document.getElementById('hgpName').textContent = g.name;
+ document.getElementById('hgpMeta').textContent = '📍 '+g.area+' · '+g.type;
+ // stats bar
+ const members = g.members||[];
+ const posts = g.posts||[];
+ const noms = g.nominations||[];
+ const smEl = document.getElementById('hgpStatMembers'); if(smEl) smEl.textContent='👥 '+members.length+' عضو';
+ const spEl = document.getElementById('hgpStatPosts'); if(spEl) spEl.textContent='💬 '+posts.length+' منشور';
+ const snEl = document.getElementById('hgpStatNoms'); if(snEl) snEl.textContent='⭐ '+noms.length+' ترشيح';
+ const cEl = document.getElementById('hgpContact');
+ if(cEl) { if(g.contact){ cEl.textContent='📞 '+g.contact; cEl.classList.remove('hidden'); } else cEl.classList.add('hidden'); }
+ // join button
+ const uid = localStorage.getItem('_nabdh_uid') || _getOrCreateUID();
+ const joined = _myHoodGroups.includes(id) || members.includes(uid);
+ const joinBtn = document.getElementById('hgpJoinBtn');
+ if (joinBtn) { joinBtn.textContent = joined?'✓ منضم':'انضم'; joinBtn.classList.toggle('joined',joined); }
+ // meeting fields toggle on type change
+ const typeEl = document.getElementById('hoodPostType');
+ if (typeEl) typeEl.onchange = function(){ _toggleHoodMeetingFields(this.value); };
+ // reset compose
+ const ptEl = document.getElementById('hoodPostText'); if(ptEl) ptEl.value='';
+ if(typeEl) { typeEl.value='message'; _toggleHoodMeetingFields('message'); }
+ switchHoodTab('posts', document.querySelector('.hood-tab'));
+ renderHoodPosts(g);
+ renderHoodMeetings(g);
+ renderHoodNominations(g);
+ renderHoodMembers(g);
+ document.getElementById('hoodGroupPage').classList.remove('hidden');
+ document.getElementById('hoodGroupPage').scrollTop = 0;
+}
+
+function _toggleHoodMeetingFields(type) {
+ const f = document.getElementById('hoodMeetingFields');
+ if (!f) return;
+ if (type === 'meeting') { f.classList.remove('hidden'); f.style.display='flex'; }
+ else { f.classList.add('hidden'); f.style.display='none'; }
+}
+
+function closeHoodGroupPage() {
+ document.getElementById('hoodGroupPage').classList.add('hidden');
+ _currentHoodGroup = null;
+}
+
+function switchHoodTab(tab, btn) {
+ document.querySelectorAll('.hood-tab').forEach(b=>b.classList.remove('active'));
+ document.querySelectorAll('.hood-tab-content').forEach(c=>c.classList.add('hidden'));
+ if (btn) btn.classList.add('active');
+ else {
+ document.querySelectorAll('.hood-tab').forEach((b,i)=>{ if(i===0) b.classList.add('active'); });
+ }
+ const map = { posts:'hoodTabPosts', meetings:'hoodTabMeetings', nominations:'hoodTabNominations', members:'hoodTabMembers' };
+ const el = document.getElementById(map[tab]);
+ if (el) el.classList.remove('hidden');
+}
+
+/* ── Posts ────────────────────────────────────────────── */
+function renderHoodPosts(g) {
+ const el = document.getElementById('hoodPostsList');
+ if (!el) return;
+ const posts = (g.posts||[]).slice().reverse();
+ if (!posts.length) {
+ el.innerHTML = '💬
لا توجد منشورات بعد
كن أول من يكتب!
';
+ return;
+ }
+ const PT_ICO = { message:'💬', meeting:'📅', initiative:'💡', nomination:'⭐' };
+ const PT_COLOR = { message:'var(--accent)', meeting:'#3498db', initiative:'#f1c40f', nomination:'#e67e22' };
+ const uid = localStorage.getItem('_nabdh_uid') || '';
+ el.innerHTML = posts.map(p => {
+ const liked = (p.likes||[]).includes(uid);
+ const meetInfo = (p.postType==='meeting' && p.meetingDate) ? `
+
+ 📅 ${p.meetingDate}${p.meetingTime?' — ⏰ '+p.meetingTime:''}
+ ${p.meetingPlace?`📍 ${p.meetingPlace} `:''}
+ + التقويم
+
` : '';
+ return `
+
+ ${PT_ICO[p.postType]||'💬'}
+ ${p.author}
+ ${_timeAgo(p.ts)}
+
+
${p.text}
+ ${meetInfo}
+
+ ${liked?'❤️':'🤍'} ${(p.likes||[]).length}
+
+
`;
+ }).join('');
+}
+
+/* ── Meetings ─────────────────────────────────────────── */
+function renderHoodMeetings(g) {
+ const el = document.getElementById('hoodMeetingsList');
+ if (!el) return;
+ const now = new Date();
+ const meetings = (g.posts||[])
+ .filter(p => p.postType==='meeting' && p.meetingDate)
+ .sort((a,b) => new Date(a.meetingDate+' '+(a.meetingTime||'00:00')) - new Date(b.meetingDate+' '+(b.meetingTime||'00:00')));
+ if (!meetings.length) {
+ el.innerHTML = '📅
لا توجد اجتماعات مجدولة
أضف اجتماعاً من تبويب المنشورات
';
+ return;
+ }
+ el.innerHTML = meetings.map(p => {
+ const dt = new Date(p.meetingDate+' '+(p.meetingTime||'00:00'));
+ const isPast = dt < now;
+ return `
+
+
${dt.getDate()}
+
${dt.toLocaleDateString('ar-SA',{month:'short'})}
+
+
+
${p.text}
+ ${p.meetingTime?`
⏰ ${p.meetingTime}
`:''}
+ ${p.meetingPlace?`
📍 ${p.meetingPlace}
`:''}
+
${p.author} · ${_timeAgo(p.ts)}
+
+
+ ${!isPast?`📆 `:''}
+ ${isPast?'انتهى':'قادم'}
+
+
`;
+ }).join('');
+}
+
+function addHoodMeetingToCalendar(encoded) {
+ try {
+ const d = JSON.parse(decodeURIComponent(encoded));
+ const dtStr = d.date.replace(/-/g,'') + (d.time ? 'T'+d.time.replace(':','')+'00' : '');
+ const url = 'https://calendar.google.com/calendar/render?action=TEMPLATE'
+ + '&text=' + encodeURIComponent(d.title)
+ + '&dates=' + dtStr + '/' + dtStr
+ + (d.place ? '&location=' + encodeURIComponent(d.place) : '');
+ window.open(url, '_blank');
+ } catch(e) {}
+}
+
+async function submitHoodPost() {
+ const g = _currentHoodGroup;
+ if (!g) return;
+ const textEl = document.getElementById('hoodPostText');
+ const text = (textEl||{}).value?.trim();
+ if (!text) { showToast('اكتب شيئاً أولاً ❗','error'); return; }
+ const uid = localStorage.getItem('_nabdh_uid') || _getOrCreateUID();
+ const author = localStorage.getItem('_nabdh_name') || 'مجهول';
+ const postType = (document.getElementById('hoodPostType')||{}).value || 'message';
+ // gather meeting fields
+ const meetingDate = postType==='meeting' ? (document.getElementById('hoodMeetingDate')||{}).value||'' : '';
+ const meetingTime = postType==='meeting' ? (document.getElementById('hoodMeetingTime')||{}).value||'' : '';
+ const meetingPlace = postType==='meeting' ? (document.getElementById('hoodMeetingPlace')||{}).value?.trim()||'' : '';
+ const btn = document.querySelector('.hgp-send-btn');
+ if (btn) { btn.disabled=true; btn.textContent='...'; }
+ try {
+ const res = await fetch('/api/hood/'+g.id+'/post', {
+ method:'POST', headers:{'Content-Type':'application/json'},
+ body: JSON.stringify({ text, postType, meetingDate, meetingTime, meetingPlace, userId:uid, author })
+ });
+ const d = await res.json();
+ if (!d.success) throw new Error(d.error);
+ if (!Array.isArray(g.posts)) g.posts = [];
+ g.posts.push(d.post);
+ textEl.value = '';
+ // clear meeting fields
+ ['hoodMeetingDate','hoodMeetingTime','hoodMeetingPlace'].forEach(id => { const e=document.getElementById(id); if(e) e.value=''; });
+ renderHoodPosts(g);
+ renderHoodMeetings(g);
+ // update stats
+ const spEl = document.getElementById('hgpStatPosts');
+ if(spEl) spEl.textContent='💬 '+g.posts.length+' منشور';
+ showToast('✅ نُشر منشورك!','success');
+ playNotifSound && playNotifSound('msg');
+ } catch(e) { showToast('فشل النشر: '+(e.message||''),'error'); }
+ finally { if(btn){ btn.disabled=false; btn.textContent='↑'; } }
+}
+
+async function likeHoodPost(groupId, postId, btn) {
+ const uid = localStorage.getItem('_nabdh_uid') || _getOrCreateUID();
+ try {
+ const res = await fetch('/api/hood/'+groupId+'/post/'+postId+'/like', {
+ method:'POST', headers:{'Content-Type':'application/json'},
+ body: JSON.stringify({ userId:uid })
+ });
+ const d = await res.json();
+ if (btn) btn.textContent = '❤️ '+d.likes;
+ const g = _hoodGroups.find(x=>x.id===groupId);
+ if (g) {
+ const p = (g.posts||[]).find(x=>x.id===postId);
+ if (p) {
+ if (!Array.isArray(p.likes)) p.likes=[];
+ const idx = p.likes.indexOf(uid);
+ if (idx===-1) p.likes.push(uid); else p.likes.splice(idx,1);
+ }
+ }
+ } catch(e) {}
+}
+
+/* ── Join/Leave from group page ──────────────────────── */
+async function toggleJoinHoodGroup() {
+ const g = _currentHoodGroup;
+ if (!g) return;
+ const uid = localStorage.getItem('_nabdh_uid') || _getOrCreateUID();
+ const joined = _myHoodGroups.includes(g.id);
+ const endpoint = joined ? 'leave' : 'join';
+ const btn = document.getElementById('hgpJoinBtn');
+ if (btn) { btn.disabled=true; }
+ try {
+ await fetch('/api/hood/'+g.id+'/'+endpoint, {
+ method:'POST', headers:{'Content-Type':'application/json'},
+ body: JSON.stringify({ userId:uid })
+ });
+ if (joined) {
+ _myHoodGroups = _myHoodGroups.filter(x=>x!==g.id);
+ g.members = (g.members||[]).filter(m=>m!==uid);
+ } else {
+ _myHoodGroups.push(g.id);
+ if (!g.members) g.members=[];
+ if (!g.members.includes(uid)) g.members.push(uid);
+ }
+ localStorage.setItem('_myHoodGroups', JSON.stringify(_myHoodGroups));
+ const newJoined = !joined;
+ if (btn) { btn.textContent=newJoined?'✓ منضم':'انضم'; btn.classList.toggle('joined',newJoined); btn.disabled=false; }
+ // update stats bar
+ const smEl = document.getElementById('hgpStatMembers');
+ if(smEl) smEl.textContent='👥 '+g.members.length+' عضو';
+ showToast(newJoined?'✅ انضممت للمجموعة!':'👋 تم مغادرة المجموعة', newJoined?'success':'info');
+ if(newJoined) playNotifSound && playNotifSound('achiev');
+ renderHoodMembers(g);
+ renderHoodGroups(); // refresh card badge
+ } catch(e) { showToast('تعذّرت العملية','error'); if(btn) btn.disabled=false; }
+}
+
+/* ── Share group link ─────────────────────────────────── */
+function shareHoodGroup() {
+ const g = _currentHoodGroup;
+ if (!g) return;
+ const url = window.location.origin + '/#hood';
+ const text = `🏘️ مجموعة حي "${g.name}" (${g.area})\nانضم معنا في نبض السودان:\n${url}`;
+ if (navigator.share) {
+ navigator.share({ title: g.name, text, url }).catch(()=>{});
+ } else {
+ // fallback: copy to clipboard
+ navigator.clipboard.writeText(text).then(() => showToast('✅ تم نسخ رابط المجموعة!','success')).catch(()=>{
+ showToast('🔗 '+url, 'info');
+ });
+ }
+}
+
+/* ── Nominations ──────────────────────────────────────── */
+function renderHoodNominations(g) {
+ const el = document.getElementById('hoodNomList');
+ if (!el) return;
+ const uid = localStorage.getItem('_nabdh_uid') || '';
+ const noms = (g.nominations||[]).slice().sort((a,b)=>(b.votes||[]).length-(a.votes||[]).length);
+ if (!noms.length) {
+ el.innerHTML = '⭐
لا توجد ترشيحات بعد
رشّح محلاً أو خدمة في حيّك
';
+ return;
+ }
+ el.innerHTML = noms.map((n,i) => {
+ const voted = (n.votes||[]).includes(uid);
+ const rank = i < 3 ? ['🥇','🥈','🥉'][i] : '';
+ return `
+
+ ${rank?`
${rank} `:''}
+
⭐ ${n.title}
+ ${n.category?`
${n.category} `:''}
+
+ ${n.desc?`
${n.desc}
`:''}
+
+
`;
+ }).join('');
+}
+
+function openNominationForm() {
+ const f = document.getElementById('hoodNomForm');
+ if (f) f.classList.toggle('hidden');
+}
+function closeNominationForm() {
+ const f = document.getElementById('hoodNomForm');
+ if (f) f.classList.add('hidden');
+}
+async function submitNomination() {
+ const g = _currentHoodGroup;
+ if (!g) return;
+ const title = (document.getElementById('nomTitle')||{}).value?.trim();
+ const category = (document.getElementById('nomCategory')||{}).value?.trim();
+ const desc = (document.getElementById('nomDesc')||{}).value?.trim();
+ if (!title) { showToast('اسم الترشيح مطلوب ❗','error'); return; }
+ const uid = localStorage.getItem('_nabdh_uid') || _getOrCreateUID();
+ const author = localStorage.getItem('_nabdh_name') || 'مجهول';
+ try {
+ const res = await fetch('/api/hood/'+g.id+'/nominate', {
+ method:'POST', headers:{'Content-Type':'application/json'},
+ body: JSON.stringify({ title, category, desc, userId:uid, author })
+ });
+ const d = await res.json();
+ if (!d.success) throw new Error(d.error);
+ if (!Array.isArray(g.nominations)) g.nominations=[];
+ g.nominations.push(d.nomination);
+ ['nomTitle','nomCategory','nomDesc'].forEach(id => { const e=document.getElementById(id); if(e) e.value=''; });
+ closeNominationForm();
+ renderHoodNominations(g);
+ // update stats
+ const snEl = document.getElementById('hgpStatNoms');
+ if(snEl) snEl.textContent='⭐ '+g.nominations.length+' ترشيح';
+ showToast('⭐ تم إرسال الترشيح!','success');
+ } catch(e) { showToast('فشل إرسال الترشيح: '+(e.message||''),'error'); }
+}
+
+async function voteNomination(groupId, nomId, btn) {
+ const uid = localStorage.getItem('_nabdh_uid') || _getOrCreateUID();
+ try {
+ const res = await fetch('/api/hood/'+groupId+'/nominate/'+nomId+'/vote', {
+ method:'POST', headers:{'Content-Type':'application/json'},
+ body: JSON.stringify({ userId:uid })
+ });
+ const d = await res.json();
+ if (btn) btn.textContent = '👍 '+d.votes+' تأييد';
+ const g = _hoodGroups.find(x=>x.id===groupId);
+ if (g) {
+ const n = (g.nominations||[]).find(x=>x.id===nomId);
+ if (n) {
+ if (!Array.isArray(n.votes)) n.votes=[];
+ const idx = n.votes.indexOf(uid);
+ if (idx===-1) n.votes.push(uid); else n.votes.splice(idx,1);
+ }
+ }
+ } catch(e) {}
+}
+
+/* ── Members list ────────────────────────────────────── */
+function renderHoodMembers(g) {
+ const el = document.getElementById('hoodMembersList');
+ if (!el) return;
+ const members = g.members||[];
+ if (!members.length) {
+ el.innerHTML = '👥
لا يوجد أعضاء بعد
كن الأول وانضم للمجموعة!
';
+ return;
+ }
+ // try to get profile names from cache
+ const profiles = window._allProfiles || [];
+ el.innerHTML = ``
+ + members.map((uid, i) => {
+ const profile = profiles.find(p => p.userId===uid || p.id===uid);
+ const name = profile ? (profile.name || profile.displayName || 'مستخدم') : (uid===g.userId ? g.author||'المنشئ' : 'عضو '+(i+1));
+ const ava = profile ? (profile.avatar || '👤') : (uid===g.userId ? '👑' : '👤');
+ const isOwner = uid === g.userId;
+ return `
+
${ava}
+
${name}
+ ${isOwner ? '
👑 منشئ ' : ''}
+
`;
+ }).join('');
+}
+
+/* ── Real-time socket events ─────────────────────────── */
+if (typeof socket !== 'undefined' && socket) {
+ socket.on('new_hood_group', function(group) {
+ if (!_hoodGroups.find(g=>g.id===group.id)) {
+ _hoodGroups.unshift(group);
+ const sel=document.getElementById('hoodCityFilter'); if(sel) delete sel.dataset.populated;
+ renderHoodGroups();
+ }
+ playNotifSound && playNotifSound('hood');
+ showToast('🏘️ مجموعة حي جديدة: '+group.name+' — '+group.area, 'info');
+ });
+ socket.on('hood_post', function(ev) {
+ const g = _hoodGroups.find(x=>x.id===ev.groupId);
+ if (g) {
+ // sound only for joined groups
+ if (_myHoodGroups.includes(ev.groupId)) {
+ playNotifSound && playNotifSound('msg');
+ showToast('💬 منشور جديد في «'+g.name+'»: '+((ev.post||{}).text||'').slice(0,40), 'info');
+ }
+ if (!Array.isArray(g.posts)) g.posts=[];
+ if (!g.posts.find(p=>p.id===ev.post.id)) g.posts.push(ev.post);
+ if (_currentHoodGroup && _currentHoodGroup.id===ev.groupId) renderHoodPosts(g);
+ }
+ });
+ socket.on('hood_join', function(ev) {
+ const g = _hoodGroups.find(x=>x.id===ev.id);
+ if (g) g.members = ev.members;
+ if (_currentHoodGroup && _currentHoodGroup.id===ev.id) renderHoodMembers(g);
+ });
+ socket.on('hood_nomination', function(ev) {
+ const g = _hoodGroups.find(x=>x.id===ev.groupId);
+ if (g) {
+ if (!Array.isArray(g.nominations)) g.nominations=[];
+ if (!g.nominations.find(n=>n.id===ev.nomination.id)) g.nominations.push(ev.nomination);
+ if (_currentHoodGroup && _currentHoodGroup.id===ev.groupId) renderHoodNominations(g);
+ }
+ });
+}
+
+/* ── Sound: hood type ────────────────────────────────── */
+if (typeof _soundDefs !== 'undefined') {
+ _soundDefs.hood = { freqs:[440,550,660,550], durs:[0.1,0.1,0.15,0.1], wave:'sine' };
+}
+
+/* ── Helper: UID ──────────────────────────────────────── */
+function _getOrCreateUID() {
+ let uid = localStorage.getItem('_nabdh_uid');
+ if (!uid) { uid = 'u_'+Date.now()+'_'+Math.random().toString(36).slice(2,8); localStorage.setItem('_nabdh_uid', uid); }
+ return uid;
+}
+
+/* ── Helper: time ago (uses existing _timeAgo or fallback) */
+function _timeAgo(ts) {
+ if (!ts) return '';
+ try {
+ if (typeof timeAgo === 'function') return timeAgo(ts);
+ const diff = Date.now() - Number(ts);
+ if (diff < 60000) return 'الآن';
+ if (diff < 3600000) return Math.floor(diff/60000)+' د';
+ if (diff < 86400000) return Math.floor(diff/3600000)+' س';
+ return Math.floor(diff/86400000)+' يوم';
+ } catch(e) { return ''; }
+}
+
+/* ── Load on section change (already handled in main goSection) ── */
+/* removed duplicate hook */
+
diff --git a/public/manifest.json b/public/manifest.json
new file mode 100644
index 0000000..d321394
--- /dev/null
+++ b/public/manifest.json
@@ -0,0 +1,55 @@
+{
+ "name": "نبض — صوت المدينة الحي",
+ "short_name": "نبض",
+ "description": "تطبيق المدينة الحي — أخبار، أسعار، خرائط، رسائل مباشرة من مجتمعك",
+ "start_url": "/?pwa=1",
+ "scope": "/",
+ "display": "standalone",
+ "display_override": ["standalone", "minimal-ui", "browser"],
+ "background_color": "#0a0e1a",
+ "theme_color": "#1abc9c",
+ "orientation": "portrait-primary",
+ "lang": "ar",
+ "dir": "rtl",
+ "categories": ["news", "social", "utilities", "lifestyle"],
+ "screenshots": [],
+ "shortcuts": [
+ {
+ "name": "بلّغ الآن",
+ "short_name": "تبليغ",
+ "description": "أرسل تقريراً عاجلاً",
+ "url": "/?section=report",
+ "icons": [{ "src": "/favicon.svg", "sizes": "any" }]
+ },
+ {
+ "name": "الخريطة الحية",
+ "short_name": "الخريطة",
+ "description": "اطلع على خريطة الأحداث",
+ "url": "/?section=map",
+ "icons": [{ "src": "/favicon.svg", "sizes": "any" }]
+ },
+ {
+ "name": "صرّاف الشعب",
+ "short_name": "الصرف",
+ "description": "تحقق من أسعار الصرف",
+ "url": "/?section=exchange",
+ "icons": [{ "src": "/favicon.svg", "sizes": "any" }]
+ },
+ {
+ "name": "رسائلي",
+ "short_name": "رسائل",
+ "description": "افتح صندوق الرسائل",
+ "url": "/?section=messages",
+ "icons": [{ "src": "/favicon.svg", "sizes": "any" }]
+ }
+ ],
+ "icons": [
+ { "src": "/favicon.svg", "sizes": "any", "type": "image/svg+xml", "purpose": "any" },
+ { "src": "/favicon.svg", "sizes": "any", "type": "image/svg+xml", "purpose": "maskable" },
+ { "src": "/images/icon-192.png", "sizes": "192x192", "type": "image/png", "purpose": "any" },
+ { "src": "/images/icon-512.png", "sizes": "512x512", "type": "image/png", "purpose": "any" },
+ { "src": "/images/icon-512.png", "sizes": "512x512", "type": "image/png", "purpose": "maskable" }
+ ],
+ "related_applications": [],
+ "prefer_related_applications": false
+}
diff --git a/public/offline.html b/public/offline.html
new file mode 100644
index 0000000..2051d26
--- /dev/null
+++ b/public/offline.html
@@ -0,0 +1,113 @@
+
+
+
+
+
+ نبض — غير متصل
+
+
+
+ 💓
+ نبض — بدون إنترنت
+
+
+
+ أنت حالياً غير متصل بالإنترنت. بعض المحتوى المحفوظ قد يكون متاحاً.
+
+ 🔄 إعادة المحاولة
+ 🏠 الصفحة الرئيسية
+
+
+
💾 وضع عدم الاتصال
+
نبض يحفظ نسخة من آخر البيانات تلقائياً حتى تتمكن من الاطلاع عليها دون إنترنت
+
+
+
+
+
diff --git a/public/sw.js b/public/sw.js
new file mode 100644
index 0000000..0ac319b
--- /dev/null
+++ b/public/sw.js
@@ -0,0 +1,223 @@
+/* ================================================================
+ NABDH نبض — Service Worker v4.0 ULTRA
+ Strategies:
+ - Static assets → Cache First (7 days)
+ - API calls → Network First (30s cache fallback)
+ - HTML pages → Network First (fallback to offline page)
+ - Background sync for failed POST requests
+================================================================ */
+
+const CACHE_NAME = 'nabdh-v10';
+const CACHE_STATIC = 'nabdh-static-v10';
+const OFFLINE_URL = '/offline.html';
+
+// Files to pre-cache on install
+const PRECACHE = [
+ '/',
+ '/offline.html',
+ '/css/style.css',
+ '/js/app.js',
+ '/favicon.svg',
+ '/manifest.json'
+];
+
+// API routes that are safe to cache
+const CACHEABLE_API = [
+ '/api/stats',
+ '/api/alerts',
+ '/api/exchange',
+ '/api/medicines',
+ '/api/market',
+ '/api/geo/sudan',
+ '/api/hospitals',
+ '/api/prayer',
+ '/api/leaderboard'
+];
+
+// ── Install ──────────────────────────────────────────────────
+self.addEventListener('install', event => {
+ event.waitUntil(
+ caches.open(CACHE_STATIC)
+ .then(cache => cache.addAll(PRECACHE).catch(() => {}))
+ .then(() => self.skipWaiting())
+ );
+});
+
+// ── Activate ─────────────────────────────────────────────────
+self.addEventListener('activate', event => {
+ event.waitUntil(
+ caches.keys()
+ .then(keys => Promise.all(
+ keys.filter(k => k !== CACHE_NAME && k !== CACHE_STATIC)
+ .map(k => caches.delete(k))
+ ))
+ .then(() => self.clients.claim())
+ );
+});
+
+// ── Fetch ────────────────────────────────────────────────────
+self.addEventListener('fetch', event => {
+ const { request } = event;
+ const url = new URL(request.url);
+
+ // Skip non-GET and cross-origin (socket.io, CDN, etc.)
+ if (request.method !== 'GET') return;
+ if (url.origin !== self.location.origin) return;
+ if (url.pathname.startsWith('/socket.io')) return;
+
+ // API calls → Network First, fallback cache
+ if (url.pathname.startsWith('/api/')) {
+ const isCacheable = CACHEABLE_API.some(p => url.pathname.startsWith(p));
+ event.respondWith(networkFirstAPI(request, isCacheable));
+ return;
+ }
+
+ // Static assets → Cache First (long TTL)
+ if (url.pathname.match(/\.(css|js|svg|png|jpg|jpeg|gif|webp|ico|woff2?|ttf)$/)) {
+ event.respondWith(cacheFirst(request));
+ return;
+ }
+
+ // HTML / navigation → Network First, fallback offline
+ event.respondWith(networkFirstHTML(request));
+});
+
+// ── Cache First ───────────────────────────────────────────────
+async function cacheFirst(request) {
+ const cached = await caches.match(request);
+ if (cached) return cached;
+ try {
+ const response = await fetch(request);
+ if (response.ok) {
+ const cache = await caches.open(CACHE_STATIC);
+ cache.put(request, response.clone());
+ }
+ return response;
+ } catch {
+ return new Response('', { status: 408 });
+ }
+}
+
+// ── Network First HTML ────────────────────────────────────────
+async function networkFirstHTML(request) {
+ try {
+ const ctrl = new AbortController();
+ const tid = setTimeout(() => ctrl.abort(), 6000);
+ const response = await fetch(request, { signal: ctrl.signal });
+ clearTimeout(tid);
+ if (response.ok) {
+ const cache = await caches.open(CACHE_NAME);
+ cache.put(request, response.clone());
+ }
+ return response;
+ } catch {
+ const cached = await caches.match(request);
+ if (cached) return cached;
+ const offline = await caches.match(OFFLINE_URL);
+ return offline || new Response(
+ 'غير متصل 💓 نبض 📵 أنت غير متصل بالإنترنت تحقق من اتصالك وأعد المحاولة
🔄 إعادة المحاولة ',
+ { headers: { 'Content-Type': 'text/html; charset=utf-8' }, status: 200 }
+ );
+ }
+}
+
+// ── Network First API ─────────────────────────────────────────
+async function networkFirstAPI(request, cacheable) {
+ try {
+ const ctrl = new AbortController();
+ const tid = setTimeout(() => ctrl.abort(), 8000);
+ const response = await fetch(request, { signal: ctrl.signal });
+ clearTimeout(tid);
+ if (response.ok && cacheable) {
+ const cache = await caches.open(CACHE_NAME);
+ cache.put(request, response.clone());
+ }
+ return response;
+ } catch {
+ if (cacheable) {
+ const cached = await caches.match(request);
+ if (cached) return cached;
+ }
+ return new Response(
+ JSON.stringify({ error: 'offline', cached: false, ts: Date.now() }),
+ { headers: { 'Content-Type': 'application/json' }, status: 503 }
+ );
+ }
+}
+
+// ── Push Notifications ───────────────────────────────────────
+self.addEventListener('push', event => {
+ const data = event.data ? event.data.json() : {};
+ const title = data.title || '💓 نبض';
+ const options = {
+ body: data.body || 'تنبيه جديد في منطقتك',
+ icon: '/favicon.svg',
+ badge: '/images/icon-192.png',
+ tag: data.tag || 'nabdh-alert',
+ data: { url: data.url || '/' },
+ dir: 'rtl',
+ lang: 'ar',
+ vibrate: [200, 100, 200],
+ requireInteraction: data.urgent || false,
+ actions: [
+ { action: 'view', title: '👁️ عرض' },
+ { action: 'dismiss', title: '✕ إغلاق' }
+ ]
+ };
+ event.waitUntil(self.registration.showNotification(title, options));
+});
+
+self.addEventListener('notificationclick', event => {
+ event.notification.close();
+ if (event.action === 'dismiss') return;
+ const url = event.notification.data?.url || '/';
+ event.waitUntil(
+ clients.matchAll({ type: 'window', includeUncontrolled: true }).then(list => {
+ for (const client of list) {
+ if ('focus' in client) return client.focus();
+ }
+ if (clients.openWindow) return clients.openWindow(url);
+ })
+ );
+});
+
+// ── Background Sync (for failed reports) ─────────────────────
+self.addEventListener('sync', event => {
+ if (event.tag === 'sync-reports') {
+ event.waitUntil(syncPendingReports());
+ }
+});
+
+async function syncPendingReports() {
+ try {
+ const cache = await caches.open('nabdh-pending');
+ const requests = await cache.keys();
+ for (const req of requests) {
+ try {
+ const resp = await fetch(req.clone());
+ if (resp.ok) await cache.delete(req);
+ } catch {}
+ }
+ } catch {}
+}
+
+// ── Periodic Background Sync ──────────────────────────────────
+self.addEventListener('periodicsync', event => {
+ if (event.tag === 'refresh-stats') {
+ event.waitUntil(
+ fetch('/api/stats').then(r => {
+ if (r.ok) return caches.open(CACHE_NAME).then(c => c.put('/api/stats', r));
+ }).catch(() => {})
+ );
+ }
+});
+
+// ── Message handler (skip waiting) ───────────────────────────
+self.addEventListener('message', event => {
+ if (event.data === 'skipWaiting') self.skipWaiting();
+ if (event.data === 'clearCache') {
+ caches.delete(CACHE_NAME).then(() => self.clients.matchAll().then(clients => {
+ clients.forEach(c => c.postMessage({ type: 'cacheCleared' }));
+ }));
+ }
+});
diff --git a/pyproject.toml b/pyproject.toml
deleted file mode 100644
index 7fd26b9..0000000
--- a/pyproject.toml
+++ /dev/null
@@ -1,3 +0,0 @@
-[build-system]
-requires = ["setuptools"]
-build-backend = "setuptools.build_meta"
\ No newline at end of file
diff --git a/render.yaml b/render.yaml
new file mode 100644
index 0000000..b25d24a
--- /dev/null
+++ b/render.yaml
@@ -0,0 +1,14 @@
+services:
+ - type: web
+ name: nabdh
+ runtime: node
+ plan: free
+ region: oregon
+ buildCommand: npm install --production=false
+ startCommand: node server.js
+ envVars:
+ - key: NODE_ENV
+ value: production
+ - key: PORT
+ value: 3000
+ autoDeploy: true
diff --git a/server.js b/server.js
new file mode 100644
index 0000000..e864979
--- /dev/null
+++ b/server.js
@@ -0,0 +1,3192 @@
+const express = require('express');
+const http = require('http');
+const { Server } = require('socket.io');
+const cors = require('cors');
+const { v4: uuidv4 } = require('uuid');
+const path = require('path');
+const fs = require('fs');
+const compression = require('compression');
+
+const app = express();
+const server = http.createServer(app);
+const io = new Server(server, {
+ cors: { origin: '*' },
+ pingTimeout: 30000,
+ pingInterval: 10000,
+ maxHttpBufferSize: 10 * 1024 * 1024 // 10MB for media
+});
+
+/* ─── Performance: Gzip compression ─── */
+app.use(compression({ level: 6, threshold: 1024 }));
+
+/* ─── Security headers ─── */
+app.use((req, res, next) => {
+ res.set('X-Content-Type-Options', 'nosniff');
+ res.set('X-Frame-Options', 'SAMEORIGIN');
+ res.set('X-XSS-Protection', '1; mode=block');
+ res.set('Referrer-Policy', 'strict-origin-when-cross-origin');
+ next();
+});
+
+/* ─── Simple in-memory rate limiting ─── */
+const _rateLimits = {};
+function rateLimit(maxReqs, windowMs) {
+ return (req, res, next) => {
+ const key = req.ip || 'unknown';
+ const now = Date.now();
+ if (!_rateLimits[key]) _rateLimits[key] = { count: 0, reset: now + windowMs };
+ if (now > _rateLimits[key].reset) { _rateLimits[key] = { count: 0, reset: now + windowMs }; }
+ _rateLimits[key].count++;
+ if (_rateLimits[key].count > maxReqs) {
+ return res.status(429).json({ error: 'طلبات كثيرة، حاول بعد قليل' });
+ }
+ next();
+ };
+}
+// Clean rate limit map every 5 min
+setInterval(() => {
+ const now = Date.now();
+ Object.keys(_rateLimits).forEach(k => { if (_rateLimits[k].reset < now) delete _rateLimits[k]; });
+}, 5 * 60 * 1000);
+
+app.use(cors());
+app.use(express.json({ limit: '50mb' }));
+app.use(express.urlencoded({ extended: true, limit: '50mb' }));
+
+/* ─── Response time header ─── */
+app.use((req, res, next) => {
+ const start = Date.now();
+ res.on('finish', () => {
+ try {
+ if (!res.headersSent) res.set('X-Response-Time', (Date.now() - start) + 'ms');
+ } catch(e) { /* ignore */ }
+ });
+ next();
+});
+
+// Static files - but sw.js must not be cached
+app.use(express.static(path.join(__dirname, 'public'), {
+ maxAge: '7d',
+ etag: true,
+ lastModified: true,
+ setHeaders: (res, filePath) => {
+ if (filePath.endsWith('sw.js')) {
+ res.set('Cache-Control', 'no-cache, no-store, must-revalidate');
+ res.set('Service-Worker-Allowed', '/');
+ } else if (filePath.match(/\.(css|js)$/)) {
+ res.set('Cache-Control', 'public, max-age=86400, stale-while-revalidate=3600');
+ } else if (filePath.match(/\.(png|jpg|svg|ico|webp|woff2?)$/)) {
+ res.set('Cache-Control', 'public, max-age=604800');
+ }
+ }
+}));
+
+/* ============================================================
+ قاعدة بيانات المناطق الجغرافية الشاملة
+ ============================================================ */
+const GEO = {
+ sudan: [
+ { state:'الخرطوم', lat:15.5007, lng:32.5599, cities:[
+ { name:'الخرطوم', lat:15.5007, lng:32.5599, hoods:['الرياض','العمارات','الموردة','الديوم','حي العرب','السجانة','الطائف','المنطقة الصناعية','الخرطوم 2','بري','الكلاكلة','مايو','جبرة','المقرن','الصحافة','الهجرة','الثورة','النيل الأزرق','كلاكلة','عرب حرة','القرشي','حارة بيضاء'] },
+ { name:'أم درمان', lat:15.6447, lng:32.4776, hoods:['السوق الشعبي','الثورة','أبو روف','الملازمين','أبو سيد','المولية','الصحافة','ود نوباوي','الثورة الخضراء','البوستة','حي المهندسين','كرري الجنوبية','دار السلام','العليافة'] },
+ { name:'بحري', lat:15.6024, lng:32.5533, hoods:['كافوري','الحلفايا','شمبات','الجريف غرب','الحاج يوسف','الصافية','تمبول','شبشة','السجانة','الدرجة الثانية','بورتسودان'] },
+ { name:'كرري', lat:15.6950, lng:32.4600, hoods:['كرري','سوبا','ود البشير','الملازمين','اليرموك','كرري الشمالية'] },
+ { name:'جبل أولياء', lat:15.3500, lng:32.4800, hoods:['جبل أولياء','مناقل','الشهيد الزبير','الفتيحاب'] },
+ { name:'أم بدة', lat:15.6200, lng:32.4000, hoods:['أم بدة','السكة حديد','البقعة','السلمة'] },
+ { name:'الشجرة', lat:15.5500, lng:32.5800, hoods:['الشجرة','الصافية','المنطقة الصناعية'] },
+ ]},
+ { state:'الجزيرة', lat:14.4000, lng:33.5000, cities:[
+ { name:'مدني', lat:14.4000, lng:33.5000, hoods:['وسط مدني','الحي الجديد','الصناعية','المطار','حي العمال','حي المحكمة','حي الجراية'] },
+ { name:'رفاعة', lat:14.7100, lng:33.2700, hoods:['وسط رفاعة','الشرق','الغرب'] },
+ { name:'الحصاحيصا', lat:14.6500, lng:33.3200, hoods:[] },
+ { name:'كمبوني', lat:14.4500, lng:33.5500, hoods:[] },
+ { name:'الجعلية', lat:15.3500, lng:33.1000, hoods:[] },
+ { name:'الكاملين', lat:14.2600, lng:33.3000, hoods:[] },
+ ]},
+ { state:'نهر النيل', lat:17.5500, lng:33.9700, cities:[
+ { name:'عطبرة', lat:17.6900, lng:34.0000, hoods:['وسط عطبرة','الصناعية','الشرق','الغرب'] },
+ { name:'شندي', lat:16.6900, lng:33.4300, hoods:['وسط شندي','الحصباي','الكبوشية'] },
+ { name:'البربر', lat:18.0200, lng:33.9700, hoods:[] },
+ { name:'أبو حمد', lat:19.5200, lng:33.3300, hoods:[] },
+ { name:'الدامر', lat:17.5500, lng:33.9700, hoods:[] },
+ { name:'مروي', lat:18.4700, lng:31.8200, hoods:[] },
+ ]},
+ { state:'الشمالية', lat:19.6200, lng:30.4200, cities:[
+ { name:'دنقلا', lat:19.1700, lng:30.4800, hoods:['وسط دنقلا','الزياده','الشلاق'] },
+ { name:'كريمة', lat:18.5500, lng:31.8500, hoods:[] },
+ { name:'مروي', lat:18.4700, lng:31.8200, hoods:[] },
+ { name:'وادي حلفا', lat:21.7900, lng:31.3400, hoods:[] },
+ { name:'أبري', lat:20.7900, lng:30.3500, hoods:[] },
+ { name:'دلقو', lat:18.6500, lng:30.8000, hoods:[] },
+ ]},
+ { state:'كسلا', lat:15.4500, lng:36.4000, cities:[
+ { name:'كسلا', lat:15.4500, lng:36.4000, hoods:['وسط كسلا','الغرب','الشرق','ريفي'] },
+ { name:'حلفا الجديدة', lat:15.3000, lng:36.2000, hoods:[] },
+ { name:'تسني', lat:14.7500, lng:36.7500, hoods:[] },
+ { name:'بورتسودان', lat:19.6200, lng:37.2200, hoods:['شرق','غرب','الميناء','الضباب'] },
+ { name:'القضارف', lat:14.0300, lng:35.8900, hoods:[] },
+ ]},
+ { state:'القضارف', lat:14.0300, lng:35.8900, cities:[
+ { name:'القضارف', lat:14.0300, lng:35.8900, hoods:['وسط القضارف','الشمال','الجنوب','الحديد'] },
+ { name:'الفاو', lat:13.6400, lng:35.7700, hoods:[] },
+ { name:'دوكة', lat:14.5000, lng:35.9000, hoods:[] },
+ { name:'باسندة', lat:14.2000, lng:35.5000, hoods:[] },
+ ]},
+ { state:'سنار', lat:13.5500, lng:33.5700, cities:[
+ { name:'سنجة', lat:13.5500, lng:33.5700, hoods:[] },
+ { name:'الدندر', lat:12.8000, lng:34.0000, hoods:[] },
+ { name:'سنار', lat:13.5600, lng:33.5600, hoods:[] },
+ { name:'الرهد', lat:13.1000, lng:33.2000, hoods:[] },
+ ]},
+ { state:'النيل الأبيض', lat:13.1600, lng:32.6600, cities:[
+ { name:'كوستي', lat:13.1600, lng:32.6600, hoods:['وسط كوستي','الجنوب','الشمال','ريفي'] },
+ { name:'ربك', lat:12.4100, lng:31.8700, hoods:[] },
+ { name:'الكوة', lat:13.8200, lng:32.2600, hoods:[] },
+ { name:'الدويم', lat:14.0000, lng:32.5000, hoods:[] },
+ ]},
+ { state:'النيل الأزرق', lat:11.8700, lng:34.3800, cities:[
+ { name:'الدمازين', lat:11.7900, lng:34.3600, hoods:['وسط الدمازين','الصناعية'] },
+ { name:'الروصيرص', lat:11.8700, lng:34.3800, hoods:[] },
+ { name:'الكرمك', lat:11.5500, lng:33.8500, hoods:[] },
+ ]},
+ { state:'جنوب كردفان', lat:11.0000, lng:29.7000, cities:[
+ { name:'كادوقلي', lat:11.0100, lng:29.7100, hoods:[] },
+ { name:'الدلنج', lat:11.5500, lng:29.7000, hoods:[] },
+ { name:'أبو جبيهة', lat:11.5700, lng:31.2700, hoods:[] },
+ { name:'الرشاد', lat:11.8500, lng:30.6500, hoods:[] },
+ { name:'هيبان', lat:11.3500, lng:29.9500, hoods:[] },
+ ]},
+ { state:'شمال كردفان', lat:13.1800, lng:30.2200, cities:[
+ { name:'الأبيض', lat:13.1800, lng:30.2200, hoods:['وسط الأبيض','الحاج يوسف','الصناعية','الربيع','الفيحاء'] },
+ { name:'بارا', lat:13.7000, lng:30.3700, hoods:[] },
+ { name:'أم روابة', lat:12.9100, lng:31.2200, hoods:[] },
+ { name:'الخوي', lat:13.0500, lng:29.6500, hoods:[] },
+ { name:'السودري', lat:13.8500, lng:29.6000, hoods:[] },
+ ]},
+ { state:'غرب كردفان', lat:12.1900, lng:29.4100, cities:[
+ { name:'الفولة', lat:11.7200, lng:28.3500, hoods:[] },
+ { name:'أبو زبد', lat:12.1900, lng:29.4100, hoods:[] },
+ { name:'العتيبة', lat:12.7500, lng:29.1000, hoods:[] },
+ ]},
+ { state:'شمال دارفور', lat:13.8500, lng:24.8900, cities:[
+ { name:'الفاشر', lat:13.6300, lng:25.3400, hoods:['وسط الفاشر','الشرق','الغرب','الجنوب'] },
+ { name:'مليط', lat:15.0900, lng:25.8500, hoods:[] },
+ { name:'كبكابية', lat:13.9100, lng:24.1500, hoods:[] },
+ { name:'كتم', lat:15.0000, lng:24.6500, hoods:[] },
+ ]},
+ { state:'جنوب دارفور', lat:11.3000, lng:24.9000, cities:[
+ { name:'نيالا', lat:12.0500, lng:24.8800, hoods:['وسط نيالا','الشرق','الغرب','الجنوب','التجارية'] },
+ { name:'كاس', lat:11.6100, lng:24.6300, hoods:[] },
+ { name:'الضعين', lat:11.4600, lng:26.1200, hoods:[] },
+ { name:'عد الفرسان',lat:12.8000, lng:25.0000, hoods:[] },
+ ]},
+ { state:'غرب دارفور', lat:13.0000, lng:22.8000, cities:[
+ { name:'الجنينة', lat:13.4500, lng:22.4500, hoods:['وسط الجنينة','الشرق','الغرب'] },
+ { name:'كرنوي', lat:13.1400, lng:22.9000, hoods:[] },
+ { name:'بيضة', lat:12.3000, lng:22.6000, hoods:[] },
+ ]},
+ { state:'وسط دارفور', lat:12.8500, lng:24.3300, cities:[
+ { name:'زالنجي', lat:12.9100, lng:23.4700, hoods:[] },
+ { name:'نيرتيتي', lat:12.7000, lng:24.1000, hoods:[] },
+ ]},
+ { state:'شرق دارفور', lat:11.8000, lng:26.1000, cities:[
+ { name:'الضعين', lat:11.4600, lng:26.1200, hoods:[] },
+ { name:'عد الفرسان', lat:12.7800, lng:27.5000, hoods:[] },
+ { name:'أبو كارنكا', lat:12.2000, lng:26.8000, hoods:[] },
+ ]},
+ { state:'البحر الأحمر', lat:19.6200, lng:37.2200, cities:[
+ { name:'بورتسودان', lat:19.6200, lng:37.2200, hoods:['شرق بورتسودان','غرب بورتسودان','الميناء','الدرجة الثانية','حي المديرية'] },
+ { name:'سواكن', lat:19.1100, lng:37.3300, hoods:[] },
+ { name:'هيا', lat:18.3300, lng:36.3900, hoods:[] },
+ { name:'طوكر', lat:18.4500, lng:37.7000, hoods:[] },
+ ]},
+ ],
+
+ world: [
+ // الدول العربية
+ { name:'مصر', lat:30.0444, lng:31.2357, region:'عربي' },
+ { name:'السعودية', lat:24.6877, lng:46.7219, region:'عربي' },
+ { name:'الإمارات', lat:24.4539, lng:54.3773, region:'عربي' },
+ { name:'الكويت', lat:29.3759, lng:47.9774, region:'عربي' },
+ { name:'قطر', lat:25.2854, lng:51.5310, region:'عربي' },
+ { name:'البحرين', lat:26.2235, lng:50.5876, region:'عربي' },
+ { name:'عُمان', lat:23.5880, lng:58.3829, region:'عربي' },
+ { name:'اليمن', lat:15.3694, lng:44.1910, region:'عربي' },
+ { name:'ليبيا', lat:32.9028, lng:13.1805, region:'عربي' },
+ { name:'تونس', lat:36.8190, lng:10.1658, region:'عربي' },
+ { name:'الجزائر', lat:36.7372, lng:3.0865, region:'عربي' },
+ { name:'المغرب', lat:34.0209, lng:-6.8416, region:'عربي' },
+ { name:'موريتانيا', lat:18.0735, lng:-15.9582, region:'عربي' },
+ { name:'الصومال', lat:2.0469, lng:45.3182, region:'عربي' },
+ { name:'العراق', lat:33.3152, lng:44.3661, region:'عربي' },
+ { name:'سوريا', lat:33.5102, lng:36.2913, region:'عربي' },
+ { name:'لبنان', lat:33.8886, lng:35.4955, region:'عربي' },
+ { name:'الأردن', lat:31.9522, lng:35.9333, region:'عربي' },
+ { name:'فلسطين', lat:31.7683, lng:35.2137, region:'عربي' },
+ { name:'جزر القمر', lat:-11.6455, lng:43.3333, region:'عربي' },
+ { name:'جيبوتي', lat:11.5720, lng:43.1456, region:'عربي' },
+ // أفريقيا
+ { name:'إثيوبيا', lat:9.0320, lng:38.7469, region:'أفريقيا' },
+ { name:'إريتريا', lat:15.3229, lng:38.9251, region:'أفريقيا' },
+ { name:'كينيا', lat:-1.2921, lng:36.8219, region:'أفريقيا' },
+ { name:'تشاد', lat:12.1348, lng:15.0557, region:'أفريقيا' },
+ { name:'نيجيريا', lat:9.0579, lng:7.4951, region:'أفريقيا' },
+ { name:'غانا', lat:5.6037, lng:-0.1870, region:'أفريقيا' },
+ { name:'جنوب أفريقيا', lat:-25.7461, lng:28.1881, region:'أفريقيا' },
+ { name:'أوغندا', lat:0.3476, lng:32.5825, region:'أفريقيا' },
+ { name:'تنزانيا', lat:-6.7924, lng:39.2083, region:'أفريقيا' },
+ { name:'الكاميرون', lat:3.8480, lng:11.5021, region:'أفريقيا' },
+ { name:'رواندا', lat:-1.9403, lng:29.8739, region:'أفريقيا' },
+ { name:'زيمبابوي', lat:-17.8252, lng:31.0335, region:'أفريقيا' },
+ { name:'موزمبيق', lat:-18.6657, lng:35.5296, region:'أفريقيا' },
+ { name:'السنغال', lat:14.4974, lng:-14.4524, region:'أفريقيا' },
+ { name:'مالي', lat:17.5707, lng:-3.9962, region:'أفريقيا' },
+ { name:'النيجر', lat:17.6078, lng:8.0817, region:'أفريقيا' },
+ { name:'بوركينا فاسو', lat:12.3640, lng:-1.5330, region:'أفريقيا' },
+ { name:'غينيا', lat:11.8636, lng:-15.1384, region:'أفريقيا' },
+ { name:'كوت ديفوار', lat:7.5400, lng:-5.5471, region:'أفريقيا' },
+ { name:'الكونغو', lat:-4.3217, lng:15.3222, region:'أفريقيا' },
+ { name:'الكونغو الديمقراطية', lat:-4.0383, lng:21.7587, region:'أفريقيا' },
+ { name:'أنغولا', lat:-8.8383, lng:13.2344, region:'أفريقيا' },
+ { name:'ناميبيا', lat:-22.9576, lng:18.4904, region:'أفريقيا' },
+ { name:'بوتسوانا', lat:-24.6282, lng:25.9231, region:'أفريقيا' },
+ { name:'زامبيا', lat:-13.1339, lng:27.8493, region:'أفريقيا' },
+ { name:'مدغشقر', lat:-18.7669, lng:46.8691, region:'أفريقيا' },
+ { name:'موريشيوس', lat:-20.1609, lng:57.4977, region:'أفريقيا' },
+ { name:'ليسوتو', lat:-29.6100, lng:28.2336, region:'أفريقيا' },
+ { name:'إسواتيني', lat:-26.5225, lng:31.4659, region:'أفريقيا' },
+ { name:'بوروندي', lat:-3.3731, lng:29.9189, region:'أفريقيا' },
+ { name:'مالاوي', lat:-13.2543, lng:34.3015, region:'أفريقيا' },
+ { name:'ليبيريا', lat:6.4281, lng:-9.4295, region:'أفريقيا' },
+ { name:'سيراليون', lat:8.4606, lng:-11.7799, region:'أفريقيا' },
+ { name:'توغو', lat:8.6195, lng:0.8248, region:'أفريقيا' },
+ { name:'بنين', lat:9.3077, lng:2.3158, region:'أفريقيا' },
+ { name:'جمهورية أفريقيا الوسطى', lat:6.6111, lng:20.9394, region:'أفريقيا' },
+ { name:'إريتريا', lat:15.1794, lng:39.7823, region:'أفريقيا' },
+ { name:'الصومال', lat:5.1521, lng:46.1996, region:'أفريقيا' },
+ { name:'جيبوتي', lat:11.8251, lng:42.5903, region:'أفريقيا' },
+ { name:'إثيوبيا', lat:9.1450, lng:40.4897, region:'أفريقيا' },
+ // آسيا
+ { name:'تركيا', lat:39.9334, lng:32.8597, region:'آسيا' },
+ { name:'إيران', lat:35.6892, lng:51.3890, region:'آسيا' },
+ { name:'باكستان', lat:33.7294, lng:73.0931, region:'آسيا' },
+ { name:'الهند', lat:28.6139, lng:77.2090, region:'آسيا' },
+ { name:'الصين', lat:39.9042, lng:116.4074, region:'آسيا' },
+ { name:'اليابان', lat:35.6762, lng:139.6503, region:'آسيا' },
+ { name:'إندونيسيا', lat:-6.2088, lng:106.8456, region:'آسيا' },
+ { name:'ماليزيا', lat:3.1390, lng:101.6869, region:'آسيا' },
+ { name:'سنغافورة', lat:1.3521, lng:103.8198, region:'آسيا' },
+ { name:'تايلاند', lat:13.7563, lng:100.5018, region:'آسيا' },
+ { name:'فيتنام', lat:21.0285, lng:105.8542, region:'آسيا' },
+ { name:'كوريا الجنوبية', lat:37.5665, lng:126.9780, region:'آسيا' },
+ { name:'الفلبين', lat:14.5995, lng:120.9842, region:'آسيا' },
+ { name:'بنغلاديش', lat:23.6850, lng:90.3563, region:'آسيا' },
+ { name:'سريلانكا', lat:7.8731, lng:80.7718, region:'آسيا' },
+ { name:'نيبال', lat:28.3949, lng:84.1240, region:'آسيا' },
+ { name:'أفغانستان', lat:33.9391, lng:67.7100, region:'آسيا' },
+ { name:'كمبوديا', lat:11.5564, lng:104.9282, region:'آسيا' },
+ { name:'أذربيجان', lat:40.4093, lng:49.8671, region:'آسيا' },
+ { name:'جورجيا', lat:42.3154, lng:43.3569, region:'آسيا' },
+ { name:'أوزبكستان', lat:41.2995, lng:69.2401, region:'آسيا' },
+ { name:'كازاخستان', lat:51.1811, lng:71.4460, region:'آسيا' },
+ // أوروبا
+ { name:'المملكة المتحدة', lat:51.5074, lng:-0.1278, region:'أوروبا' },
+ { name:'فرنسا', lat:48.8566, lng:2.3522, region:'أوروبا' },
+ { name:'ألمانيا', lat:52.5200, lng:13.4050, region:'أوروبا' },
+ { name:'إيطاليا', lat:41.9028, lng:12.4964, region:'أوروبا' },
+ { name:'إسبانيا', lat:40.4168, lng:-3.7038, region:'أوروبا' },
+ { name:'هولندا', lat:52.3676, lng:4.9041, region:'أوروبا' },
+ { name:'السويد', lat:59.3293, lng:18.0686, region:'أوروبا' },
+ { name:'النرويج', lat:59.9139, lng:10.7522, region:'أوروبا' },
+ { name:'الدنمارك', lat:55.6761, lng:12.5683, region:'أوروبا' },
+ { name:'فنلندا', lat:60.1699, lng:24.9384, region:'أوروبا' },
+ { name:'بولندا', lat:52.2297, lng:21.0122, region:'أوروبا' },
+ { name:'رومانيا', lat:44.4268, lng:26.1025, region:'أوروبا' },
+ { name:'اليونان', lat:37.9838, lng:23.7275, region:'أوروبا' },
+ { name:'البرتغال', lat:38.7223, lng:-9.1393, region:'أوروبا' },
+ { name:'النمسا', lat:48.2082, lng:16.3738, region:'أوروبا' },
+ { name:'سويسرا', lat:46.9480, lng:7.4474, region:'أوروبا' },
+ { name:'بلجيكا', lat:50.8503, lng:4.3517, region:'أوروبا' },
+ { name:'روسيا', lat:55.7558, lng:37.6173, region:'أوروبا' },
+ { name:'أوكرانيا', lat:50.4501, lng:30.5234, region:'أوروبا' },
+ { name:'المجر', lat:47.4979, lng:19.0402, region:'أوروبا' },
+ { name:'التشيك', lat:50.0755, lng:14.4378, region:'أوروبا' },
+ // أمريكا
+ { name:'الولايات المتحدة', lat:38.9072, lng:-77.0369, region:'أمريكا' },
+ { name:'كندا', lat:45.4215, lng:-75.6919, region:'أمريكا' },
+ { name:'البرازيل', lat:-15.7942, lng:-47.8825, region:'أمريكا' },
+ { name:'المكسيك', lat:19.4326, lng:-99.1332, region:'أمريكا' },
+ { name:'الأرجنتين', lat:-34.6037, lng:-58.3816, region:'أمريكا' },
+ { name:'كولومبيا', lat:4.7110, lng:-74.0721, region:'أمريكا' },
+ { name:'تشيلي', lat:-33.4489, lng:-70.6693, region:'أمريكا' },
+ { name:'بيرو', lat:-12.0464, lng:-77.0428, region:'أمريكا' },
+ { name:'فنزويلا', lat:10.4806, lng:-66.9036, region:'أمريكا' },
+ { name:'الإكوادور', lat:-0.1807, lng:-78.4678, region:'أمريكا' },
+ // أوقيانوسيا
+ { name:'أستراليا', lat:-35.2802, lng:149.1310, region:'أوقيانوسيا' },
+ { name:'نيوزيلندا', lat:-41.2866, lng:174.7756, region:'أوقيانوسيا' },
+ { name:'بابوا غينيا الجديدة', lat:-9.4438, lng:147.1803, region:'أوقيانوسيا' },
+ { name:'فيجي', lat:-18.1416, lng:178.4419, region:'أوقيانوسيا' },
+ ]
+};
+
+/* ============================================================
+ DATA STORE + PERSISTENCE
+ ============================================================ */
+const DB_FILE = path.join(__dirname, 'nabdh_data.json');
+
+function loadData() {
+ try {
+ if (fs.existsSync(DB_FILE)) {
+ const raw = fs.readFileSync(DB_FILE, 'utf8');
+ const saved = JSON.parse(raw);
+ return {
+ alerts: saved.alerts || [],
+ exchangeRates:saved.exchangeRates || [],
+ medicines: saved.medicines || [],
+ mapPins: saved.mapPins || [],
+ voiceItems: saved.voiceItems || [],
+ skills: saved.skills || [],
+ marketplace: saved.marketplace || [],
+ chatRooms: saved.chatRooms || {},
+ onlineUsers: {},
+ profiles: saved.profiles || {},
+ messages: saved.messages || {},
+ bloodRequests:saved.bloodRequests || [],
+ bloodDonors: saved.bloodDonors || [],
+ powerSchedule:saved.powerSchedule || [],
+ images: saved.images || {},
+ hospitals: saved.hospitals || [],
+ news: saved.news || [],
+ rides: saved.rides || [],
+ waterReports: saved.waterReports || [],
+ studyGroups: saved.studyGroups || {},
+ hoodGroups: saved.hoodGroups || {},
+ helpRequests: saved.helpRequests || [],
+ polls: saved.polls || [],
+ leaderboard: saved.leaderboard || [],
+ referrals: saved.referrals || [],
+ stats: { users:0, reports: saved.stats ? (saved.stats.reports||0) : 0,
+ lives_saved: saved.stats ? (saved.stats.lives_saved||0) : 0,
+ cities: saved.stats ? (saved.stats.cities||0) : 0 }
+ };
+ }
+ } catch(e) { console.warn('⚠️ Could not load data:', e.message); }
+ return {
+ alerts:[], exchangeRates:[], medicines:[], mapPins:[], voiceItems:[],
+ skills:[], marketplace:[], chatRooms:{}, onlineUsers:{}, profiles:{},
+ messages:{}, bloodRequests:[], bloodDonors:[], powerSchedule:[],
+ images:{}, hospitals:[], news:[], rides:[], waterReports:[],
+ studyGroups:{}, hoodGroups:{}, helpRequests:[], polls:[],
+ stats:{ users:0, reports:0, lives_saved:0, cities:0 }
+ };
+}
+
+let data = loadData();
+console.log(`📂 Data loaded: ${data.alerts.length} alerts, ${data.marketplace.length} market items, ${data.bloodDonors.length} blood donors`);
+
+let _saveTimer = null;
+function saveData() {
+ if (_saveTimer) clearTimeout(_saveTimer);
+ _saveTimer = setTimeout(() => {
+ try {
+ const toSave = {
+ alerts: data.alerts.slice(0,200),
+ exchangeRates: data.exchangeRates.slice(0,100),
+ medicines: data.medicines.slice(0,200),
+ mapPins: data.mapPins.slice(0,300),
+ voiceItems: data.voiceItems.slice(0,200),
+ skills: data.skills.slice(0,200),
+ marketplace: data.marketplace.slice(0,200),
+ chatRooms: data.chatRooms,
+ profiles: data.profiles,
+ messages: data.messages,
+ bloodRequests: data.bloodRequests.slice(0,200),
+ bloodDonors: data.bloodDonors.slice(0,500),
+ powerSchedule: data.powerSchedule.slice(0,300),
+ images: data.images,
+ hospitals: data.hospitals.slice(0,300),
+ news: data.news.slice(0,200),
+ rides: data.rides.slice(0,200),
+ waterReports: data.waterReports.slice(0,200),
+ studyGroups: data.studyGroups,
+ hoodGroups: data.hoodGroups,
+ helpRequests: data.helpRequests.slice(0,200),
+ polls: data.polls.slice(0,100),
+ leaderboard: (data.leaderboard||[]).slice(0,2000),
+ referrals: (data.referrals||[]).slice(0,5000),
+ stats: { reports: data.stats.reports, lives_saved: data.stats.lives_saved, cities: data.stats.cities }
+ };
+ fs.writeFileSync(DB_FILE, JSON.stringify(toSave), 'utf8');
+ } catch(e) { console.warn('⚠️ Save error:', e.message); }
+ }, 2000);
+}
+
+function updateCitiesCount() {
+ const a = [
+ ...data.alerts.map(x=>x.area),
+ ...data.exchangeRates.map(x=>x.source),
+ ...data.medicines.map(x=>x.area),
+ ...data.voiceItems.map(x=>x.area),
+ ...data.marketplace.map(x=>x.area),
+ ];
+ data.stats.cities = new Set(a.filter(Boolean)).size;
+}
+
+function haversine(la1,lo1,la2,lo2){
+ const R=6371, dL=d=>d*Math.PI/180;
+ const a=Math.sin(dL(la2-la1)/2)**2+Math.cos(dL(la1))*Math.cos(dL(la2))*Math.sin(dL(lo2-lo1)/2)**2;
+ return R*2*Math.atan2(Math.sqrt(a),Math.sqrt(1-a));
+}
+
+/* ============================================================
+ API - البيانات الجغرافية
+ ============================================================ */
+app.get('/api/geo/sudan', (_,res) => res.json(GEO.sudan));
+app.get('/api/geo/world', (_,res) => res.json(GEO.world));
+
+app.get('/api/geo/search', (req,res) => {
+ const q = (req.query.q||'').trim().toLowerCase();
+ if (!q || q.length < 1) return res.json([]);
+ const results = [];
+ for (const st of GEO.sudan) {
+ if (st.state.includes(q))
+ results.push({ label:`🇸🇩 ${st.state}`, name:st.state, lat:st.lat, lng:st.lng, type:'state' });
+ for (const ct of st.cities) {
+ if (ct.name.includes(q))
+ results.push({ label:`🏙️ ${ct.name} - ${st.state}`, name:ct.name, state:st.state, lat:ct.lat, lng:ct.lng, type:'city' });
+ for (const h of ct.hoods) {
+ if (h.includes(q))
+ results.push({ label:`🏘️ حي ${h} - ${ct.name}`, name:`حي ${h}`, city:ct.name, state:st.state, lat:ct.lat+(Math.random()-.5)*.02, lng:ct.lng+(Math.random()-.5)*.02, type:'hood' });
+ }
+ }
+ }
+ for (const c of GEO.world) {
+ if (c.name.includes(q))
+ results.push({ label:`🌍 ${c.name} (${c.region})`, name:c.name, lat:c.lat, lng:c.lng, type:'country' });
+ }
+ res.json(results.slice(0,15));
+});
+
+/* ============================================================
+ API - الإحصاء
+ ============================================================ */
+app.get('/api/stats', (_,res) => {
+ // حساب الإحصائيات الحية
+ const areas = new Set([
+ ...data.alerts.map(x=>x.area),
+ ...data.exchangeRates.map(x=>x.source),
+ ...data.voiceItems.map(x=>x.area),
+ ...data.marketplace.map(x=>x.area),
+ ].filter(Boolean));
+ data.stats.cities = Math.max(areas.size, data.stats.cities || 0);
+ data.stats.reports = Math.max(data.alerts.length, data.stats.reports || 0);
+ const onlineCount = Object.keys(data.onlineUsers||{}).length;
+ res.json({
+ users: onlineCount || data.stats.users || 0,
+ reports: data.stats.reports,
+ lives_saved: data.stats.lives_saved || 0,
+ cities: data.stats.cities,
+ total_alerts: data.alerts.length,
+ market_items: data.marketplace.length,
+ blood_donors: data.bloodDonors.filter(d=>d.available).length,
+ online: onlineCount
+ });
+});
+
+/* ============================================================
+ API - التنبيهات والخريطة
+ ============================================================ */
+app.get('/api/alerts', (_,res) => res.json(data.alerts.sort((a,b)=>(b.time||b.ts||0)-(a.time||a.ts||0))));
+app.get('/api/map', (_,res) => res.json(data.mapPins));
+
+app.get('/api/alerts/nearby', (req,res) => {
+ const lat=parseFloat(req.query.lat), lng=parseFloat(req.query.lng), km=parseFloat(req.query.km)||100;
+ if (isNaN(lat)||isNaN(lng)) return res.json(data.alerts);
+ res.json(data.alerts.filter(a=>a.lat&&a.lng&&haversine(lat,lng,a.lat,a.lng)<=km).sort((a,b)=>(b.time||b.ts||0)-(a.time||a.ts||0)));
+});
+
+// إحصاء كثافة المناطق (heatmap data)
+app.get('/api/heatmap', (_,res) => {
+ const pts = data.alerts.filter(a=>a.lat&&a.lng).map(a=>({ lat:a.lat, lng:a.lng, type:a.type, weight: a.votes+1 }));
+ res.json(pts);
+});
+
+// المستخدمون النشطون القريبون (للـ P2P)
+app.get('/api/users/nearby', (req,res) => {
+ const lat=parseFloat(req.query.lat), lng=parseFloat(req.query.lng), km=parseFloat(req.query.km)||50;
+ if (isNaN(lat)||isNaN(lng)) return res.json([]);
+ const now = Date.now();
+ const active = Object.values(data.onlineUsers).filter(u=>
+ u.lat && u.lng && (now - u.time) < 300000 && haversine(lat,lng,u.lat,u.lng) <= km
+ );
+ res.json(active.map(u=>({ name:u.name||'مستخدم', area:u.area||'غير محدد', lat:u.lat, lng:u.lng, dist:Math.round(haversine(lat,lng,u.lat,u.lng)) })));
+});
+
+app.post('/api/alerts', (req,res) => {
+ const { type, msg, area, lat, lng, imageId } = req.body;
+ if (!msg?.trim()) return res.status(400).json({ error:'الرسالة مطلوبة' });
+ const icons = { danger:'🔴', warning:'🟡', info:'🟢' };
+ const alert = {
+ id: uuidv4(), type:type||'warning', icon:icons[type]||'🟡',
+ msg: msg.trim(), area: area||'غير محدد',
+ lat: lat||null, lng: lng||null,
+ imageId: imageId||null,
+ votes:0, time: Date.now()
+ };
+ data.alerts.unshift(alert);
+ data.mapPins.unshift({...alert});
+ data.stats.reports++;
+ updateCitiesCount();
+ saveData();
+ io.emit('new_alert', alert);
+ io.emit('stats_update', data.stats);
+ res.json({ success:true, alert });
+});
+
+app.post('/api/alerts/:id/vote', (req,res) => {
+ const a = data.alerts.find(x=>x.id===req.params.id);
+ if (!a) return res.status(404).json({ error:'غير موجود' });
+ a.votes++;
+ const pin = data.mapPins.find(x=>x.id===a.id);
+ if (pin) pin.votes = a.votes;
+ io.emit('vote_update', { id:a.id, votes:a.votes });
+ res.json({ success:true, votes:a.votes });
+});
+
+/* ============================================================
+ API - سعر الصرف
+ ============================================================ */
+app.get('/api/exchange', (_,res) => res.json(data.exchangeRates.sort((a,b)=>(b.time||b.ts||0)-(a.time||a.ts||0))));
+
+app.post('/api/exchange', (req,res) => {
+ const { rate, source, lat, lng } = req.body;
+ if (!rate||isNaN(rate)||Number(rate)<1) return res.status(400).json({ error:'سعر غير صحيح' });
+ const entry = {
+ id:uuidv4(), rate:Number(rate), source:source||'غير محدد',
+ lat:lat||null, lng:lng||null, verified:false, time:Date.now()
+ };
+ data.exchangeRates.unshift(entry);
+ updateCitiesCount();
+ io.emit('new_rate', entry);
+ res.json({ success:true, entry });
+});
+
+/* ============================================================
+ API - الأدوية
+ ============================================================ */
+app.get('/api/medicines', (req,res) => {
+ const q=(req.query.q||'').toLowerCase();
+ res.json(q ? data.medicines.filter(m=>m.name.includes(q)||(m.nameEn||'').toLowerCase().includes(q)) : data.medicines);
+});
+
+app.post('/api/medicines', (req,res) => {
+ const { name, nameEn, pharmacy, area, price, available, lat, lng } = req.body;
+ if (!name?.trim()) return res.status(400).json({ error:'اسم الدواء مطلوب' });
+ const med = {
+ id:uuidv4(), name:name.trim(), nameEn:(nameEn||'').trim(),
+ pharmacy:(pharmacy||'غير محدد').trim(), area:(area||'غير محدد').trim(),
+ price:Number(price)||0, available:available===true||available==='true',
+ lat:lat||null, lng:lng||null, time:Date.now()
+ };
+ data.medicines.unshift(med);
+ updateCitiesCount();
+ io.emit('new_medicine', med);
+ res.json({ success:true, med });
+});
+
+/* ============================================================
+ API - صوت الحي
+ ============================================================ */
+app.get('/api/voice', (_,res) => res.json(data.voiceItems.sort((a,b)=>b.votes-a.votes)));
+
+app.post('/api/voice', (req,res) => {
+ const { title, desc, area, category, lat, lng } = req.body;
+ if (!title?.trim()) return res.status(400).json({ error:'العنوان مطلوب' });
+ const item = {
+ id:uuidv4(), title:title.trim(), desc:(desc||'').trim(),
+ area:(area||'غير محدد').trim(), category:category||'أخرى',
+ lat:lat||null, lng:lng||null, votes:0, time:Date.now()
+ };
+ data.voiceItems.unshift(item);
+ data.stats.reports++;
+ updateCitiesCount();
+ io.emit('new_voice', item);
+ io.emit('stats_update', data.stats);
+ res.json({ success:true, item });
+});
+
+app.post('/api/voice/:id/vote', (req,res) => {
+ const v = data.voiceItems.find(x=>x.id===req.params.id);
+ if (!v) return res.status(404).json({ error:'غير موجود' });
+ v.votes++;
+ io.emit('voice_vote', { id:v.id, votes:v.votes });
+ res.json({ success:true, votes:v.votes });
+});
+
+/* ============================================================
+ API - بورصة المهارات
+ ============================================================ */
+app.get('/api/skills', (_,res) => res.json(data.skills));
+
+app.post('/api/skills', (req,res) => {
+ const { name, skill, offer, want, area, lat, lng, contact } = req.body;
+ if (!name||!offer||!want) return res.status(400).json({ error:'بيانات ناقصة' });
+ const s = {
+ id:uuidv4(), name:name.trim(), skill:(skill||offer).trim(),
+ offer:offer.trim(), want:want.trim(), area:(area||'غير محدد').trim(),
+ lat:lat||null, lng:lng||null, contact:contact||'',
+ rating:5.0, avatar:name.trim().substring(0,2).toUpperCase(), time:Date.now()
+ };
+ data.skills.unshift(s);
+ io.emit('new_skill', s);
+ res.json({ success:true, skill:s });
+});
+
+/* ============================================================
+ API - سوق P2P
+ ============================================================ */
+app.get('/api/market', (_,res) => res.json(data.marketplace.sort((a,b)=>(b.time||b.ts||0)-(a.time||a.ts||0))));
+
+app.get('/api/market/nearby', (req,res) => {
+ const lat=parseFloat(req.query.lat), lng=parseFloat(req.query.lng), km=parseFloat(req.query.km)||50;
+ if (isNaN(lat)||isNaN(lng)) return res.json(data.marketplace);
+ res.json(data.marketplace.filter(m=>m.lat&&m.lng&&haversine(lat,lng,m.lat,m.lng)<=km).sort((a,b)=>(b.time||b.ts||0)-(a.time||a.ts||0)));
+});
+
+app.post('/api/market', (req,res) => {
+ const { title, desc, type, price, currency, area, lat, lng, contact, category } = req.body;
+ if (!title?.trim()) return res.status(400).json({ error:'عنوان المنتج مطلوب' });
+ const item = {
+ id:uuidv4(),
+ title:title.trim(),
+ desc:(desc||'').trim(),
+ type:type||'sell',
+ price:Number(price)||0,
+ currency:currency||'ج.س',
+ category:category||'أخرى',
+ area:(area||'غير محدد').trim(),
+ lat:lat||null, lng:lng||null,
+ contact:(contact||'').trim(),
+ views:0, likes:0,
+ status:'active',
+ time:Date.now()
+ };
+ data.marketplace.unshift(item);
+ updateCitiesCount();
+ io.emit('new_market_item', item);
+ res.json({ success:true, item });
+});
+
+app.post('/api/market/:id/like', (req,res) => {
+ const m = data.marketplace.find(x=>x.id===req.params.id);
+ if (!m) return res.status(404).json({ error:'غير موجود' });
+ m.likes++;
+ io.emit('market_like', { id:m.id, likes:m.likes });
+ res.json({ success:true, likes:m.likes });
+});
+
+app.post('/api/market/:id/view', (req,res) => {
+ const m = data.marketplace.find(x=>x.id===req.params.id);
+ if (m) m.views++;
+ res.json({ success:true });
+});
+
+/* ============================================================
+ API - دردشة P2P
+ ============================================================ */
+app.get('/api/chat/:room', (req,res) => {
+ const msgs = data.chatRooms[req.params.room] || [];
+ res.json(msgs.slice(-50));
+});
+
+app.post('/api/chat/:room', (req,res) => {
+ const { text, sender, senderArea, mediaType=null, mediaData=null, mediaName=null } = req.body;
+ if (!text?.trim() && !mediaData) return res.status(400).json({ error:'الرسالة فارغة' });
+ const room = req.params.room;
+ if (!data.chatRooms[room]) data.chatRooms[room] = [];
+ const msg = {
+ id: uuidv4(),
+ text: (text||'').trim(),
+ sender: sender||'مجهول',
+ senderArea: senderArea||'',
+ mediaType: mediaType||null,
+ mediaData: mediaData||null,
+ mediaName: mediaName||null,
+ time: Date.now()
+ };
+ data.chatRooms[room].push(msg);
+ if (data.chatRooms[room].length > 200) data.chatRooms[room] = data.chatRooms[room].slice(-200);
+ saveData();
+ io.to(`chat:${room}`).emit('chat_msg', { room, msg });
+ res.json({ success:true, msg });
+});
+
+/* ============================================================
+ API - الملف الشخصي (User Profiles) - Enhanced v2
+ ============================================================ */
+
+// إنشاء أو تحديث الملف الشخصي - مُحسَّن بإضافة رقم معلن وشركة وموقع
+app.post('/api/profile', (req, res) => {
+ const {
+ userId, name, phone, email, bio, avatar, area, lat, lng,
+ isPublic, showOnMap, publicPhone, company, website, jobTitle,
+ whatsapp, telegram, instagram, twitter, profileImage
+ } = req.body;
+ if (!userId) return res.status(400).json({ error: 'userId مطلوب' });
+ const existing = data.profiles[userId] || {};
+ const profile = {
+ ...existing,
+ userId,
+ name: (name || existing.name || '').trim(),
+ phone: (phone || existing.phone || '').trim(),
+ publicPhone: (publicPhone !== undefined ? publicPhone : existing.publicPhone) || '', // الرقم المعلن
+ email: (email || existing.email || '').trim(),
+ bio: (bio || existing.bio || '').trim(),
+ avatar: (avatar || existing.avatar || ''),
+ profileImage: (profileImage|| existing.profileImage|| ''),
+ area: (area || existing.area || 'غير محدد').trim(),
+ lat: lat != null ? Number(lat) : (existing.lat || null),
+ lng: lng != null ? Number(lng) : (existing.lng || null),
+ isPublic: isPublic != null ? (isPublic === true || isPublic === 'true') : (existing.isPublic !== false),
+ showOnMap: showOnMap != null ? (showOnMap === true || showOnMap === 'true') : (existing.showOnMap !== false),
+ // معلومات الشركة/المهنة
+ company: (company || existing.company || '').trim(),
+ website: (website || existing.website || '').trim(),
+ jobTitle: (jobTitle || existing.jobTitle || '').trim(),
+ // وسائل التواصل
+ whatsapp: (whatsapp || existing.whatsapp || '').trim(),
+ telegram: (telegram || existing.telegram || '').trim(),
+ instagram: (instagram || existing.instagram || '').trim(),
+ twitter: (twitter || existing.twitter || '').trim(),
+ verified: existing.verified || false,
+ joinDate: existing.joinDate || Date.now(),
+ lastSeen: Date.now(),
+ reports: existing.reports || 0,
+ };
+ data.profiles[userId] = profile;
+ // Broadcast profile update to nearby users
+ saveData();
+ io.emit('profile_updated', { userId, name: profile.name, avatar: profile.avatar, area: profile.area });
+ res.json({ success: true, profile });
+});
+
+// جلب ملف شخصي بـ userId
+app.get('/api/profile/:userId', (req, res) => {
+ const p = data.profiles[req.params.userId];
+ if (!p) return res.status(404).json({ error: 'الملف الشخصي غير موجود' });
+ res.json(p);
+});
+
+// جلب قائمة ملفات شخصية عامة
+app.get('/api/profiles', (req, res) => {
+ const list = Object.values(data.profiles)
+ .filter(p => p.isPublic)
+ .sort((a, b) => b.lastSeen - a.lastSeen)
+ .slice(0, 100);
+ res.json(list);
+});
+
+/* ============================================================
+ API - البحث عن الأشخاص (Truecaller-style) - مُحسَّن
+ ============================================================ */
+app.get('/api/search/people', (req, res) => {
+ const q = (req.query.q || '').trim().toLowerCase();
+ const type = req.query.type || 'all'; // name | phone | email | company | all
+ const limit = Math.min(parseInt(req.query.limit) || 30, 50);
+ if (!q || q.length < 2) return res.json([]);
+
+ // Search across profiles
+ const profileResults = Object.values(data.profiles).filter(p => {
+ if (!p.isPublic) return false;
+ const matchName = p.name && p.name.toLowerCase().includes(q);
+ const matchPhone = p.phone && p.phone.replace(/\s/g,'').includes(q.replace(/\s/g,''));
+ const matchPubPhone= p.publicPhone && p.publicPhone.replace(/\s/g,'').includes(q.replace(/\s/g,''));
+ const matchEmail = p.email && p.email.toLowerCase().includes(q);
+ const matchArea = p.area && p.area.toLowerCase().includes(q);
+ const matchCompany = p.company && p.company.toLowerCase().includes(q);
+ const matchJob = p.jobTitle && p.jobTitle.toLowerCase().includes(q);
+ const matchWebsite = p.website && p.website.toLowerCase().includes(q);
+ if (type === 'name') return matchName;
+ if (type === 'phone') return matchPhone || matchPubPhone;
+ if (type === 'email') return matchEmail;
+ if (type === 'company') return matchCompany || matchJob || matchWebsite;
+ return matchName || matchPhone || matchPubPhone || matchEmail || matchArea || matchCompany || matchJob;
+ }).map(p => {
+ const exactPhone = (p.phone && p.phone.replace(/\s/g,'') === q.replace(/\s/g,'')) ||
+ (p.publicPhone && p.publicPhone.replace(/\s/g,'') === q.replace(/\s/g,''));
+ const exactEmail = p.email && p.email.toLowerCase() === q;
+ return {
+ userId: p.userId,
+ name: p.name,
+ bio: p.bio,
+ area: p.area,
+ avatar: p.avatar,
+ profileImage:p.profileImage,
+ verified: p.verified,
+ joinDate: p.joinDate,
+ lastSeen: p.lastSeen,
+ lat: p.lat,
+ lng: p.lng,
+ company: p.company,
+ jobTitle: p.jobTitle,
+ website: p.website,
+ whatsapp: p.whatsapp,
+ telegram: p.telegram,
+ // الرقم المعلن يُظهر دائماً
+ publicPhone: p.publicPhone || '',
+ // الرقم الشخصي يُخفى جزئياً ما لم يكن بحثاً تاماً
+ phone: exactPhone ? p.phone : (p.phone ? p.phone.replace(/\d(?=\d{4})/g, '*') : ''),
+ email: exactEmail ? p.email : (p.email ? p.email.replace(/(?<=.{2}).(?=[^@]*@)/g, '*') : ''),
+ type: 'person',
+ };
+ });
+
+ // Also search market listings for companies/businesses
+ const marketResults = data.marketplace.filter(m => {
+ const qLow = q;
+ return m.title.toLowerCase().includes(qLow) || (m.area && m.area.toLowerCase().includes(qLow));
+ }).slice(0, 5).map(m => ({
+ userId: null,
+ name: m.title,
+ bio: m.desc,
+ area: m.area,
+ avatar: '🛒',
+ verified: false,
+ lat: m.lat,
+ lng: m.lng,
+ publicPhone: m.contact,
+ phone: m.contact,
+ email: '',
+ company: m.category,
+ type: 'listing',
+ listingId: m.id,
+ price: m.price,
+ currency: m.currency,
+ }));
+
+ // Also search skills (freelancers/companies)
+ const skillResults = data.skills.filter(s => {
+ const qLow = q;
+ return s.name.toLowerCase().includes(qLow) || s.skill.toLowerCase().includes(qLow) ||
+ s.offer.toLowerCase().includes(qLow) || (s.area && s.area.toLowerCase().includes(qLow));
+ }).slice(0, 5).map(s => ({
+ userId: null,
+ name: s.name,
+ bio: s.offer + ' ↔ ' + s.want,
+ area: s.area,
+ avatar: '🤝',
+ verified: false,
+ lat: s.lat,
+ lng: s.lng,
+ publicPhone: s.contact,
+ phone: s.contact,
+ email: '',
+ company: s.skill,
+ type: 'skill',
+ }));
+
+ const combined = [...profileResults, ...marketResults, ...skillResults].slice(0, limit);
+ res.json(combined);
+});
+
+// بحث سريع بالرقم المُعلن
+app.get('/api/search/phone/:phone', (req, res) => {
+ const phone = req.params.phone.replace(/\s/g, '');
+ const profile = Object.values(data.profiles).find(p =>
+ (p.phone && p.phone.replace(/\s/g,'') === phone) ||
+ (p.publicPhone && p.publicPhone.replace(/\s/g,'') === phone) ||
+ (p.whatsapp && p.whatsapp.replace(/\s/g,'') === phone)
+ );
+ if (!profile) {
+ const onlineMatch = Object.values(data.onlineUsers).find(u =>
+ u.phone && u.phone.replace(/\s/g,'') === phone
+ );
+ if (onlineMatch) return res.json({ found: true, name: onlineMatch.name, area: onlineMatch.area, online: true });
+ return res.json({ found: false });
+ }
+ if (!profile.isPublic) return res.json({ found: false });
+ res.json({ found: true, ...profile });
+});
+
+// تحديث الرقم المعلن فقط
+app.post('/api/profile/:userId/public-phone', (req, res) => {
+ const { publicPhone } = req.body;
+ const p = data.profiles[req.params.userId];
+ if (!p) return res.status(404).json({ error: 'الملف غير موجود' });
+ p.publicPhone = (publicPhone || '').trim();
+ res.json({ success: true, publicPhone: p.publicPhone });
+});
+
+// تحديد موقع شخص محدد على الخريطة
+app.get('/api/people/locate/:userId', (req, res) => {
+ const uid = req.params.userId;
+ // Check online users first (real-time location)
+ const onlineEntry = Object.values(data.onlineUsers).find(u => u.userId === uid);
+ if (onlineEntry && onlineEntry.lat) {
+ return res.json({
+ found: true, live: true,
+ lat: onlineEntry.lat, lng: onlineEntry.lng,
+ area: onlineEntry.area, name: onlineEntry.name,
+ lastSeen: onlineEntry.time,
+ });
+ }
+ // Fallback to profile location
+ const profile = data.profiles[uid];
+ if (profile && profile.lat && profile.isPublic && profile.showOnMap !== false) {
+ return res.json({
+ found: true, live: false,
+ lat: profile.lat, lng: profile.lng,
+ area: profile.area, name: profile.name,
+ lastSeen: profile.lastSeen,
+ });
+ }
+ res.json({ found: false });
+});
+
+/* ============================================================
+ API - المراسلة المباشرة (Direct Messaging)
+ ============================================================ */
+
+// جلب المحادثات
+app.get('/api/dm/:userId', (req, res) => {
+ const uid = req.params.userId;
+ const convs = Object.entries(data.messages)
+ .filter(([id]) => id.includes(uid))
+ .map(([id, msgs]) => {
+ const other = id.split('__').find(p => p !== uid) || 'unknown';
+ const otherProfile = data.profiles[other] || { name: 'مستخدم', userId: other };
+ return {
+ conversationId: id,
+ otherUser: { userId: other, name: otherProfile.name, avatar: otherProfile.avatar, area: otherProfile.area },
+ lastMsg: msgs[msgs.length - 1] || null,
+ unread: msgs.filter(m => !m.read && m.senderId !== uid).length,
+ };
+ })
+ .sort((a, b) => (b.lastMsg?.time || 0) - (a.lastMsg?.time || 0));
+ res.json(convs);
+});
+
+// جلب رسائل محادثة معينة
+app.get('/api/dm/:userId/:otherId', (req, res) => {
+ const { userId, otherId } = req.params;
+ const convId = [userId, otherId].sort().join('__');
+ const msgs = data.messages[convId] || [];
+ // Mark as read
+ msgs.forEach(m => { if (m.senderId !== userId) m.read = true; });
+ res.json(msgs.slice(-80));
+});
+
+// إرسال رسالة مباشرة
+app.post('/api/dm/:userId/:otherId', (req, res) => {
+ const { userId, otherId } = req.params;
+ const { text, senderName } = req.body;
+ if (!text?.trim()) return res.status(400).json({ error: 'الرسالة فارغة' });
+ const convId = [userId, otherId].sort().join('__');
+ if (!data.messages[convId]) data.messages[convId] = [];
+ const msg = {
+ id: uuidv4(),
+ senderId: userId,
+ senderName: senderName || data.profiles[userId]?.name || 'مستخدم',
+ text: text.trim(),
+ time: Date.now(),
+ read: false,
+ };
+ data.messages[convId].push(msg);
+ if (data.messages[convId].length > 500) data.messages[convId] = data.messages[convId].slice(-500);
+ saveData();
+ // Emit to the recipient via socket
+ const recipientSocket = Object.entries(data.onlineUsers).find(([, u]) => u.userId === otherId);
+ if (recipientSocket) {
+ io.to(recipientSocket[0]).emit('dm_msg', { conversationId: convId, msg, from: userId });
+ }
+ res.json({ success: true, msg });
+});
+
+/* ============================================================
+ API - المستخدمون على الخريطة (Live People Map)
+ ============================================================ */
+app.get('/api/people/map', (req, res) => {
+ const now = Date.now();
+ const people = Object.values(data.onlineUsers)
+ .filter(u => u.lat && u.lng && u.showOnMap && (now - u.time) < 600000) // 10 min
+ .map(u => ({
+ socketId: u.socketId,
+ userId: u.userId || null,
+ name: u.name || 'مستخدم',
+ area: u.area || '',
+ lat: u.lat,
+ lng: u.lng,
+ avatar: u.avatar || '',
+ lastSeen: u.time,
+ }));
+ res.json(people);
+});
+
+/* ============================================================
+ API - نداء الاستغاثة SOS
+ ============================================================ */
+app.post('/api/sos', (req,res) => {
+ const { lat, lng, name, area } = req.body;
+ if (!lat || !lng) return res.status(400).json({ error:'الموقع مطلوب' });
+ const sos = { id:uuidv4(), lat:Number(lat), lng:Number(lng), name:name||'مستخدم', area:area||'غير محدد', time:Date.now() };
+ // Broadcast to all nearby users
+ const nearby = Object.values(data.onlineUsers).filter(u =>
+ u.lat && u.lng && haversine(lat,lng,u.lat,u.lng) <= 100
+ );
+ nearby.forEach(u => {
+ io.to(u.socketId).emit('sos_alert', { ...sos, dist: Math.round(haversine(lat,lng,u.lat,u.lng)) });
+ });
+ io.emit('sos_alert', sos); // broadcast to all
+ // Also add as a danger alert
+ const icons = { danger:'🆘' };
+ const alert = {
+ id: uuidv4(), type:'danger', icon:'🆘',
+ msg: `🆘 نداء استغاثة من ${name||'مستخدم'}`,
+ area: area||'غير محدد', lat:Number(lat), lng:Number(lng), votes:0, time:Date.now()
+ };
+ data.alerts.unshift(alert);
+ data.mapPins.unshift({...alert});
+ data.stats.reports++;
+ updateCitiesCount();
+ io.emit('new_alert', alert);
+ io.emit('stats_update', data.stats);
+ res.json({ success:true, sos, notified: nearby.length });
+});
+
+
+
+/* ============================================================
+ SOCKET.IO
+ ============================================================ */
+io.on('connection', socket => {
+ data.stats.users++;
+ io.emit('stats_update', data.stats);
+
+ // تسجيل موقع المستخدم (يُستدعى تلقائياً كلما تحرّك المستخدم)
+ socket.on('user_location', ({ lat, lng, name, area, userId, showOnMap, avatar, phone }) => {
+ data.onlineUsers[socket.id] = {
+ lat, lng, name:name||'مستخدم', area:area||'غير محدد',
+ time:Date.now(), socketId:socket.id,
+ userId: userId || null,
+ showOnMap: showOnMap !== false,
+ avatar: avatar || '',
+ phone: phone || '',
+ };
+ // تحديث lastSeen في الملف الشخصي
+ if (userId && data.profiles[userId]) {
+ data.profiles[userId].lastSeen = Date.now();
+ data.profiles[userId].lat = lat;
+ data.profiles[userId].lng = lng;
+ data.profiles[userId].area = area || data.profiles[userId].area;
+ }
+ // إرسال المستخدمين القريبين لهذا المستخدم
+ if (lat && lng) {
+ const nearby = Object.values(data.onlineUsers)
+ .filter(u => u.socketId !== socket.id && u.lat && u.lng && haversine(lat,lng,u.lat,u.lng) <= 50);
+ socket.emit('nearby_users', nearby.map(u=>({
+ name:u.name, area:u.area, lat:u.lat, lng:u.lng,
+ userId:u.userId, avatar:u.avatar,
+ dist:Math.round(haversine(lat,lng,u.lat,u.lng))
+ })));
+ // Broadcast updated people map to nearby
+ io.emit('people_map_update');
+ }
+ });
+
+ socket.on('disconnect', () => {
+ data.stats.users = Math.max(0, data.stats.users-1);
+ delete data.onlineUsers[socket.id];
+ io.emit('stats_update', data.stats);
+ });
+
+ // P2P direct message
+ socket.on('p2p_msg', msg => io.to(msg.to).emit('p2p_msg', msg));
+
+ // DM via socket (real-time)
+ socket.on('dm_send', ({ toUserId, text, senderName, fromUserId, mediaType=null, mediaData=null, mediaName=null }) => {
+ if ((!text?.trim() && !mediaData) || !toUserId || !fromUserId) return;
+ const convId = [fromUserId, toUserId].sort().join('__');
+ if (!data.messages[convId]) data.messages[convId] = [];
+ const msg = {
+ id: uuidv4(),
+ senderId: fromUserId,
+ senderName: senderName || 'مستخدم',
+ text: (text || '').trim(),
+ mediaType: mediaType || null,
+ mediaData: mediaData || null,
+ mediaName: mediaName || null,
+ time: Date.now(),
+ read: false
+ };
+ data.messages[convId].push(msg);
+ if (data.messages[convId].length > 500) data.messages[convId] = data.messages[convId].slice(-500);
+ saveData();
+ // Send to recipient (real-time)
+ const recipientEntry = Object.entries(data.onlineUsers).find(([, u]) => u.userId === toUserId);
+ if (recipientEntry) io.to(recipientEntry[0]).emit('dm_msg', { conversationId: convId, msg, from: fromUserId });
+ // Confirm to sender
+ socket.emit('dm_sent', { conversationId: convId, msg });
+ });
+
+ // انضمام لغرفة
+ socket.on('join_room', room => socket.join(room));
+
+ // دردشة غرفة عامة
+ socket.on('join_chat', room => socket.join(`chat:${room}`));
+ socket.on('leave_chat', room => socket.leave(`chat:${room}`));
+
+ // ======================================================
+ // 🎓 STUDY GROUP REAL-TIME ROOMS
+ // ======================================================
+ socket.on('join_study', groupId => {
+ socket.join('study:' + groupId);
+ });
+ socket.on('leave_study', groupId => {
+ socket.leave('study:' + groupId);
+ });
+
+ // Typing indicator for group chat
+ socket.on('study_typing', ({ groupId, name }) => {
+ socket.to('study:' + groupId).emit('study_typing', { name });
+ });
+
+ // ======================================================
+ // 🎙️ WebRTC SIGNALING - إشارة WebRTC للمكالمات
+ // ======================================================
+
+ // بدء مكالمة (إلى مستخدم محدد أو مجموعة)
+ socket.on('call_request', ({ to, from, fromName, type, groupId }) => {
+ // to: socket.id أو 'group:groupId'
+ if (groupId) {
+ socket.to('study:' + groupId).emit('call_request', { from: socket.id, fromName, type, groupId });
+ } else if (to) {
+ io.to(to).emit('call_request', { from: socket.id, fromName, type });
+ }
+ });
+
+ socket.on('call_accept', ({ to, groupId }) => {
+ if (groupId) {
+ socket.to('study:' + groupId).emit('call_accept', { from: socket.id });
+ } else {
+ io.to(to).emit('call_accept', { from: socket.id });
+ }
+ });
+
+ socket.on('call_reject', ({ to }) => {
+ io.to(to).emit('call_reject', { from: socket.id });
+ });
+
+ socket.on('call_end', ({ to, groupId }) => {
+ if (groupId) {
+ socket.to('study:' + groupId).emit('call_end', { from: socket.id });
+ } else if (to) {
+ io.to(to).emit('call_end', { from: socket.id });
+ }
+ });
+
+ // WebRTC SDP offer / answer / ICE candidates
+ socket.on('webrtc_offer', ({ to, offer, groupId }) => {
+ if (groupId) {
+ socket.to('study:' + groupId).emit('webrtc_offer', { from: socket.id, offer });
+ } else {
+ io.to(to).emit('webrtc_offer', { from: socket.id, offer });
+ }
+ });
+
+ socket.on('webrtc_answer', ({ to, answer }) => {
+ io.to(to).emit('webrtc_answer', { from: socket.id, answer });
+ });
+
+ socket.on('webrtc_ice', ({ to, candidate, groupId }) => {
+ if (groupId) {
+ socket.to('study:' + groupId).emit('webrtc_ice', { from: socket.id, candidate });
+ } else {
+ io.to(to).emit('webrtc_ice', { from: socket.id, candidate });
+ }
+ });
+
+ // ======================================================
+ // 💬 DM TYPING
+ // ======================================================
+ socket.on('dm_typing', ({ toUserId }) => {
+ // Find target socket
+ const targetEntry = Object.entries(data.onlineUsers).find(([sid, u]) => u.userId === toUserId);
+ if (targetEntry) {
+ io.to(targetEntry[0]).emit('dm_typing', { fromSocketId: socket.id });
+ }
+ });
+
+ // Ping لإبقاء المستخدم نشطاً
+ socket.on('ping_alive', () => {
+ if (data.onlineUsers[socket.id]) {
+ data.onlineUsers[socket.id].time = Date.now();
+ }
+ });
+});
+
+/* ============================================================
+ 🩸 بنك الدم
+ ============================================================ */
+// data.bloodRequests and data.bloodDonors initialized in loadData()
+
+app.get('/api/blood/requests', (req,res) => {
+ const { type, lat, lng, km=100 } = req.query;
+ let list = [...data.bloodRequests].sort((a,b)=>(b.time||b.ts||0)-(a.time||a.ts||0));
+ if (type) list = list.filter(r => r.bloodType === type);
+ if (lat && lng) list = list.map(r=>({...r, dist:Math.round(haversine(lat,lng,r.lat,r.lng))}))
+ .filter(r=>r.dist <= Number(km))
+ .sort((a,b)=>a.dist-b.dist);
+ res.json(list.slice(0,50));
+});
+
+app.get('/api/blood/donors', (req,res) => {
+ const { type, lat, lng, km=50 } = req.query;
+ let list = [...data.bloodDonors].filter(d=>d.available).sort((a,b)=>(b.time||b.ts||0)-(a.time||a.ts||0));
+ if (type) list = list.filter(d => d.bloodType === type);
+ if (lat && lng) list = list.map(d=>({...d, dist:Math.round(haversine(lat,lng,d.lat,d.lng))}))
+ .filter(d=>d.dist <= Number(km))
+ .sort((a,b)=>a.dist-b.dist);
+ res.json(list.slice(0,30));
+});
+
+app.post('/api/blood/request', (req,res) => {
+ const { bloodType, patientName, hospital, contact, area, lat, lng, urgent, userId } = req.body;
+ if (!bloodType || !contact) return res.status(400).json({error:'فصيلة الدم وجهة الاتصال مطلوبان'});
+ const req2 = {
+ id: uuidv4(), bloodType, patientName: patientName||'مريض', hospital: hospital||'',
+ contact, area: area||'غير محدد', lat: Number(lat)||0, lng: Number(lng)||0,
+ urgent: !!urgent, userId: userId||null, fulfilled: false,
+ time: Date.now()
+ };
+ data.bloodRequests.unshift(req2);
+ if (data.bloodRequests.length > 200) data.bloodRequests = data.bloodRequests.slice(0,200);
+ io.emit('new_blood_request', req2);
+ updateCitiesCount();
+ saveData();
+ res.json({success:true, data:req2});
+});
+
+app.post('/api/blood/donor', (req,res) => {
+ const { bloodType, name, contact, area, lat, lng, userId } = req.body;
+ if (!bloodType || !contact) return res.status(400).json({error:'فصيلة الدم وجهة الاتصال مطلوبان'});
+ const existing = data.bloodDonors.findIndex(d=>d.userId===userId||d.contact===contact);
+ const donor = {
+ id: uuidv4(), bloodType, name: name||'متبرع', contact,
+ area: area||'غير محدد', lat: Number(lat)||0, lng: Number(lng)||0,
+ userId: userId||null, available: true, time: Date.now()
+ };
+ if (existing>=0) { data.bloodDonors[existing] = {...data.bloodDonors[existing],...donor}; }
+ else { data.bloodDonors.unshift(donor); }
+ if (data.bloodDonors.length > 500) data.bloodDonors = data.bloodDonors.slice(0,500);
+ io.emit('new_blood_donor', donor);
+ saveData();
+ res.json({success:true, data:donor});
+});
+
+app.post('/api/blood/request/:id/fulfill', (req,res) => {
+ const r = data.bloodRequests.find(r=>r.id===req.params.id);
+ if (!r) return res.status(404).json({error:'الطلب غير موجود'});
+ r.fulfilled = true;
+ saveData();
+ io.emit('blood_fulfilled', {id:r.id});
+ res.json({success:true});
+});
+
+/* ============================================================
+ ⚡ جدول الكهرباء التشاركي
+ ============================================================ */
+// data.powerSchedule initialized in loadData()
+
+app.get('/api/power', (req,res) => {
+ const { area, lat, lng, km=30 } = req.query;
+ let list = [...data.powerSchedule].sort((a,b)=>(b.time||b.ts||0)-(a.time||a.ts||0));
+ if (area) list = list.filter(p => p.area.includes(area) || p.district.includes(area));
+ if (lat && lng) list = list.map(p=>({...p, dist:Math.round(haversine(lat,lng,p.lat||0,p.lng||0))}))
+ .filter(p=>p.dist <= Number(km));
+ res.json(list.slice(0,60));
+});
+
+app.post('/api/power', (req,res) => {
+ const { area, district, cutStart, cutEnd, lat, lng, userId } = req.body;
+ if (!area || !cutStart) return res.status(400).json({error:'المنطقة ووقت الانقطاع مطلوبان'});
+ const entry = {
+ id: uuidv4(), area, district: district||area,
+ cutStart, cutEnd: cutEnd||'غير محدد',
+ lat: Number(lat)||0, lng: Number(lng)||0,
+ status: 'مقطوع', votes: 1, confirms: 1, denies: 0,
+ userId: userId||null, time: Date.now()
+ };
+ data.powerSchedule.unshift(entry);
+ if (data.powerSchedule.length > 300) data.powerSchedule = data.powerSchedule.slice(0,300);
+ io.emit('new_power_report', entry);
+ saveData();
+ res.json({success:true, data:entry});
+});
+
+app.post('/api/power/:id/vote', (req,res) => {
+ const { vote } = req.body; // 'confirm' | 'deny'
+ const entry = data.powerSchedule.find(p=>p.id===req.params.id);
+ if (!entry) return res.status(404).json({error:'السجل غير موجود'});
+ if (vote==='confirm') { entry.confirms++; entry.votes++; }
+ else if (vote==='deny') { entry.denies++; }
+ if (entry.denies > entry.confirms * 2) entry.status = 'غير مؤكد';
+ else if (entry.confirms >= 3) entry.status = 'مؤكد';
+ io.emit('power_vote_update', {id:entry.id, confirms:entry.confirms, denies:entry.denies, status:entry.status});
+ saveData();
+ res.json({success:true, data:entry});
+});
+
+/* ============================================================
+ 🕌 أوقات الصلاة - خوارزمية PrayTimes (MWL / Umm Al-Qura)
+ ============================================================ */
+function calcPrayerTimes(lat, lng, date, method, tz) {
+ // Methods: 2=MWL, 4=Umm Al-Qura, 5=Egyptian, 3=ISNA
+ const M = {
+ 2: { fajr: 18.0, isha: 17.0 }, // MWL
+ 3: { fajr: 15.0, isha: 15.0 }, // ISNA
+ 4: { fajr: 18.5, ishaMin: 90 }, // Umm Al-Qura (isha = maghrib + 90 min)
+ 5: { fajr: 19.5, isha: 17.5 }, // Egyptian
+ };
+ const conf = M[Number(method)] || M[4];
+ const D2R = Math.PI / 180;
+ const d = date || new Date();
+
+ // Julian date
+ function julianDate(year, month, day) {
+ if (month <= 2) { year--; month += 12; }
+ const A = Math.floor(year / 100);
+ const B = 2 - A + Math.floor(A / 4);
+ return Math.floor(365.25 * (year + 4716)) + Math.floor(30.6001 * (month + 1)) + day + B - 1524.5;
+ }
+
+ const JD = julianDate(d.getFullYear(), d.getMonth() + 1, d.getDate());
+ const T = (JD - 2451545.0) / 36525.0;
+
+ // Sun position
+ const L0 = (280.46646 + 36000.76983 * T) % 360;
+ const M0 = (357.52911 + 35999.05029 * T - 0.0001537 * T * T) % 360;
+ const C = (1.914602 - 0.004817 * T - 0.000014 * T * T) * Math.sin(M0 * D2R)
+ + (0.019993 - 0.000101 * T) * Math.sin(2 * M0 * D2R)
+ + 0.000289 * Math.sin(3 * M0 * D2R);
+ const SunLon = L0 + C;
+ const omega = 125.04 - 1934.136 * T;
+ const lambda = SunLon - 0.00569 - 0.00478 * Math.sin(omega * D2R);
+ const epsilon = 23.0 + 26.0 / 60.0 + 21.448 / 3600.0
+ - (46.8150 / 3600.0) * T
+ - (0.00059 / 3600.0) * T * T
+ + (0.001813 / 3600.0) * T * T * T;
+ const epsilonApp = epsilon + 0.00256 * Math.cos(omega * D2R);
+
+ // Equation of time (minutes)
+ const y = Math.tan((epsilonApp / 2) * D2R) ** 2;
+ const L0r = L0 * D2R;
+ const M0r = M0 * D2R;
+ const eot = 4 * (y * Math.sin(2 * L0r)
+ - 2 * 0.016708634 * Math.sin(M0r)
+ + 4 * 0.016708634 * y * Math.sin(M0r) * Math.cos(2 * L0r)
+ - 0.5 * y * y * Math.sin(4 * L0r)
+ - 1.25 * 0.016708634 * 0.016708634 * Math.sin(2 * M0r)) * (180 / Math.PI);
+
+ // Declination
+ const decl = Math.asin(Math.sin(epsilonApp * D2R) * Math.sin(lambda * D2R)) / D2R;
+
+ // Timezone offset (use passed tz or compute from longitude)
+ const tzOffset = (tz !== undefined) ? Number(tz) : Math.round(lng / 15);
+ const noon = 12 + tzOffset - lng / 15 - eot / 60;
+
+ function hourAngle(altitude) {
+ const cosH = (Math.sin(altitude * D2R) - Math.sin(lat * D2R) * Math.sin(decl * D2R))
+ / (Math.cos(lat * D2R) * Math.cos(decl * D2R));
+ if (cosH < -1 || cosH > 1) return null;
+ return Math.acos(cosH) / D2R / 15;
+ }
+
+ function toTime(h) {
+ if (h === null || isNaN(h)) return '--:--';
+ h = ((h % 24) + 24) % 24;
+ const hh = Math.floor(h);
+ const mm = Math.round((h - hh) * 60);
+ const hh2 = mm === 60 ? hh + 1 : hh;
+ const mm2 = mm === 60 ? 0 : mm;
+ return String(hh2 % 24).padStart(2, '0') + ':' + String(mm2).padStart(2, '0');
+ }
+
+ const fajrHA = hourAngle(-conf.fajr);
+ const sunriseHA = hourAngle(-0.8333);
+ const asrHA = (function() {
+ const shadowFactor = 1; // Shafi'i (1), Hanafi (2)
+ const a = Math.atan(1 / (shadowFactor + Math.tan(Math.abs(lat - decl) * D2R))) / D2R;
+ const cosH = (Math.sin(a * D2R) - Math.sin(lat * D2R) * Math.sin(decl * D2R))
+ / (Math.cos(lat * D2R) * Math.cos(decl * D2R));
+ if (cosH < -1 || cosH > 1) return null;
+ return Math.acos(cosH) / D2R / 15;
+ })();
+ const ishaHA = conf.ishaMin ? null : hourAngle(-conf.isha);
+
+ const fajr = toTime(noon - (fajrHA || 1.5));
+ const sunrise = toTime(noon - (sunriseHA || 0.0889));
+ const dhuhr = toTime(noon + 0.0167); // +1 min
+ const asr = toTime(noon + (asrHA || 3.5));
+ const maghrib = toTime(noon + (sunriseHA || 0.0889));
+ const isha = conf.ishaMin
+ ? toTime(noon + (sunriseHA || 0.0889) + conf.ishaMin / 60)
+ : toTime(noon + (ishaHA || 1.5));
+
+ return { fajr, sunrise, dhuhr, asr, maghrib, isha, date: d.toLocaleDateString('ar-SA') };
+}
+
+app.get('/api/prayer', (req,res) => {
+ const { lat=15.5007, lng=32.5599, method=4, tz=3 } = req.query;
+ try {
+ const times = calcPrayerTimes(Number(lat), Number(lng), new Date(), method, Number(tz));
+ res.json({success:true, times, lat:Number(lat), lng:Number(lng)});
+ } catch(e) {
+ console.error('Prayer error:', e);
+ res.status(500).json({error:'خطأ في حساب الأوقات'});
+ }
+});
+
+/* ============================================================
+ 📸 رفع الصور (base64 — للعروض التجريبية)
+ ============================================================ */
+// data.images initialized in loadData()
+
+app.post('/api/upload/image', (req,res) => {
+ const { imageData, type='report', userId, refId } = req.body;
+ if (!imageData) return res.status(400).json({error:'البيانات مطلوبة'});
+ if (imageData.length > 2*1024*1024) return res.status(413).json({error:'الصورة كبيرة جداً (الحد 1.5 ميغابايت)'});
+ const id = uuidv4();
+ data.images[id] = { id, data: imageData, type, userId: userId||null, refId: refId||null, time: Date.now() };
+ // cleanup old images > 100
+ const keys = Object.keys(data.images);
+ if (keys.length > 100) { delete data.images[keys[0]]; }
+ res.json({success:true, imageId: id, url: '/api/image/'+id});
+});
+
+app.get('/api/image/:id', (req,res) => {
+ const img = data.images[req.params.id];
+ if (!img) return res.status(404).json({error:'الصورة غير موجودة'});
+ const matches = img.data.match(/^data:([^;]+);base64,(.+)$/);
+ if (!matches) return res.status(400).json({error:'بيانات غير صالحة'});
+ const buf = Buffer.from(matches[2], 'base64');
+ res.set('Content-Type', matches[1]);
+ res.set('Cache-Control', 'public, max-age=3600');
+ res.send(buf);
+});
+
+
+const PORT = process.env.PORT || 3000;
+server.listen(PORT, '0.0.0.0', () => {
+ const cities = GEO.sudan.reduce((s,st)=>s+st.cities.length,0);
+ const hoods = GEO.sudan.reduce((s,st)=>s+st.cities.reduce((ss,c)=>ss+c.hoods.length,0),0);
+ const world = GEO.world.length;
+ console.log(`🚀 نبض يعمل على المنفذ ${PORT}`);
+ console.log(`🇸🇩 ${GEO.sudan.length} ولاية | ${cities} مدينة | ${hoods} حي`);
+ console.log(`🌍 ${world} دولة عالمية`);
+ console.log(`✅ لا توجد بيانات وهمية - كل شيء حقيقي من المستخدمين`);
+ console.log(`🛒 P2P سوق + دردشة + تتبع مباشر مفعّل`);
+});
+
+/* ============================================================
+ 🏥 HOSPITALS - دليل المستشفيات
+ ============================================================ */
+app.get('/api/hospitals', (req,res) => {
+ const { area, type, lat, lng, r=30 } = req.query;
+ let list = [...data.hospitals].sort((a,b)=>(b.time||b.ts||0)-(a.time||a.ts||0));
+ if (area) list = list.filter(h => (h.area||'').includes(area));
+ if (type) list = list.filter(h => h.type === type);
+ if (lat && lng) {
+ list = list.map(h => ({ ...h, dist: h.lat && h.lng ? haversine(Number(lat),Number(lng),h.lat,h.lng) : 9999 }))
+ .filter(h => h.dist <= Number(r))
+ .sort((a,b) => a.dist - b.dist);
+ }
+ res.json(list.slice(0,60));
+});
+
+app.post('/api/hospitals', (req,res) => {
+ const { name, type='مستشفى', area, address, phone, emergency=false, lat, lng, services=[] } = req.body;
+ if (!name || !area) return res.status(400).json({error:'الاسم والمنطقة مطلوبان'});
+ const h = { id: uuidv4(), name: name.trim(), type, area: area.trim(), address: address||'', phone: phone||'',
+ emergency: !!emergency, lat: lat||null, lng: lng||null, services,
+ rating: 0, ratingCount: 0, userId: req.body.userId||null,
+ createdAt: new Date().toISOString(), time: Date.now() };
+ data.hospitals.unshift(h);
+ if (data.hospitals.length > 300) data.hospitals = data.hospitals.slice(0,300);
+ saveData();
+ io.emit('new_hospital', h);
+ res.json({success:true, id:h.id, hospital:h});
+});
+
+app.post('/api/hospitals/:id/rate', (req,res) => {
+ const h = data.hospitals.find(x=>x.id===req.params.id);
+ if (!h) return res.status(404).json({error:'غير موجود'});
+ const { rating } = req.body;
+ if (!rating || rating < 1 || rating > 5) return res.status(400).json({error:'تقييم 1-5'});
+ h.rating = ((h.rating * h.ratingCount) + Number(rating)) / (h.ratingCount + 1);
+ h.ratingCount++;
+ saveData();
+ res.json({success:true, rating: h.rating.toFixed(1), count: h.ratingCount});
+});
+
+/* ============================================================
+ 📰 NEWS - الأخبار المحلية
+ ============================================================ */
+app.get('/api/news', (req,res) => {
+ const { area, cat } = req.query;
+ let list = [...data.news].sort((a,b)=>(b.time||b.ts||0)-(a.time||a.ts||0));
+ if (area) list = list.filter(n=>(n.area||'').includes(area));
+ if (cat) list = list.filter(n=>n.category===cat);
+ res.json(list.slice(0,50));
+});
+
+app.post('/api/news', (req,res) => {
+ const { title, body, category='عام', area, lat, lng, source } = req.body;
+ if (!title||!body) return res.status(400).json({error:'العنوان والمحتوى مطلوبان'});
+ const item = { id: uuidv4(), title: title.trim(), body: body.trim(), category,
+ area: area||'', source: source||'مستخدم', lat: lat||null, lng: lng||null,
+ upvotes: 0, downvotes: 0, views: 0,
+ author: req.body.author||'مجهول', userId: req.body.userId||null,
+ createdAt: new Date().toISOString(), time: Date.now() };
+ data.news.unshift(item);
+ if (data.news.length > 200) data.news = data.news.slice(0,200);
+ saveData();
+ io.emit('new_news', item);
+ res.json({success:true, id:item.id, item});
+});
+
+app.post('/api/news/:id/vote', (req,res) => {
+ const item = data.news.find(x=>x.id===req.params.id);
+ if (!item) return res.status(404).json({error:'غير موجود'});
+ const { dir, vote } = req.body;
+ if (dir === 'up' || vote === 'credible') item.upvotes = (item.upvotes||0)+1;
+ else { item.downvotes = (item.downvotes||0)+1; item.notCredible = (item.notCredible||0)+1; }
+ saveData();
+ io.emit('news_vote', {id:item.id, upvotes:item.upvotes, downvotes:item.downvotes});
+ res.json({success:true, upvotes:item.upvotes, downvotes:item.downvotes});
+});
+
+/* ============================================================
+ 🚗 CARPOOLING - مشاركة التنقل
+ ============================================================ */
+app.get('/api/rides', (req,res) => {
+ const { from, to, date } = req.query;
+ let list = data.rides.filter(r=>r.status==='active').sort((a,b)=>(b.time||b.ts||0)-(a.time||a.ts||0));
+ if (from) list = list.filter(r=>(r.from||'').includes(from));
+ if (to) list = list.filter(r=>(r.to||'').includes(to));
+ if (date) list = list.filter(r=>r.date===date);
+ res.json(list.slice(0,60));
+});
+
+app.post('/api/rides', (req,res) => {
+ const { from, to, date, time: rtime, seats=1, price=0, currency='SDG', contact, notes, lat, lng } = req.body;
+ if (!from||!to||!contact) return res.status(400).json({error:'من/إلى/التواصل مطلوبة'});
+ const ride = { id: uuidv4(), from: from.trim(), to: to.trim(), date: date||'', time: rtime||'',
+ seats: Number(seats), seatsLeft: Number(seats), price: Number(price), currency,
+ contact: contact.trim(), notes: notes||'', lat: lat||null, lng: lng||null,
+ status: 'active', requests: [], userId: req.body.userId||null,
+ author: req.body.author||'مجهول', createdAt: new Date().toISOString(), postedAt: Date.now() };
+ data.rides.unshift(ride);
+ if (data.rides.length > 200) data.rides = data.rides.slice(0,200);
+ saveData();
+ io.emit('new_ride', ride);
+ res.json({success:true, id:ride.id, ride});
+});
+
+app.post('/api/rides/:id/request', (req,res) => {
+ const ride = data.rides.find(r=>r.id===req.params.id);
+ if (!ride) return res.status(404).json({error:'الرحلة غير موجودة'});
+ if (ride.seatsLeft < 1) return res.status(400).json({error:'لا توجد مقاعد متاحة'});
+ ride.seatsLeft = Math.max(0, ride.seatsLeft - 1);
+ if (ride.seatsLeft === 0) ride.status = 'full';
+ ride.requests.push({ name: req.body.name||'مستخدم', contact: req.body.contact||'', time: Date.now() });
+ saveData();
+ io.emit('ride_update', {id:ride.id, seatsLeft:ride.seatsLeft, status:ride.status});
+ res.json({success:true, seatsLeft:ride.seatsLeft});
+});
+
+/* ============================================================
+ 💧 WATER REPORTS - تقارير المياه
+ ============================================================ */
+app.get('/api/water', (req,res) => {
+ const { area, lat, lng, r=25 } = req.query;
+ let list = [...data.waterReports].sort((a,b)=>(b.time||b.ts||0)-(a.time||a.ts||0));
+ if (area) list = list.filter(w=>(w.area||'').includes(area));
+ if (lat && lng) {
+ list = list.map(w=>({...w, dist: w.lat&&w.lng ? haversine(Number(lat),Number(lng),w.lat,w.lng):9999}))
+ .filter(w=>w.dist<=Number(r)).sort((a,b)=>a.dist-b.dist);
+ }
+ res.json(list.slice(0,60));
+});
+
+app.post('/api/water', (req,res) => {
+ const { type='cut', area, duration, notes, lat, lng } = req.body;
+ if (!area) return res.status(400).json({error:'المنطقة مطلوبة'});
+ const item = { id: uuidv4(), type, area: area.trim(), duration: duration||'',
+ notes: notes||'', lat: lat||null, lng: lng||null,
+ upvotes: 1, downvotes: 0, status: 'active',
+ userId: req.body.userId||null, createdAt: new Date().toISOString(), time: Date.now() };
+ data.waterReports.unshift(item);
+ if (data.waterReports.length > 200) data.waterReports = data.waterReports.slice(0,200);
+ saveData(); io.emit('new_water_report', item);
+ res.json({success:true, id:item.id, item});
+});
+
+app.post('/api/water/:id/vote', (req,res) => {
+ const item = data.waterReports.find(x=>x.id===req.params.id);
+ if (!item) return res.status(404).json({error:'غير موجود'});
+ const dir = req.body.dir || req.body.vote;
+ if (dir === 'up' || dir === 'confirm') item.upvotes = (item.upvotes||0)+1;
+ else item.downvotes = (item.downvotes||0)+1;
+ saveData();
+ res.json({success:true, upvotes:item.upvotes, downvotes:item.downvotes});
+});
+
+/* ============================================================
+ 🎓 STUDY GROUPS - مجموعات التعلم
+ ============================================================ */
+app.get('/api/study', (req,res) => {
+ const groups = Object.values(data.studyGroups).sort((a,b)=>(b.time||b.ts||0)-(a.time||a.ts||0));
+ res.json(groups.slice(0,50));
+});
+
+app.post('/api/study', (req,res) => {
+ const { name, subject, level, area, maxMembers=20, schedule, contact } = req.body;
+ if (!name||!subject) return res.status(400).json({error:'اسم المجموعة والمادة مطلوبان'});
+ const id = uuidv4();
+ const userId = req.body.userId||null;
+ const group = { id, name: name.trim(), subject: subject.trim(), level: level||'عام',
+ area: area||'', maxMembers: Number(maxMembers), members: userId ? [userId] : [],
+ schedule: schedule||'', contact: contact||'', messages: [],
+ userId, author: req.body.author||'مجهول', createdAt: new Date().toISOString(), time: Date.now() };
+ data.studyGroups[id] = group;
+ saveData(); io.emit('new_study_group', group);
+ res.json({success:true, id, group});
+});
+
+app.post('/api/study/:id/join', (req,res) => {
+ const g = data.studyGroups[req.params.id];
+ if (!g) return res.status(404).json({error:'المجموعة غير موجودة'});
+ if (!Array.isArray(g.members)) g.members = [];
+ const uid = req.body.userId;
+ if (uid && !g.members.includes(uid)) {
+ if (g.members.length >= g.maxMembers) return res.status(400).json({error:'المجموعة ممتلئة'});
+ g.members.push(uid);
+ }
+ saveData();
+ io.emit('study_join', {id:g.id, members:g.members});
+ res.json({success:true, members:g.members});
+});
+
+// Join via invite token - MUST be before /:id routes
+app.post('/api/study/join-invite/:token', (req,res) => {
+ const g = Object.values(data.studyGroups).find(g=>g.inviteToken===req.params.token);
+ if (!g) return res.status(404).json({error:'رابط الدعوة غير صالح'});
+ if (!Array.isArray(g.members)) g.members=[];
+ const uid = req.body.userId;
+ if (uid && !g.members.includes(uid)) {
+ if (g.members.length >= (g.maxMembers||20)) return res.status(400).json({error:'المجموعة ممتلئة'});
+ g.members.push(uid);
+ }
+ saveData();
+ io.emit('study_join', {id:g.id, members:g.members});
+ res.json({success:true, group: g});
+});
+
+/* ============================================================
+ 📎 GROUP ADVANCED MSG (with media, replies, reactions)
+ NOTE: Must come BEFORE :id/msg to avoid route collision
+ ============================================================ */
+app.post('/api/study/:id/msg/advanced', (req,res) => {
+ const g = data.studyGroups[req.params.id];
+ if (!g) return res.status(404).json({error:'غير موجود'});
+ const { text='', author='عضو', userId=null, replyTo=null, mediaType=null, mediaData=null, mediaName=null } = req.body;
+ const msg = {
+ id: uuidv4(),
+ text: text.trim(),
+ author,
+ userId,
+ replyTo, // { id, text, author }
+ mediaType, // 'image' | 'video' | 'audio' | null
+ mediaData, // base64 data URL or null
+ mediaName,
+ reactions: {}, // { emoji: [userId, ...] }
+ createdAt: new Date().toISOString(),
+ time: Date.now()
+ };
+ if (!g.messages) g.messages = [];
+ g.messages.push(msg);
+ if (g.messages.length > 200) g.messages = g.messages.slice(-200);
+ saveData();
+ io.to('study:' + req.params.id).emit('study_msg', { groupId: req.params.id, msg });
+ res.json({ success: true, msg });
+});
+
+// Basic msg route (kept for backward compat, after advanced to avoid conflict)
+app.post('/api/study/:id/msg', (req,res) => {
+ const g = data.studyGroups[req.params.id];
+ if (!g) return res.status(404).json({error:'غير موجود'});
+ const { text='', author, name, userId=null, mediaType=null, mediaData=null, mediaName=null } = req.body;
+ const msg = {
+ id: uuidv4(),
+ text: text.trim(),
+ author: author || name || 'عضو',
+ userId,
+ mediaType: mediaType || null,
+ mediaData: mediaData || null,
+ mediaName: mediaName || null,
+ createdAt: new Date().toISOString(),
+ time: Date.now(),
+ reactions: {}
+ };
+ if (!g.messages) g.messages=[];
+ g.messages.push(msg);
+ if (g.messages.length>200) g.messages=g.messages.slice(-200);
+ saveData();
+ io.to('study:'+req.params.id).emit('study_msg', {groupId:req.params.id, msg});
+ res.json({success:true, msg});
+});
+
+// Messages alias
+app.get('/api/study/:id/messages', (req,res) => {
+ const g = data.studyGroups[req.params.id];
+ if (!g) return res.json([]);
+ res.json(g.messages || []);
+});
+
+app.post('/api/study/:id/message', (req,res) => {
+ const g = data.studyGroups[req.params.id];
+ if (!g) return res.status(404).json({error:'غير موجود'});
+ const msg = { id:uuidv4(), text:req.body.text||'', author:req.body.author||'عضو', userId:req.body.userId||null, createdAt:new Date().toISOString(), time:Date.now(), reactions:{} };
+ if (!g.messages) g.messages=[];
+ g.messages.push(msg);
+ if (g.messages.length>200) g.messages=g.messages.slice(-200);
+ saveData();
+ io.to('study:'+req.params.id).emit('study_msg', {groupId:req.params.id, msg});
+ res.json({success:true, msg});
+});
+
+// React to a message
+app.post('/api/study/:id/msg/:msgId/react', (req,res) => {
+ const g = data.studyGroups[req.params.id];
+ if (!g) return res.status(404).json({error:'غير موجود'});
+ const msg = (g.messages||[]).find(m=>m.id===req.params.msgId);
+ if (!msg) return res.status(404).json({error:'الرسالة غير موجودة'});
+ const { emoji, userId } = req.body;
+ if (!emoji||!userId) return res.status(400).json({error:'مطلوب'});
+ if (!msg.reactions) msg.reactions = {};
+ if (!msg.reactions[emoji]) msg.reactions[emoji] = [];
+ const idx = msg.reactions[emoji].indexOf(userId);
+ if (idx>=0) msg.reactions[emoji].splice(idx,1); else msg.reactions[emoji].push(userId);
+ if (msg.reactions[emoji].length===0) delete msg.reactions[emoji];
+ saveData();
+ io.to('study:'+req.params.id).emit('study_react', {groupId:req.params.id, msgId:req.params.msgId, reactions:msg.reactions});
+ res.json({success:true, reactions:msg.reactions});
+});
+
+// Get single group info
+app.get('/api/study/:id', (req,res) => {
+ const g = data.studyGroups[req.params.id];
+ if (!g) return res.status(404).json({error:'غير موجود'});
+ res.json(g);
+});
+
+// Generate invite link (returns token stored on group)
+app.post('/api/study/:id/invite', (req,res) => {
+ const g = data.studyGroups[req.params.id];
+ if (!g) return res.status(404).json({error:'غير موجود'});
+ if (!g.inviteToken) g.inviteToken = uuidv4().replace(/-/g,'').slice(0,12);
+ saveData();
+ res.json({success:true, token: g.inviteToken, groupId: g.id});
+});
+
+// Update group info
+app.put('/api/study/:id', (req,res) => {
+ const g = data.studyGroups[req.params.id];
+ if (!g) return res.status(404).json({error:'غير موجود'});
+ const allowed = ['name','subject','level','area','maxMembers','schedule','contact','description','avatar'];
+ allowed.forEach(k=>{ if (req.body[k]!==undefined) g[k]=req.body[k]; });
+ saveData();
+ io.emit('study_updated', g);
+ res.json({success:true, group:g});
+});
+
+// Leave group
+app.post('/api/study/:id/leave', (req,res) => {
+ const g = data.studyGroups[req.params.id];
+ if (!g) return res.status(404).json({error:'غير موجود'});
+ const uid = req.body.userId;
+ if (uid && Array.isArray(g.members)) g.members = g.members.filter(m=>m!==uid);
+ saveData();
+ io.emit('study_join', {id:g.id, members:g.members});
+ res.json({success:true, members:g.members});
+});
+
+/* ============================================================
+ 🎙️ WebRTC SIGNALING (Socket-based but REST fallback)
+ ============================================================ */
+// These are handled via socket.io events: webrtc_offer, webrtc_answer, webrtc_ice, call_request, call_accept, call_reject, call_end
+
+/* ============================================================
+ 📦 HELP REQUESTS - طلبات المساعدة
+ ============================================================ */
+app.get('/api/help', (req,res) => {
+ const { type, area } = req.query;
+ let list = data.helpRequests.filter(h=>h.status!=='closed').sort((a,b)=>(b.time||b.ts||0)-(a.time||a.ts||0));
+ if (type) list = list.filter(h=>h.type===type);
+ if (area) list = list.filter(h=>(h.area||'').includes(area));
+ res.json(list.slice(0,60));
+});
+
+app.post('/api/help', (req,res) => {
+ const { type='other', title, desc, area, contact, urgent=false, lat, lng } = req.body;
+ if (!title||!contact) return res.status(400).json({error:'العنوان والتواصل مطلوبان'});
+ const item = { id:uuidv4(), type, title:title.trim(), desc:desc||'',
+ area:area||'', contact:contact.trim(), urgent:!!urgent,
+ lat:lat||null, lng:lng||null, offers:0, status:'open',
+ author:req.body.author||'مجهول', userId:req.body.userId||null,
+ createdAt: new Date().toISOString(), time:Date.now() };
+ data.helpRequests.unshift(item);
+ if (data.helpRequests.length>200) data.helpRequests=data.helpRequests.slice(0,200);
+ saveData(); io.emit('new_help_request', item);
+ res.json({success:true, id:item.id, item});
+});
+
+app.post('/api/help/:id/offer', (req,res) => {
+ const item = data.helpRequests.find(x=>x.id===req.params.id);
+ if (!item) return res.status(404).json({error:'غير موجود'});
+ item.offers = (item.offers||0)+1;
+ saveData();
+ io.emit('help_offer', {id:item.id, offers:item.offers});
+ res.json({success:true, offers:item.offers});
+});
+
+app.post('/api/help/:id/close', (req,res) => {
+ const item = data.helpRequests.find(x=>x.id===req.params.id);
+ if (!item) return res.status(404).json({error:'غير موجود'});
+ item.status = 'closed';
+ data.stats.lives_saved = (data.stats.lives_saved||0)+1;
+ saveData();
+ io.emit('stats_update', data.stats);
+ res.json({success:true});
+});
+
+/* ============================================================
+ 🗳️ POLLS - استطلاعات الرأي
+ ============================================================ */
+app.get('/api/polls', (req,res) => {
+ res.json([...data.polls].sort((a,b)=>(b.time||b.ts||0)-(a.time||a.ts||0)).slice(0,30));
+});
+
+app.post('/api/polls', (req,res) => {
+ const { question, options=[], area } = req.body;
+ const expiresIn = Number(req.body.expiry || req.body.expiresIn || 24);
+ if (!question || options.length < 2) return res.status(400).json({error:'السؤال وخيارين على الأقل مطلوبان'});
+ const poll = { id:uuidv4(), question:question.trim(),
+ options: options.slice(0,8).map(o=>({text:String(o).trim(), votes:0})),
+ voters:{}, area:area||'', totalVotes:0,
+ expiresAt: Date.now()+(expiresIn*3600000),
+ userId:req.body.userId||null, author:req.body.author||'مجهول',
+ createdAt: new Date().toISOString(), time:Date.now() };
+ data.polls.unshift(poll);
+ if (data.polls.length>100) data.polls=data.polls.slice(0,100);
+ saveData(); io.emit('new_poll', poll);
+ res.json({success:true, id:poll.id, poll});
+});
+
+app.post('/api/polls/:id/vote', (req,res) => {
+ const poll = data.polls.find(x=>x.id===req.params.id);
+ if (!poll) return res.status(404).json({error:'غير موجود'});
+ if (poll.expiresAt < Date.now()) return res.status(400).json({error:'انتهى الاستطلاع'});
+ const idx = Number(req.body.optionIndex !== undefined ? req.body.optionIndex : req.body.option);
+ if (isNaN(idx)||!poll.options[idx]) return res.status(400).json({error:'خيار غير صالح'});
+ const uid = req.body.userId;
+ if (!poll.voters) poll.voters={};
+ if (uid && poll.voters[uid] !== undefined) return res.status(400).json({error:'لقد صوّتت مسبقاً'});
+ poll.options[idx].votes++;
+ poll.totalVotes = (poll.totalVotes||0)+1;
+ if (uid) poll.voters[uid] = idx;
+ saveData();
+ io.emit('poll_vote', {id:poll.id, options:poll.options, totalVotes:poll.totalVotes});
+ res.json({success:true, options:poll.options, totalVotes:poll.totalVotes});
+});
+
+/* ============================================================
+ 🌦️ WEATHER - الطقس (local estimation based on Sudan climate)
+ ============================================================ */
+app.get('/api/weather', (req, res) => {
+ const lat = parseFloat(req.query.lat) || 15.5;
+ const lng = parseFloat(req.query.lng) || 32.56;
+ // Sudan climate simulation based on month
+ const month = new Date().getMonth(); // 0-11
+ // Khartoum climate averages
+ const monthData = [
+ { temp:29, tempMax:35, tempMin:23, humidity:15, condition:'Clear', description:'صحو ومشمس' }, // Jan
+ { temp:31, tempMax:38, tempMin:24, humidity:12, condition:'Clear', description:'صحو ومشمس' }, // Feb
+ { temp:35, tempMax:42, tempMin:28, humidity:10, condition:'Clear', description:'حار وجاف' }, // Mar
+ { temp:39, tempMax:44, tempMin:33, humidity:10, condition:'Clear', description:'حار جداً' }, // Apr
+ { temp:41, tempMax:46, tempMin:36, humidity:12, condition:'Clear', description:'حار جداً' }, // May
+ { temp:39, tempMax:43, tempMin:34, humidity:25, condition:'Clouds', description:'غائم جزئياً' }, // Jun
+ { temp:35, tempMax:39, tempMin:30, humidity:50, condition:'Rain', description:'ممطر' }, // Jul
+ { temp:33, tempMax:37, tempMin:29, humidity:60, condition:'Rain', description:'ممطر' }, // Aug
+ { temp:34, tempMax:39, tempMin:29, humidity:45, condition:'Clouds', description:'غائم جزئياً' }, // Sep
+ { temp:36, tempMax:41, tempMin:31, humidity:22, condition:'Clear', description:'صحو' }, // Oct
+ { temp:33, tempMax:39, tempMin:27, humidity:15, condition:'Clear', description:'صحو لطيف' }, // Nov
+ { temp:29, tempMax:35, tempMin:22, humidity:14, condition:'Clear', description:'صحو ولطيف' }, // Dec
+ ];
+ const base = monthData[month];
+ // Add some variation based on location
+ const variation = Math.sin(lat * lng) * 2;
+ const sunrise = '06:' + String(20 + Math.floor(Math.abs(variation))).padStart(2,'0');
+ const sunset = '18:' + String(10 + Math.floor(Math.abs(variation))).padStart(2,'0');
+ // City name lookup (simple)
+ const cities = {
+ 'الخرطوم': [15.5, 32.56], 'أم درمان': [15.64, 32.48], 'الخرطوم بحري': [15.61, 32.55],
+ 'بورتسودان': [19.6, 37.22], 'كسلا': [15.45, 36.4], 'القضارف': [14.03, 35.39],
+ 'ود مدني': [14.4, 33.52], 'عطبرة': [17.7, 33.97], 'الأبيض': [13.18, 30.22]
+ };
+ let city = 'الخرطوم';
+ let minDist = 999;
+ Object.entries(cities).forEach(([name, [clat, clng]]) => {
+ const d = Math.sqrt(Math.pow(lat-clat,2) + Math.pow(lng-clng,2));
+ if (d < minDist) { minDist = d; city = name; }
+ });
+ res.json({
+ city,
+ temp: Math.round(base.temp + variation),
+ tempMax: Math.round(base.tempMax + variation),
+ tempMin: Math.round(base.tempMin + variation),
+ feelsLike: Math.round(base.temp + variation + 2),
+ humidity: base.humidity,
+ windSpeed: Math.round(10 + Math.abs(variation) * 3),
+ visibility: 10000,
+ condition: base.condition,
+ description: base.description,
+ sunrise,
+ sunset,
+ source: 'estimate'
+ });
+});
+
+/* ============================================================
+ 📊 STATS DASHBOARD - لوحة الإحصاءات
+ ============================================================ */
+app.get('/api/dashboard', (_,res) => {
+ const now = Date.now();
+ const day = 24*3600000;
+ const topAreas = (() => {
+ const all = [
+ ...data.alerts, ...data.marketplace, ...data.medicines,
+ ...data.voiceItems, ...(data.helpRequests||[]), ...(data.news||[])
+ ].map(x=>x.area||x.district).filter(Boolean);
+ const cnt = {};
+ all.forEach(a=>{ cnt[a]=(cnt[a]||0)+1; });
+ return Object.entries(cnt).sort((a,b)=>b[1]-a[1]).slice(0,10).map(([area,count])=>({area,count}));
+ })();
+ const onlineCount = Object.keys(data.onlineUsers||{}).length;
+ res.json({
+ stats: {
+ online: onlineCount,
+ users: data.stats.users || onlineCount,
+ reports: data.stats.reports,
+ lives: data.stats.lives_saved,
+ cities: data.stats.cities,
+ exchange: data.exchangeRates.length,
+ medicines: data.medicines.length,
+ voice: data.voiceItems.length,
+ skills: data.skills.length,
+ market: data.marketplace.length,
+ bloodDonors: data.bloodDonors.length,
+ power: data.powerSchedule.length,
+ hospitals: (data.hospitals||[]).length,
+ news: (data.news||[]).length,
+ rides: (data.rides||[]).filter(r=>r.status==='active').length,
+ water: (data.waterReports||[]).length,
+ study: Object.keys(data.studyGroups||{}).length,
+ help: (data.helpRequests||[]).filter(h=>h.status!=='closed').length,
+ polls: (data.polls||[]).filter(p=>p.expiresAt>now).length,
+ },
+ topAreas: topAreas.slice(0,10).map(({area,count})=>({name:area,count})),
+ last24h: {
+ '🗺️ بلاغات': data.alerts.filter(x=>x.time>now-day).length,
+ '🛒 إعلانات': data.marketplace.filter(x=>x.time>now-day).length,
+ '📰 أخبار': (data.news||[]).filter(x=>x.time>now-day).length,
+ '📦 مساعدة': (data.helpRequests||[]).filter(x=>x.time>now-day).length,
+ }
+ });
+});
+
+/* ================================================================
+ 🔥 VIRAL FEATURES SERVER ENDPOINTS
+ نقاط | متصدرون | إحالة | إحصاءات حية | بلاغات فيروسية
+================================================================ */
+
+// ── In-memory leaderboard store (persisted to data.leaderboard) ─
+if (!data.leaderboard) data.leaderboard = [];
+if (!data.referrals) data.referrals = [];
+if (!data.pointEvents) data.pointEvents = [];
+
+// ── GET /api/leaderboard ──────────────────────────────────────
+app.get('/api/leaderboard', (req, res) => {
+ const tab = req.query.tab || 'weekly';
+ const limit = parseInt(req.query.limit) || 50;
+ const now = Date.now();
+ const week = 7 * 24 * 3600 * 1000;
+
+ let list = [...(data.leaderboard || [])];
+
+ if (tab === 'weekly') {
+ list = list.filter(u => u.lastActivity && now - u.lastActivity < week);
+ list.sort((a, b) => (b.weekPts || 0) - (a.weekPts || 0));
+ } else if (tab === 'city') {
+ const userId = req.query.userId;
+ const user = userId && data.leaderboard.find(u => u.userId === userId);
+ const city = user ? user.area : null;
+ if (city) list = list.filter(u => u.area === city);
+ list.sort((a, b) => (b.pts || 0) - (a.pts || 0));
+ } else {
+ list.sort((a, b) => (b.pts || 0) - (a.pts || 0));
+ }
+
+ const trimmed = list.slice(0, limit).map((u, i) => ({
+ rank: i + 1,
+ userId: u.userId,
+ name: u.name || 'مستخدم',
+ area: u.area || '',
+ pts: tab === 'weekly' ? (u.weekPts || 0) : (u.pts || 0),
+ avatar: u.avatar || null,
+ badges: u.badges || []
+ }));
+
+ const userId = req.query.userId;
+ const myEntry = userId ? data.leaderboard.find(u => u.userId === userId) : null;
+ const myRank = myEntry ? {
+ rank: list.findIndex(u => u.userId === userId) + 1,
+ pts: tab === 'weekly' ? (myEntry.weekPts || 0) : (myEntry.pts || 0)
+ } : null;
+
+ res.json({ list: trimmed, myRank, total: list.length });
+});
+
+// ── POST /api/points/add ───────────────────────────────────────
+app.post('/api/points/add', (req, res) => {
+ const { userId, action, pts, name, area, avatar } = req.body;
+ if (!userId || !pts) return res.json({ ok: false });
+
+ if (!data.leaderboard) data.leaderboard = [];
+ let user = data.leaderboard.find(u => u.userId === userId);
+ if (!user) {
+ user = { userId, name: name || 'مستخدم', area: area || '', pts: 0, weekPts: 0, badges: [], lastActivity: Date.now() };
+ data.leaderboard.push(user);
+ }
+ user.pts = (user.pts || 0) + pts;
+ user.weekPts = (user.weekPts || 0) + pts;
+ user.lastActivity = Date.now();
+ if (name) user.name = name;
+ if (area) user.area = area;
+ if (avatar) user.avatar = avatar;
+
+ // Keep leaderboard lean
+ if (data.leaderboard.length > 5000) {
+ data.leaderboard.sort((a, b) => (b.pts||0)-(a.pts||0));
+ data.leaderboard = data.leaderboard.slice(0, 2000);
+ }
+
+ saveData();
+ io.emit('points_update', { userId, pts: user.pts, weekPts: user.weekPts });
+ res.json({ ok: true, total: user.pts, weekPts: user.weekPts });
+});
+
+// ── POST /api/referral ─────────────────────────────────────────
+app.post('/api/referral', (req, res) => {
+ const { ref, newUser } = req.body;
+ if (!ref || !newUser) return res.json({ ok: false });
+
+ // Find referrer user by partial id
+ const referrer = data.leaderboard ? data.leaderboard.find(u => u.userId && u.userId.startsWith(ref)) : null;
+ if (!referrer) return res.json({ ok: false, msg: 'referrer not found' });
+
+ // Prevent duplicate rewards
+ if (!data.referrals) data.referrals = [];
+ if (data.referrals.find(r => r.ref === ref && r.newUser === newUser)) {
+ return res.json({ ok: false, msg: 'already rewarded' });
+ }
+
+ data.referrals.push({ ref, newUser, time: Date.now() });
+ referrer.pts = (referrer.pts || 0) + 20;
+ referrer.weekPts = (referrer.weekPts || 0) + 20;
+ saveData();
+ io.emit('points_update', { userId: referrer.userId, pts: referrer.pts });
+ res.json({ ok: true, referrerName: referrer.name });
+});
+
+// ── GET /api/stats/live ────────────────────────────────────────
+app.get('/api/stats/live', (req, res) => {
+ const now = Date.now();
+ const hour = 3600 * 1000;
+ const day = 24 * hour;
+
+ const activeAlerts = data.alerts.filter(a => now - (a.time||0) < 2*hour).length;
+ const todayReports = data.alerts.filter(a => now - (a.time||0) < day).length;
+ const activeZones = new Set(data.alerts.filter(a => now - (a.time||0) < 2*hour).map(a => a.area).filter(Boolean)).size;
+
+ // Find trending topic (most common area in last 2h)
+ const areaCounts = {};
+ data.alerts.filter(a => now - (a.time||0) < 2*hour).forEach(a => {
+ if (a.area) areaCounts[a.area] = (areaCounts[a.area]||0) + 1;
+ });
+ const trending = Object.entries(areaCounts).sort((a,b)=>b[1]-a[1])[0];
+
+ res.json({
+ online: data.stats.users || 0,
+ users: data.stats.users || 0,
+ todayReports,
+ activeAlerts,
+ activeZones,
+ lives_saved: data.stats.lives_saved || 0,
+ trending: trending ? trending[0] : '---',
+ cities: data.stats.cities || 0
+ });
+});
+
+// ── GET /api/alerts/viral ──────────────────────────────────────
+app.get('/api/alerts/viral', (req, res) => {
+ const now = Date.now();
+ const day = 24 * 3600 * 1000;
+
+ // Score = votes*3 + shares*5 + views*0.5
+ const scored = data.alerts
+ .filter(a => now - (a.time||0) < day)
+ .map(a => ({
+ ...a,
+ _score: (a.votes||0)*3 + (a.shares||0)*5 + (a.views||0)*0.5
+ }))
+ .sort((a, b) => b._score - a._score)
+ .slice(0, 10)
+ .map(({ _score, ...a }) => a);
+
+ res.json(scored);
+});
+
+// ── POST /api/alerts/:id/view ──────────────────────────────────
+app.post('/api/alerts/:id/view', (req, res) => {
+ const alert = data.alerts.find(a => a.id === req.params.id);
+ if (!alert) return res.json({ ok: false });
+ alert.views = (alert.views || 0) + 1;
+ saveData();
+ res.json({ ok: true, views: alert.views });
+});
+
+// ── POST /api/alerts/:id/share ─────────────────────────────────
+app.post('/api/alerts/:id/share', (req, res) => {
+ const alert = data.alerts.find(a => a.id === req.params.id);
+ if (!alert) return res.json({ ok: false });
+ alert.shares = (alert.shares || 0) + 1;
+ saveData();
+ io.emit('alert_shared', { id: alert.id, shares: alert.shares });
+ res.json({ ok: true, shares: alert.shares });
+});
+
+// ── Weekly reset (every Monday midnight) ─────────────────────
+setInterval(() => {
+ const d = new Date();
+ if (d.getDay() === 1 && d.getHours() === 0 && d.getMinutes() < 5) {
+ if (data.leaderboard) {
+ data.leaderboard.forEach(u => { u.weekPts = 0; });
+ saveData();
+ io.emit('leaderboard_reset', { msg: 'تمت إعادة تعيين نقاط الأسبوع!' });
+ }
+ }
+}, 5 * 60 * 1000); // check every 5 min
+
+
+/* ============================================================
+ 🚀 ADVANCED FEATURES v7 — نبض المستقبل
+ ============================================================ */
+
+// ── Global Search API ──────────────────────────────────────────
+app.get('/api/search', rateLimit(60, 60000), (req, res) => {
+ const q = (req.query.q || '').trim().toLowerCase();
+ if (!q || q.length < 2) return res.json({ results: [] });
+ const results = [];
+ // Alerts
+ data.alerts.filter(a => (a.msg||'').toLowerCase().includes(q) || (a.area||'').toLowerCase().includes(q))
+ .slice(0,5).forEach(a => results.push({ type:'alert', icon:a.icon||'🔴', title:a.msg, sub:a.area, id:a.id, section:'map' }));
+ // Market
+ data.marketplace.filter(m => (m.title||'').toLowerCase().includes(q) || (m.desc||'').toLowerCase().includes(q))
+ .slice(0,5).forEach(m => results.push({ type:'market', icon:'🛒', title:m.title, sub:m.area||'', id:m.id, section:'market' }));
+ // Medicine
+ data.medicines.filter(m => (m.name||'').toLowerCase().includes(q) || (m.pharmacy||'').toLowerCase().includes(q))
+ .slice(0,3).forEach(m => results.push({ type:'medicine', icon:'💊', title:m.name, sub:m.pharmacy||'', section:'medicine' }));
+ // News
+ (data.news||[]).filter(n => (n.title||n.text||'').toLowerCase().includes(q))
+ .slice(0,3).forEach(n => results.push({ type:'news', icon:'📰', title:n.title||n.text, sub:n.area||'', section:'news' }));
+ // Blood donors
+ data.bloodDonors.filter(d => (d.name||'').toLowerCase().includes(q) || (d.type||'').toLowerCase().includes(q))
+ .slice(0,3).forEach(d => results.push({ type:'blood', icon:'🩸', title:d.name+' — '+d.type, sub:d.area||'', section:'blood' }));
+ // People profiles
+ Object.values(data.profiles).filter(p => (p.name||'').toLowerCase().includes(q) || (p.area||'').toLowerCase().includes(q))
+ .slice(0,5).forEach(p => results.push({ type:'person', icon:'👤', title:p.name||'مستخدم', sub:p.area||'', userId:p.userId, section:'people' }));
+ res.json({ results: results.slice(0, 20) });
+});
+
+// ── Notifications Center API ────────────────────────────────────
+if (!data.notifications) data.notifications = {};
+app.get('/api/notifications/:userId', (req, res) => {
+ const notifs = (data.notifications[req.params.userId] || []).slice(-50).reverse();
+ res.json(notifs);
+});
+app.post('/api/notifications/:userId/read', (req, res) => {
+ if (data.notifications[req.params.userId]) {
+ data.notifications[req.params.userId].forEach(n => n.read = true);
+ saveData();
+ }
+ res.json({ ok: true });
+});
+app.delete('/api/notifications/:userId', (req, res) => {
+ data.notifications[req.params.userId] = [];
+ saveData();
+ res.json({ ok: true });
+});
+
+// ── Market Advanced Search ──────────────────────────────────────
+app.get('/api/market/search', rateLimit(120, 60000), (req, res) => {
+ const { q='', type='all', cat='all', minPrice, maxPrice, area='', sort='newest' } = req.query;
+ let list = [...data.marketplace];
+ if (q) list = list.filter(m => (m.title||'').toLowerCase().includes(q.toLowerCase()) || (m.desc||'').toLowerCase().includes(q.toLowerCase()));
+ if (type !== 'all') list = list.filter(m => m.type === type);
+ if (cat !== 'all') list = list.filter(m => m.category === cat);
+ if (area) list = list.filter(m => (m.area||'').includes(area));
+ if (minPrice) list = list.filter(m => parseFloat(m.price||0) >= parseFloat(minPrice));
+ if (maxPrice) list = list.filter(m => parseFloat(m.price||0) <= parseFloat(maxPrice));
+ if (sort === 'newest') list.sort((a,b) => (b.time||0)-(a.time||0));
+ else if (sort === 'price_asc') list.sort((a,b) => parseFloat(a.price||0)-parseFloat(b.price||0));
+ else if (sort === 'price_desc') list.sort((a,b) => parseFloat(b.price||0)-parseFloat(a.price||0));
+ else if (sort === 'popular') list.sort((a,b) => (b.likes||0)-(a.likes||0));
+ res.json(list.slice(0, 50));
+});
+
+// ── Blood Emergency Notify ──────────────────────────────────────
+app.post('/api/blood/emergency', rateLimit(5, 60000), (req, res) => {
+ const { type, area, hospital, contactPhone, urgent=true } = req.body;
+ if (!type) return res.status(400).json({ error: 'فصيلة الدم مطلوبة' });
+ const alert = {
+ id: uuidv4(),
+ type: 'blood_emergency',
+ bloodType: type,
+ area: area || 'غير محدد',
+ hospital,
+ contactPhone,
+ urgent,
+ time: Date.now()
+ };
+ io.emit('blood_emergency', alert);
+ // Notify matching donors
+ const donors = data.bloodDonors.filter(d => d.type === type);
+ donors.forEach(d => {
+ if (d.userId && data.onlineUsers) {
+ const sock = Object.values(data.onlineUsers).find(u => u.userId === d.userId);
+ if (sock) io.to(sock.socketId).emit('blood_needed', alert);
+ }
+ });
+ res.json({ ok: true, notified: donors.length });
+});
+
+// ── Live Stats SSE endpoint ──────────────────────────────────────
+app.get('/api/stats/stream', (req, res) => {
+ res.set({
+ 'Content-Type': 'text/event-stream',
+ 'Cache-Control': 'no-cache',
+ 'Connection': 'keep-alive',
+ 'Access-Control-Allow-Origin': '*'
+ });
+ res.flushHeaders();
+ const send = () => {
+ const stats = {
+ online: Object.keys(data.onlineUsers||{}).length,
+ reports: data.stats.reports,
+ lives: data.stats.lives_saved,
+ cities: data.stats.cities,
+ market: data.marketplace.length,
+ blood: data.bloodDonors.length,
+ ts: Date.now()
+ };
+ res.write(`data: ${JSON.stringify(stats)}\n\n`);
+ };
+ send();
+ const interval = setInterval(send, 15000);
+ req.on('close', () => clearInterval(interval));
+});
+
+// ── Enhanced Alerts with media ──────────────────────────────────
+app.get('/api/alerts/filter', rateLimit(60, 60000), (req, res) => {
+ const { type='all', area='', hours=24, sort='newest', limit=30 } = req.query;
+ const cutoff = Date.now() - (parseFloat(hours)*3600000);
+ let list = data.alerts.filter(a => (a.time||0) >= cutoff);
+ if (type !== 'all') list = list.filter(a => a.type === type);
+ if (area) list = list.filter(a => (a.area||'').includes(area));
+ if (sort === 'votes') list.sort((a,b) => (b.votes||0)-(a.votes||0));
+ else if (sort === 'views') list.sort((a,b) => (b.views||0)-(a.views||0));
+ else list.sort((a,b) => (b.time||0)-(a.time||0));
+ res.json(list.slice(0, parseInt(limit)));
+});
+
+// ── News categories ──────────────────────────────────────────────
+app.get('/api/news/categories', (req, res) => {
+ const cats = {};
+ (data.news||[]).forEach(n => { const c = n.category||'عام'; cats[c]=(cats[c]||0)+1; });
+ res.json(Object.entries(cats).map(([cat,count]) => ({ cat, count })).sort((a,b)=>b.count-a.count));
+});
+
+// ── Trending Topics ──────────────────────────────────────────────
+app.get('/api/trending', (req, res) => {
+ const now = Date.now();
+ const h6 = 6 * 3600000;
+ const recent = [
+ ...data.alerts.filter(a => now-(a.time||0)({ text:a.msg, type:'alert', score:(a.votes||0)*3+(a.views||0) })),
+ ...(data.news||[]).filter(n => now-(n.time||0)({ text:n.title||n.text, type:'news', score:(n.upvotes||0)*2+(n.views||0) })),
+ ...(data.voiceItems||[]).filter(v => now-(v.time||0)({ text:v.text, type:'voice', score:v.votes||0 }))
+ ].sort((a,b)=>b.score-a.score).slice(0,10);
+ res.json(recent);
+});
+
+// ── User Activity Feed ────────────────────────────────────────────
+app.get('/api/feed/:userId', rateLimit(30, 60000), (req, res) => {
+ const userId = req.params.userId;
+ const profile = data.profiles[userId];
+ const area = profile?.area || '';
+ const now = Date.now();
+ const day = 24*3600000;
+ let feed = [];
+ // Nearby alerts
+ data.alerts.filter(a => now-(a.time||0) feed.push({ type:'alert', icon:a.icon||'🔴', title:a.msg, sub:a.area, time:a.time, id:a.id }));
+ // Market items
+ data.marketplace.filter(m => now-(m.time||0) feed.push({ type:'market', icon:'🛒', title:m.title, sub:m.area, time:m.time, id:m.id }));
+ // News
+ (data.news||[]).filter(n => now-(n.time||0) feed.push({ type:'news', icon:'📰', title:n.title||n.text, sub:n.area, time:n.time, id:n.id }));
+ feed.sort((a,b)=>(b.time||0)-(a.time||0));
+ res.json(feed.slice(0, 20));
+});
+
+// ── Health Check endpoint ─────────────────────────────────────────
+app.get('/health', (_, res) => {
+ res.json({
+ status: 'ok',
+ version: '7.0',
+ uptime: process.uptime(),
+ memory: Math.round(process.memoryUsage().heapUsed/1024/1024) + 'MB',
+ online: Object.keys(data.onlineUsers||{}).length,
+ ts: new Date().toISOString()
+ });
+});
+
+// ── Bulk data prefetch for PWA ────────────────────────────────────
+app.get('/api/prefetch', (req, res) => {
+ const now = Date.now();
+ const day = 24*3600000;
+ res.json({
+ alerts: data.alerts.filter(a=>now-(a.time||0) {
+ const now = Date.now();
+ const month = 30 * 24 * 3600000;
+ const week = 7 * 24 * 3600000;
+ let changed = false;
+ // Remove alerts older than 30 days
+ const alertsBefore = data.alerts.length;
+ data.alerts = data.alerts.filter(a => now - (a.time||0) < month);
+ if (data.alerts.length !== alertsBefore) changed = true;
+ // Remove market items older than 30 days
+ data.marketplace = data.marketplace.filter(m => now - (m.time||0) < month);
+ // Remove expired polls
+ if (data.polls) data.polls = data.polls.filter(p => !p.expiresAt || new Date(p.expiresAt) > new Date(now - week));
+ if (changed) saveData();
+}, 6 * 3600000); // every 6 hours
+
+/* ============================================================
+ 🚀 ADVANCED FEATURES v7.0 - Final additions
+============================================================ */
+
+// ── User stats by area ────────────────────────────────────────
+app.get('/api/stats/areas', (req, res) => {
+ const areaCounts = {};
+ data.alerts.forEach(a => {
+ const area = (a.area || '').split(' ')[0];
+ if (area) areaCounts[area] = (areaCounts[area] || 0) + 1;
+ });
+ const sorted = Object.entries(areaCounts)
+ .sort((a, b) => b[1] - a[1])
+ .slice(0, 10)
+ .map(([area, count]) => ({ area, count }));
+ res.json(sorted);
+});
+
+// ── Get online users count ────────────────────────────────────
+app.get('/api/users/count', (req, res) => {
+ res.json({ online: Object.keys(data.onlineUsers || {}).length, total: Object.keys(data.profiles || {}).length });
+});
+
+// ── Search alerts ─────────────────────────────────────────────
+app.get('/api/alerts/search', (req, res) => {
+ const q = (req.query.q || '').toLowerCase().trim();
+ if (!q || q.length < 2) return res.json([]);
+ const results = data.alerts
+ .filter(a => (a.msg || '').toLowerCase().includes(q) || (a.area || '').toLowerCase().includes(q))
+ .slice(0, 20);
+ res.json(results);
+});
+
+// ── Market categories ─────────────────────────────────────────
+app.get('/api/market/categories', (req, res) => {
+ const cats = {};
+ data.marketplace.forEach(m => { const c = m.category || m.type || 'عام'; cats[c] = (cats[c] || 0) + 1; });
+ res.json(Object.entries(cats).map(([cat, count]) => ({ cat, count })).sort((a, b) => b.count - a.count));
+});
+
+// ── Quick stats for topbar ────────────────────────────────────
+app.get('/api/stats/quick', (req, res) => {
+ const now = Date.now();
+ const day = 24 * 3600000;
+ const todayAlerts = data.alerts.filter(a => now - (a.time || 0) < day).length;
+ const usdRate = data.exchangeRates.find(r => (r.currency || '').includes('دولار') || (r.currency || '').toLowerCase() === 'usd');
+ res.json({
+ users: Object.keys(data.onlineUsers || {}).length,
+ reports: data.alerts.length,
+ todayReports: todayAlerts,
+ lives_saved: data.stats?.lives_saved || 0,
+ cities: data.stats?.cities || 1,
+ usdRate: usdRate ? (usdRate.buy || usdRate.rate || 0) : 0
+ });
+});
+
+// ── Save/bookmark an alert (local profile feature) ───────────
+app.post('/api/alerts/:id/bookmark', rateLimit(20, 60000), (req, res) => {
+ const { userId } = req.body;
+ const alert = data.alerts.find(a => a.id === req.params.id);
+ if (!alert) return res.status(404).json({ error: 'not found' });
+ if (userId && data.profiles[userId]) {
+ if (!data.profiles[userId].bookmarks) data.profiles[userId].bookmarks = [];
+ const idx = data.profiles[userId].bookmarks.indexOf(req.params.id);
+ if (idx === -1) data.profiles[userId].bookmarks.push(req.params.id);
+ else data.profiles[userId].bookmarks.splice(idx, 1);
+ saveData();
+ res.json({ ok: true, bookmarked: idx === -1 });
+ } else {
+ res.json({ ok: true });
+ }
+});
+
+// ── Get user bookmarks ────────────────────────────────────────
+app.get('/api/profile/:userId/bookmarks', rateLimit(30, 60000), (req, res) => {
+ const profile = data.profiles[req.params.userId];
+ if (!profile) return res.json([]);
+ const bookmarkIds = profile.bookmarks || [];
+ const bookmarked = data.alerts.filter(a => bookmarkIds.includes(a.id));
+ res.json(bookmarked);
+});
+
+// ── Report abuse/spam ─────────────────────────────────────────
+app.post('/api/alerts/:id/report-abuse', rateLimit(5, 60000), (req, res) => {
+ const alert = data.alerts.find(a => a.id === req.params.id);
+ if (!alert) return res.status(404).json({ error: 'not found' });
+ alert.abuseReports = (alert.abuseReports || 0) + 1;
+ // Auto-hide if 5+ abuse reports
+ if (alert.abuseReports >= 5) alert.hidden = true;
+ saveData();
+ res.json({ ok: true, reports: alert.abuseReports });
+});
+
+// ── Get market item by ID ─────────────────────────────────────
+app.get('/api/market/:id', (req, res) => {
+ const item = data.marketplace.find(m => m.id === req.params.id);
+ if (!item) return res.status(404).json({ error: 'not found' });
+ item.views = (item.views || 0) + 1;
+ res.json(item);
+});
+
+// ── Update market item (seller only) ──────────────────────────
+app.put('/api/market/:id', rateLimit(10, 60000), (req, res) => {
+ const { userId, title, price, desc } = req.body;
+ const item = data.marketplace.find(m => m.id === req.params.id);
+ if (!item) return res.status(404).json({ error: 'not found' });
+ if (item.userId !== userId) return res.status(403).json({ error: 'غير مصرح' });
+ if (title) item.title = title.substring(0, 80);
+ if (price !== undefined) item.price = price;
+ if (desc) item.desc = desc.substring(0, 300);
+ item.updatedAt = Date.now();
+ saveData();
+ res.json({ ok: true, item });
+});
+
+// ── Delete market item ────────────────────────────────────────
+app.delete('/api/market/:id', rateLimit(5, 60000), (req, res) => {
+ const { userId } = req.body;
+ const idx = data.marketplace.findIndex(m => m.id === req.params.id && m.userId === userId);
+ if (idx === -1) return res.status(403).json({ error: 'غير مصرح أو غير موجود' });
+ data.marketplace.splice(idx, 1);
+ saveData();
+ res.json({ ok: true });
+});
+
+// ── User profile public view (enhanced) ──────────────────────
+app.get('/api/profile/:userId', rateLimit(60, 60000), (req, res) => {
+ const p = data.profiles[req.params.userId];
+ if (!p) return res.status(404).json({ error: 'not found' });
+ // Return public fields only
+ res.json({
+ id: req.params.userId,
+ name: p.name || 'مستخدم',
+ avatar: p.avatar,
+ area: p.area,
+ jobTitle: p.jobTitle,
+ company: p.company,
+ bio: p.bio,
+ publicPhone: p.publicPhone,
+ website: p.website,
+ reports: p.reports || 0,
+ points: p.points || 0,
+ badges: p.badges || [],
+ joinedAt: p.joinedAt,
+ lastSeen: p.lastSeen
+ });
+});
+
+// ── Nearby alerts with radius ─────────────────────────────────
+app.get('/api/alerts/radius', (req, res) => {
+ const { lat, lng, km } = req.query;
+ if (!lat || !lng) return res.json(data.alerts.slice(0, 30));
+ const R = 6371;
+ const lat1 = parseFloat(lat) * Math.PI / 180;
+ const lng1 = parseFloat(lng) * Math.PI / 180;
+ const maxKm = parseFloat(km) || 50;
+ const nearby = data.alerts.filter(a => {
+ if (!a.lat || !a.lng) return false;
+ const lat2 = a.lat * Math.PI / 180;
+ const lng2 = a.lng * Math.PI / 180;
+ const dLat = lat2 - lat1, dLng = lng2 - lng1;
+ const x = Math.sin(dLat / 2) ** 2 + Math.cos(lat1) * Math.cos(lat2) * Math.sin(dLng / 2) ** 2;
+ const d = 2 * R * Math.asin(Math.sqrt(x));
+ return d <= maxKm;
+ });
+ res.json(nearby.slice(0, 50));
+});
+
+// ── Batch update - mark many alerts as read ───────────────────
+app.post('/api/alerts/batch-view', rateLimit(10, 60000), (req, res) => {
+ const { ids } = req.body;
+ if (!Array.isArray(ids)) return res.status(400).json({ error: 'ids required' });
+ ids.slice(0, 50).forEach(id => {
+ const a = data.alerts.find(x => x.id === id);
+ if (a) a.views = (a.views || 0) + 1;
+ });
+ res.json({ ok: true, updated: ids.length });
+});
+
+// ── Get active users in an area ───────────────────────────────
+app.get('/api/users/area/:area', (req, res) => {
+ const area = decodeURIComponent(req.params.area).toLowerCase();
+ const users = Object.values(data.onlineUsers || {})
+ .filter(u => (u.area || '').toLowerCase().includes(area))
+ .slice(0, 20)
+ .map(u => ({ name: u.name, area: u.area }));
+ res.json({ count: users.length, users });
+});
+
+// ── Global search ─────────────────────────────────────────────
+
+// ─────────────────────────────────────────────────────────────
+// ── App Version Info (v7.0) ─────────────────────────────────
+app.get('/api/version', (_, res) => {
+ res.json({
+ version: '7.0.0',
+ codename: 'Nabdh-Complete',
+ build: Date.now(),
+ features: [
+ 'media-chat','study-groups','dm','pwa','offline',
+ 'trending','leaderboard','blood-bank','prayer-times',
+ 'weather','market-p2p','voice','skills','exchange',
+ 'news','polls','rides','water','help','hospitals',
+ 'global-search','bookmarks','notifications','live-stats'
+ ],
+ minClientVersion: '5.0'
+ });
+});
+
+
+/* ============================================================
+ ⚡ ADVANCED FEATURES v7.1 - محرك الأداء المتقدم
+ ============================================================ */
+
+// ── Smart Cache Layer ─────────────────────────────────────────
+const _cache = new Map();
+function cacheGet(key) {
+ const item = _cache.get(key);
+ if (!item) return null;
+ if (Date.now() > item.expires) { _cache.delete(key); return null; }
+ return item.data;
+}
+function cacheSet(key, data, ttlMs = 30000) {
+ _cache.set(key, { data, expires: Date.now() + ttlMs });
+}
+setInterval(() => {
+ const now = Date.now();
+ for (const [k, v] of _cache) { if (now > v.expires) _cache.delete(k); }
+}, 60000);
+
+// ── Real-time stats SSE (Server-Sent Events) ──────────────────
+app.get('/api/events', (req, res) => {
+ res.set({
+ 'Content-Type': 'text/event-stream',
+ 'Cache-Control': 'no-cache',
+ 'Connection': 'keep-alive',
+ 'Access-Control-Allow-Origin': '*'
+ });
+ res.flushHeaders();
+ const send = () => {
+ const payload = JSON.stringify({
+ online: Object.keys(data.onlineUsers || {}).length,
+ alerts: data.alerts.length,
+ time: Date.now()
+ });
+ res.write(`data: ${payload}\n\n`);
+ };
+ send();
+ const iv = setInterval(send, 10000);
+ req.on('close', () => clearInterval(iv));
+});
+
+// ── Leaderboard API ───────────────────────────────────────────
+app.get('/api/leaderboard', (req, res) => {
+ const cached = cacheGet('leaderboard');
+ if (cached) return res.json(cached);
+ const profiles = Object.entries(data.profiles || {});
+ const board = profiles
+ .map(([uid, p]) => ({
+ uid,
+ name: p.name || 'مجهول',
+ avatar: p.avatar || '👤',
+ area: p.area || '',
+ points: p.points || 0,
+ level: p.level || 1,
+ badges: (p.badges || []).slice(0, 3),
+ reports: p.reports || 0
+ }))
+ .sort((a, b) => b.points - a.points)
+ .slice(0, 50);
+ cacheSet('leaderboard', { board, total: board.length }, 60000);
+ res.json({ board, total: board.length });
+});
+
+// ── Prayer times calculator ───────────────────────────────────
+app.get('/api/prayer/:lat/:lng', (req, res) => {
+ const lat = parseFloat(req.params.lat) || 15.5;
+ const lng = parseFloat(req.params.lng) || 32.5;
+ const now = new Date();
+ // Simple prayer time calculation (Khartoum approximate)
+ const base = { fajr:'04:45', dhuhr:'12:10', asr:'15:30', maghrib:'18:15', isha:'19:45' };
+ res.json({ prayers: base, lat, lng, date: now.toISOString().split('T')[0], timezone: 'Africa/Khartoum' });
+});
+
+// ── Weather API ───────────────────────────────────────────────
+app.get('/api/weather/:area', (req, res) => {
+ const area = req.params.area || 'الخرطوم';
+ const cached = cacheGet(`weather_${area}`);
+ if (cached) return res.json(cached);
+ // Simulated weather (replace with real API in production)
+ const conditions = ['مشمس','غائم جزئياً','غائم','رياح خفيفة','حار وجاف'];
+ const weather = {
+ area,
+ temp: Math.floor(Math.random() * 15) + 30,
+ feels_like: Math.floor(Math.random() * 15) + 33,
+ humidity: Math.floor(Math.random() * 30) + 20,
+ wind: Math.floor(Math.random() * 20) + 5,
+ condition: conditions[Math.floor(Math.random() * conditions.length)],
+ icon: '☀️',
+ updated: new Date().toISOString()
+ };
+ cacheSet(`weather_${area}`, weather, 300000); // 5 min cache
+ res.json(weather);
+});
+
+// ── Notifications system ──────────────────────────────────────
+app.post('/api/notify/subscribe', (req, res) => {
+ const { userId, subscription } = req.body;
+ if (!userId || !subscription) return res.status(400).json({ error: 'missing fields' });
+ if (!data.pushSubs) data.pushSubs = {};
+ data.pushSubs[userId] = { subscription, ts: Date.now() };
+ res.json({ ok: true });
+});
+
+// ── Reports by category stats ─────────────────────────────────
+app.get('/api/stats/categories', (req, res) => {
+ const cats = {};
+ data.alerts.forEach(a => {
+ const t = a.type || 'general';
+ cats[t] = (cats[t] || 0) + 1;
+ });
+ const sorted = Object.entries(cats)
+ .map(([type, count]) => ({ type, count }))
+ .sort((a, b) => b.count - a.count);
+ res.json({ categories: sorted, total: data.alerts.length });
+});
+
+// ── Hot topics / trending tags ────────────────────────────────
+app.get('/api/tags/trending', (req, res) => {
+ const cached = cacheGet('trending_tags');
+ if (cached) return res.json(cached);
+ const tagCounts = {};
+ const sixHours = Date.now() - 6 * 3600000;
+ data.alerts.filter(a => a.time > sixHours).forEach(a => {
+ const words = (a.msg || '').split(/\s+/).filter(w => w.length > 3);
+ words.forEach(w => { tagCounts[w] = (tagCounts[w] || 0) + 1; });
+ });
+ const tags = Object.entries(tagCounts)
+ .filter(([, c]) => c > 1)
+ .sort(([, a], [, b]) => b - a)
+ .slice(0, 10)
+ .map(([tag, count]) => ({ tag, count }));
+ cacheSet('trending_tags', { tags }, 120000);
+ res.json({ tags });
+});
+
+// ── Blood donors by blood type ────────────────────────────────
+app.get('/api/blood/stats', (req, res) => {
+ const stats = {};
+ (data.bloodDonors || []).forEach(d => {
+ const t = d.bloodType || 'غير محدد';
+ stats[t] = (stats[t] || 0) + 1;
+ });
+ res.json({ stats, total: (data.bloodDonors || []).length });
+});
+
+// ── Nearby services (hospitals, markets, etc.) ───────────────
+app.get('/api/nearby/services', (req, res) => {
+ const { lat, lng, type, radius = 10 } = req.query;
+ if (!lat || !lng) return res.json({ services: [] });
+ const R = parseFloat(radius);
+ const userLat = parseFloat(lat);
+ const userLng = parseFloat(lng);
+ let services = [];
+ // Add hospitals
+ if (!type || type === 'hospital') {
+ services = services.concat(
+ (data.hospitals || []).map(h => ({ ...h, serviceType: 'hospital', icon: '🏥' }))
+ );
+ }
+ // Add market items with location
+ if (!type || type === 'market') {
+ services = services.concat(
+ (data.marketplace || [])
+ .filter(m => m.lat && m.lng)
+ .map(m => ({ ...m, serviceType: 'market', icon: '🛒' }))
+ );
+ }
+ // Filter by radius
+ const haversine = (la1, ln1, la2, ln2) => {
+ const R2 = 6371;
+ const dLat = (la2 - la1) * Math.PI / 180;
+ const dLon = (ln2 - ln1) * Math.PI / 180;
+ const a = Math.sin(dLat/2)**2 + Math.cos(la1*Math.PI/180)*Math.cos(la2*Math.PI/180)*Math.sin(dLon/2)**2;
+ return R2 * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
+ };
+ const filtered = services
+ .filter(s => s.lat && s.lng && haversine(userLat, userLng, s.lat, s.lng) <= R)
+ .slice(0, 20);
+ res.json({ services: filtered, count: filtered.length });
+});
+
+// ── Market advanced filter ────────────────────────────────────
+app.get('/api/market/filter', rateLimit(60, 60000), (req, res) => {
+ const { type, area, minPrice, maxPrice, sort = 'time', page = 1, limit = 20 } = req.query;
+ let items = [...(data.marketplace || [])];
+ if (type) items = items.filter(m => m.type === type);
+ if (area) items = items.filter(m => (m.area || '').toLowerCase().includes(area.toLowerCase()));
+ if (minPrice) items = items.filter(m => (m.price || 0) >= parseFloat(minPrice));
+ if (maxPrice) items = items.filter(m => (m.price || 0) <= parseFloat(maxPrice));
+ if (sort === 'price_asc') items.sort((a, b) => (a.price || 0) - (b.price || 0));
+ else if (sort === 'price_desc') items.sort((a, b) => (b.price || 0) - (a.price || 0));
+ else if (sort === 'likes') items.sort((a, b) => (b.likes || 0) - (a.likes || 0));
+ else items.sort((a, b) => b.time - a.time);
+ const pg = parseInt(page), lm = Math.min(parseInt(limit), 50);
+ const total = items.length;
+ items = items.slice((pg - 1) * lm, pg * lm);
+ res.json({ items, total, page: pg, pages: Math.ceil(total / lm) });
+});
+
+// ── Voice posts ───────────────────────────────────────────────
+app.get('/api/voice/trending', (req, res) => {
+ const voices = (data.voicePosts || [])
+ .sort((a, b) => ((b.votes || 0) + (b.plays || 0)) - ((a.votes || 0) + (a.plays || 0)))
+ .slice(0, 10);
+ res.json({ voices, total: voices.length });
+});
+
+// ── Skills marketplace ────────────────────────────────────────
+app.get('/api/skills/categories', (req, res) => {
+ const cats = {};
+ (data.skills || []).forEach(s => {
+ const c = s.category || 'عام';
+ cats[c] = (cats[c] || 0) + 1;
+ });
+ const categories = Object.entries(cats)
+ .map(([name, count]) => ({ name, count }))
+ .sort((a, b) => b.count - a.count);
+ res.json({ categories });
+});
+
+// ── Polls voting stats ────────────────────────────────────────
+app.get('/api/polls/active', (req, res) => {
+ const now = Date.now();
+ const active = (data.polls || [])
+ .filter(p => !p.expiresAt || p.expiresAt > now)
+ .sort((a, b) => (b.totalVotes || 0) - (a.totalVotes || 0))
+ .slice(0, 10);
+ res.json({ polls: active, count: active.length });
+});
+
+// ── Study groups by subject ───────────────────────────────────
+app.get('/api/study/subjects', (req, res) => {
+ const subjects = {};
+ (data.studyGroups || []).forEach(g => {
+ const s = g.subject || 'عام';
+ subjects[s] = (subjects[s] || 0) + 1;
+ });
+ res.json({
+ subjects: Object.entries(subjects).map(([name, count]) => ({ name, count })),
+ total: (data.studyGroups || []).length
+ });
+});
+
+// ── SOS history ───────────────────────────────────────────────
+app.get('/api/sos/recent', rateLimit(20, 60000), (req, res) => {
+ const recent = (data.sosAlerts || [])
+ .sort((a, b) => b.time - a.time)
+ .slice(0, 10)
+ .map(s => ({ id: s.id, area: s.area, time: s.time, resolved: s.resolved || false }));
+ res.json({ sos: recent, count: recent.length });
+});
+
+// ── Rides board ───────────────────────────────────────────────
+app.get('/api/rides/active', (req, res) => {
+ const now = Date.now();
+ const active = (data.rides || [])
+ .filter(r => r.time > now - 24 * 3600000)
+ .sort((a, b) => b.time - a.time)
+ .slice(0, 20);
+ res.json({ rides: active, count: active.length });
+});
+
+// ── Help requests urgent ──────────────────────────────────────
+app.get('/api/help/urgent', (req, res) => {
+ const urgent = (data.helpRequests || [])
+ .filter(h => h.urgent || h.priority === 'high')
+ .sort((a, b) => b.time - a.time)
+ .slice(0, 10);
+ res.json({ requests: urgent, count: urgent.length });
+});
+
+// ── Water reports map ─────────────────────────────────────────
+app.get('/api/water/heatmap', (req, res) => {
+ const points = (data.waterReports || [])
+ .filter(w => w.lat && w.lng)
+ .map(w => ({ lat: w.lat, lng: w.lng, weight: w.severity || 1 }));
+ res.json({ points, count: points.length });
+});
+
+// ── Power outages heatmap ─────────────────────────────────────
+app.get('/api/power/heatmap', (req, res) => {
+ const points = (data.powerSchedules || [])
+ .filter(p => p.lat && p.lng && p.status === 'off')
+ .map(p => ({ lat: p.lat, lng: p.lng, weight: 1, area: p.area }));
+ res.json({ points, count: points.length });
+});
+
+// ── User activity feed enhanced ───────────────────────────────
+app.get('/api/feed/enhanced/:userId', rateLimit(30, 60000), (req, res) => {
+ const userId = req.params.userId;
+ const profile = (data.profiles || {})[userId];
+ const userArea = profile?.area || '';
+ const now = Date.now();
+ const oneDay = 24 * 3600000;
+
+ let feed = [];
+
+ // Alerts in user's area (higher priority)
+ const areaAlerts = data.alerts
+ .filter(a => a.time > now - oneDay && (userArea ? (a.area || '').includes(userArea) : true))
+ .map(a => ({ ...a, feedType: 'alert', priority: 2 }));
+
+ // Market items
+ const market = (data.marketplace || [])
+ .filter(m => m.time > now - oneDay)
+ .map(m => ({ ...m, feedType: 'market', priority: 1 }));
+
+ // Trending voice posts
+ const voice = (data.voicePosts || [])
+ .filter(v => v.time > now - oneDay && (v.votes || 0) > 0)
+ .map(v => ({ ...v, feedType: 'voice', priority: 1 }));
+
+ // News
+ const news = (data.news || [])
+ .filter(n => n.time > now - oneDay)
+ .map(n => ({ ...n, feedType: 'news', priority: 1 }));
+
+ feed = [...areaAlerts, ...market.slice(0, 5), ...voice.slice(0, 3), ...news.slice(0, 5)];
+ feed.sort((a, b) => (b.priority || 0) - (a.priority || 0) || b.time - a.time);
+
+ res.json({ feed: feed.slice(0, 30), total: feed.length });
+});
+
+// ── Online users map data ─────────────────────────────────────
+app.get('/api/users/map', (req, res) => {
+ const users = Object.values(data.onlineUsers || {})
+ .filter(u => u.lat && u.lng)
+ .map(u => ({ name: u.name, lat: u.lat, lng: u.lng, area: u.area }));
+ res.json({ users, count: users.length });
+});
+
+// ── Exchange rates history ────────────────────────────────────
+app.get('/api/exchange/history', (req, res) => {
+ const history = (data.exchangeRates || [])
+ .sort((a, b) => b.time - a.time)
+ .slice(0, 30);
+ res.json({ history, count: history.length });
+});
+
+// ── Medicine availability by area ────────────────────────────
+app.get('/api/medicine/by-area', (req, res) => {
+ const { area } = req.query;
+ let meds = data.medicines || [];
+ if (area) meds = meds.filter(m => (m.area || '').toLowerCase().includes(area.toLowerCase()));
+ const byArea = {};
+ meds.forEach(m => {
+ const a = m.area || 'غير محدد';
+ if (!byArea[a]) byArea[a] = [];
+ byArea[a].push({ name: m.name, available: m.available, price: m.price });
+ });
+ res.json({ areas: byArea, total: meds.length });
+});
+
+// ── Dashboard full stats ──────────────────────────────────────
+app.get('/api/dashboard/full', (req, res) => {
+ const cached = cacheGet('dashboard_full');
+ if (cached) return res.json(cached);
+ const now = Date.now();
+ const today = now - 24 * 3600000;
+ const week = now - 7 * 24 * 3600000;
+
+ const stats = {
+ online: Object.keys(data.onlineUsers || {}).length,
+ reports: data.alerts.length,
+ reports_today: data.alerts.filter(a => a.time > today).length,
+ reports_week: data.alerts.filter(a => a.time > week).length,
+ lives_saved: data.livesSaved || 0,
+ cities: new Set(data.alerts.map(a => a.area?.split('،')[0]).filter(Boolean)).size,
+ market_items: (data.marketplace || []).length,
+ blood_donors: (data.bloodDonors || []).length,
+ study_groups: (data.studyGroups || []).length,
+ voice_posts: (data.voicePosts || []).length,
+ skills: (data.skills || []).length,
+ polls: (data.polls || []).length,
+ help_requests: (data.helpRequests || []).length,
+ water_reports: (data.waterReports || []).length,
+ power_reports: (data.powerSchedules || []).length,
+ registered_users: Object.keys(data.profiles || {}).length
+ };
+
+ const top_areas = Object.entries(
+ data.alerts.reduce((acc, a) => {
+ const area = a.area?.split('،')[0] || 'غير محدد';
+ acc[area] = (acc[area] || 0) + 1;
+ return acc;
+ }, {})
+ ).sort(([, a], [, b]) => b - a).slice(0, 5).map(([area, count]) => ({ area, count }));
+
+ const result = { stats, top_areas, updated: now };
+ cacheSet('dashboard_full', result, 30000);
+ res.json(result);
+});
+
+/* ============================================================
+ 🏘️ HOOD GROUPS - مجموعات الأحياء
+ ============================================================ */
+
+// GET all hood groups (optionally filter by area)
+app.get('/api/hood', (req, res) => {
+ let groups = Object.values(data.hoodGroups || {})
+ .sort((a, b) => (b.updatedAt || b.createdAt || 0) - (a.updatedAt || a.createdAt || 0));
+ if (req.query.area) {
+ const q = req.query.area.trim().toLowerCase();
+ groups = groups.filter(g => (g.area||'').toLowerCase().includes(q));
+ }
+ if (req.query.type) groups = groups.filter(g => g.type === req.query.type);
+ res.json(groups.slice(0, 100));
+});
+
+// GET single hood group
+app.get('/api/hood/:id', (req, res) => {
+ const g = (data.hoodGroups || {})[req.params.id];
+ if (!g) return res.status(404).json({ error: 'المجموعة غير موجودة' });
+ res.json(g);
+});
+
+// POST create hood group
+app.post('/api/hood', (req, res) => {
+ const { name, area, type, desc, contact, userId, author } = req.body;
+ if (!name || !area) return res.status(400).json({ error: 'اسم المجموعة والحي مطلوبان' });
+ const id = uuidv4();
+ const now = Date.now();
+ const group = {
+ id,
+ name: name.trim(),
+ area: area.trim(),
+ type: type || 'عام', // نظافة | اجتماعات | ترشيح | مبادرة | عام
+ desc: (desc || '').trim(),
+ contact: contact || '',
+ members: userId ? [userId] : [],
+ posts: [],
+ nominations: [],
+ userId: userId || null,
+ author: author || 'مجهول',
+ createdAt: now,
+ updatedAt: now,
+ pinned: false
+ };
+ if (!data.hoodGroups) data.hoodGroups = {};
+ data.hoodGroups[id] = group;
+ saveData();
+ io.emit('new_hood_group', group);
+ res.json({ success: true, id, group });
+});
+
+// POST join hood group
+app.post('/api/hood/:id/join', (req, res) => {
+ const g = (data.hoodGroups || {})[req.params.id];
+ if (!g) return res.status(404).json({ error: 'المجموعة غير موجودة' });
+ if (!Array.isArray(g.members)) g.members = [];
+ const uid = req.body.userId;
+ if (uid && !g.members.includes(uid)) g.members.push(uid);
+ saveData();
+ io.emit('hood_join', { id: g.id, members: g.members });
+ res.json({ success: true, members: g.members });
+});
+
+// POST leave hood group
+app.post('/api/hood/:id/leave', (req, res) => {
+ const g = (data.hoodGroups || {})[req.params.id];
+ if (!g) return res.status(404).json({ error: 'المجموعة غير موجودة' });
+ const uid = req.body.userId;
+ g.members = (g.members || []).filter(m => m !== uid);
+ saveData();
+ io.emit('hood_leave', { id: g.id, members: g.members });
+ res.json({ success: true, members: g.members });
+});
+
+// POST add post/message to group
+app.post('/api/hood/:id/post', (req, res) => {
+ const g = (data.hoodGroups || {})[req.params.id];
+ if (!g) return res.status(404).json({ error: 'المجموعة غير موجودة' });
+ const { text, userId, author, postType, meetingDate, meetingTime, meetingPlace } = req.body;
+ if (!text) return res.status(400).json({ error: 'النص مطلوب' });
+ const post = {
+ id: uuidv4(),
+ text: text.trim(),
+ postType: postType || 'message', // message | meeting | initiative | nomination
+ userId: userId || null,
+ author: author || 'مجهول',
+ likes: [],
+ ts: Date.now()
+ };
+ // meeting fields
+ if (postType === 'meeting') {
+ if (meetingDate) post.meetingDate = meetingDate;
+ if (meetingTime) post.meetingTime = meetingTime;
+ if (meetingPlace) post.meetingPlace = meetingPlace.trim();
+ }
+ if (!Array.isArray(g.posts)) g.posts = [];
+ g.posts.push(post);
+ if (g.posts.length > 200) g.posts = g.posts.slice(-200);
+ g.updatedAt = Date.now();
+ saveData();
+ io.emit('hood_post', { groupId: g.id, post });
+ res.json({ success: true, post });
+});
+
+// POST like a post inside group
+app.post('/api/hood/:id/post/:postId/like', (req, res) => {
+ const g = (data.hoodGroups || {})[req.params.id];
+ if (!g) return res.status(404).json({ error: 'المجموعة غير موجودة' });
+ const post = (g.posts || []).find(p => p.id === req.params.postId);
+ if (!post) return res.status(404).json({ error: 'المنشور غير موجود' });
+ const uid = req.body.userId;
+ if (!Array.isArray(post.likes)) post.likes = [];
+ const idx = post.likes.indexOf(uid);
+ if (idx === -1) post.likes.push(uid);
+ else post.likes.splice(idx, 1);
+ saveData();
+ res.json({ success: true, likes: post.likes.length });
+});
+
+// POST add nomination (ترشيح محل/مبادرة)
+app.post('/api/hood/:id/nominate', (req, res) => {
+ const g = (data.hoodGroups || {})[req.params.id];
+ if (!g) return res.status(404).json({ error: 'المجموعة غير موجودة' });
+ const { title, category, desc, userId, author } = req.body;
+ if (!title) return res.status(400).json({ error: 'عنوان الترشيح مطلوب' });
+ const nom = {
+ id: uuidv4(),
+ title: title.trim(),
+ category: (category || '').trim(),
+ desc: (desc || '').trim(),
+ votes: [],
+ userId: userId || null,
+ author: author || 'مجهول',
+ ts: Date.now()
+ };
+ if (!Array.isArray(g.nominations)) g.nominations = [];
+ g.nominations.push(nom);
+ g.updatedAt = Date.now();
+ saveData();
+ io.emit('hood_nomination', { groupId: g.id, nomination: nom });
+ res.json({ success: true, nomination: nom });
+});
+
+// POST vote on nomination
+app.post('/api/hood/:id/nominate/:nomId/vote', (req, res) => {
+ const g = (data.hoodGroups || {})[req.params.id];
+ if (!g) return res.status(404).json({ error: 'المجموعة غير موجودة' });
+ const nom = (g.nominations || []).find(n => n.id === req.params.nomId);
+ if (!nom) return res.status(404).json({ error: 'الترشيح غير موجود' });
+ const uid = req.body.userId;
+ if (!Array.isArray(nom.votes)) nom.votes = [];
+ const idx = nom.votes.indexOf(uid);
+ if (idx === -1) nom.votes.push(uid);
+ else nom.votes.splice(idx, 1);
+ saveData();
+ res.json({ success: true, votes: nom.votes.length });
+});
+
+// DELETE hood group (owner only)
+app.delete('/api/hood/:id', (req, res) => {
+ const g = (data.hoodGroups || {})[req.params.id];
+ if (!g) return res.status(404).json({ error: 'المجموعة غير موجودة' });
+ if (g.userId && g.userId !== req.body.userId) return res.status(403).json({ error: 'غير مصرح' });
+ delete data.hoodGroups[req.params.id];
+ saveData();
+ io.emit('hood_deleted', { id: req.params.id });
+ res.json({ success: true });
+});
+
+// ── Ping / heartbeat ──────────────────────────────────────────
+app.get('/ping', (_, res) => res.json({ pong: true, ts: Date.now() }));
+
diff --git a/setup.cfg b/setup.cfg
deleted file mode 100644
index 109276f..0000000
--- a/setup.cfg
+++ /dev/null
@@ -1,81 +0,0 @@
-[metadata]
-name = autoagent
-version = 0.1.0
-author = jiabintang
-description = dynamic agentic framework.
-long_description = file: README.md
-long_description_content_type = text/markdown
-license = MIT
-
-[options]
-package_dir =
- = .
-packages = find_namespace:
-zip_safe = True
-include_package_data = True
-install_requires =
- numpy
- openai>=1.52.0
- pytest
- requests
- tqdm
- pre-commit
- instructor
- litellm==1.55.0
- beautifulsoup4
- browsergym==0.13.0
- chromadb
- click
- datasets
- docling
- filelock
- Flask
- gymnasium
- html2text
- httpx
- huggingface_hub
- inquirer
- loguru
- mammoth
- markdownify
- matplotlib
- networkx
- pandas
- pathvalidate==3.2.1
- pdfminer.six
- Pillow
- playwright==1.39.0
- prompt_toolkit
- psutil
- puremagic
- pydantic
- pydub
- python_pptx
- PyYAML
- rich
- SpeechRecognition
- tenacity
- termcolor
- tiktoken
- tree_sitter==0.23.1
- uvicorn
- youtube_transcript_api
- moviepy
- faster_whisper
- sentence_transformers
-
-[options.packages.find]
-where = .
-include = autoagent*
-
-python_requires = >=3.10
-
-[options.entry_points]
-console_scripts =
- auto = autoagent.cli:cli
-[tool.autopep8]
-max_line_length = 120
-ignore = E501,W6
-in-place = true
-recursive = true
-aggressive = 3
\ No newline at end of file