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 @@ + + + + + + نبض — دليل المستخدم الشامل + + + + + + + + + +
+
+
+
+
مباشر الآن · السودان والعالم
+

نبض
صوت المجتمع الحي

+

+ تطبيق مجتمعي متكامل يجمع الأخبار اللحظية، الأسعار، الخرائط الحية،
+ خدمات الطوارئ، والتواصل المجتمعي في منصة واحدة مجانية. +

+ +
+
23+قسم متكامل
+
145+واجهة برمجية
+
388+وظيفة تفاعلية
+
18ولاية سودانية
+
+
+
+ + +
+
+
+ ما هو نبض؟ +

المنصة المجتمعية الأولى للسودان

+

نبض هو تطبيق ويب تقدمي (PWA) يعمل من المتصفح مباشرةً — لا تحتاج لتحميل أي شيء

+
+
+ ✅ مجاني 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)... تتراكم النقاط لترفع مستواك من "جديد" إلى "أسطورة" مع شارات حصرية ومركز في لوحة المتصدرين.
+
+ +
+
هل التطبيق متاح خارج السودان؟
+
نعم! يمكن استخدامه من أي مكان في العالم. يدعم البحث الجغرافي العالمي ويتيح للمغتربين السودانيين متابعة أخبار الوطن والتواصل مع المجتمع.
+
+ +
+
+
+ + +
+
+
+

جاهز للانضمام لمجتمع نبض؟

+

أكثر من مجرد تطبيق — نبض هو صوت كل مواطن سوداني يريد مجتمعاً أفضل.

+ +
+
+
+ + + + + + + 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تقرير موثق
+
0حياة أُنقذت
+
0منطقة
+
+
+ + +
+ 📍 + جاري التحديد التلقائي... + +
+ + +
+
🔔
+
مرحباً بك في نبض - الموقع يتحدد تلقائياً 💓
+
+ + +
+ +
+ + +
+

⚡ الوصول السريع

+
+ + + + + + + + + + + + + + + + + + + + +
+
+ + +
+
+

🔴 تنبيهات منطقتك

+ +
+
+
+ + +
+
+

💵 سعر الصرف الآن

+ +
+
+
--- ج.س / دولار
+
لم يُبلَّغ عن أي سعر بعد
+
+
+ + +
+
+

🛒 أحدث عروض السوق

+ +
+
+
+ + +
+

⚡ نبض الآن

+ +
+
+
+
0
+
مباشر الآن
+
+
+
+
0
+
بلاغ اليوم
+
+
+
+
0
+
منطقة نشطة
+
+
+
+ +
الأكثر تداولاً
+
+
+
+ + +
+
+

🏆 أبطال المجتمع

+ +
+
+
+ + +
+
+
🎯 تحدي اليوم
+
جاري التحميل...
+
+
+
+ 0 / 0 مشارك +
+
🏅 المكافأة: نقاط + شارة
+
+
+ + +
+
+

🔥 الأكثر انتشاراً

+ يتجدد كل 5 دقائق +
+
+
+ + +
+ + +
+ + +
+
+
🔗
+
+
ادعُ صديقاً واكسب نقاطاً!
+
كل صديق تدعوه = 20 نقطة مجانية لك
+
+
+20 🏆
+
+
+
+ + +
+ + +
+
+ + +
+ + +
+ + + + + +
+ + + + + + + +
+ + +
+
+ + +
+ + + +
+
+ + +
+
+ 0 خطر +
+
+ 0 تحذير +
+
+ 0 معلومة +
+
+ 0 شخص +
+
0 إجمالي
+ +
+ + +
+ +
+ + +
+
+

📋 جميع التنبيهات

+
+ + +
+
+
+
+
+ + +
+
+

🔍 البحث عن الأشخاص

+

بحث شامل - أشخاص، شركات، أرقام، مهارات وإعلانات

+
+ + +
+ + + +
+ + +
+

📡 أشخاص نشطون قريبون منك

+ +
+
+
+
+ + + + +
+ + + + + + + + + + + + +
+
+
+ +
+ 🌱 + جديد +
+ +
+ +
+
+ + + + +
+
👤
+ +
📷
+
+
+
+
+
+ + +
+
+

مستخدم نبض

+ +
+ +
+ + غير محدد +
+
+ 💓 نبض + 🟢 نشط + 🌱 جديد +
+
+ + +
+ + + + + + +
+ + +
+
+ التقدم نحو المستوى التالي + 0 / 100 XP +
+
+
+
+
+
+
+
+ +
+ + +
+
+ 0 +
بلاغات
+
+
+ 0 +
نقطة
+
+
+ 1 +
مستوى
+
+
+ +
انضمام
+
+
+ + + + + +
+ + +
+
+
+
+
🌱
+
+
+
+
جديد
+
0 نقطة
+ +
+ +
+ +
+
+
+ 📊 + نشاط الأسبوع +
+
+
+
+
+ +
+ أنجز تحديات لتحصل على شارات 🏅 +
+ +
+ + + +
+
+ + +
+ + +
+
+ 📞 + التواصل +
+
+ +
+
📱
+
+ الهاتف + غير مُضاف +
+ +
+
+
✉️
+
+ البريد + غير مُضاف +
+ +
+
+
💬
+
+ واتساب + +
+ +
+
+
✈️
+
+ تيليغرام + +
+ +
+
+
+ + +
+
+ 💼 + المعلومات المهنية +
+
+
+
💼
+
+ الوظيفة + +
+
+
+
🏢
+
+ الشركة + +
+
+
+
🌐
+
+ الموقع + +
+
+
+
+ + +
+
+ 📋 + البيانات الشخصية +
+
+
+
📍
+
+ المنطقة + غير محدد +
+ +
+
+
📝
+
+ نبذة + أضف نبذة عنك... +
+
+
+
+ + +
+
+ 🏅 + شاراتك ومستواك +
+
+
🌟 عضو جديد
+
+
+ + +
+ + + + + + + + + +
+ +
+ + + + +
+ + + + +
+ + + + + + +
+
+

💬 الرسائل المباشرة

+

تواصل خاص ومباشر مع أي شخص في نبض

+
+ + +
+ + + +
+

📨 محادثاتك

+ +
+
+ + +
+ +
+
+
+ + +
+
+

💵 صرّاف الشعب

+

سعر الصرف الحقيقي من السوق مباشرة - يتحدث تلقائياً بموقعك

+
+
+
+
سعر الدولار الآن
+
---
+
جنيه سوداني
+
لم يُبلَّغ بعد
+
+
+
-أعلى اليوم
+
-أدنى اليوم
+
-المتوسط
+
+
+
+

📈 منحنى السعر

+ +
+ +
+

🧮 حاسبة الصرف السريعة

+
+
+ + +
+
+ +
+
+
+ +
+
يستخدم سعر السوق المُبلَّغ عنه مباشرة
+
+
+ +
+

📤 شارك السعر من مكانك

+
+
+ + + + +
+ +
+ 📍 يتحدد موقعك تلقائياً + +
+ +
+
+
+

🕐 آخر الأسعار المُبلَّغ عنها

+
+
+
+ + +
+
+

💊 دواء موجود

+

ابحث عن دوائك في أقرب صيدلية - يعمل بموقعك تلقائياً

+
+
+ +
+ + + + +
+
+
+
+

➕ أضف معلومة دواء

+
+ + +
+ + + + +
+
+ 📍 يتحدد موقعك تلقائياً + +
+ +
+ + +
+ +
+
+
+ + +
+
+

📢 صوت الحي

+

صوتك يصنع التغيير - يُرسل بموقعك الفعلي

+
+
+
+ + +
+ + + + +
+
+ 📍 يتحدد موقعك تلقائياً + +
+ + +
+
+
+
+ + +
+
+

🤝 بورصة المهارات

+

تبادل الخدمات بدون نقود - P2P حقيقي

+
+
+
💡 عرض مهارتك واحصل على ما تحتاجه من مجتمعك - مجاناً تماماً
+
+

➕ أضف مهارتك

+
+ + + + +
+ + + + +
+
+ 📍 يتحدد موقعك تلقائياً + +
+ +
+
+
+ + +
+
+

🛒 سوق P2P المباشر

+

بيع • شراء • تبادل مباشر بين الناس بناءً على موقعك الفعلي

+
+ +
+ + + + + +
+ +
+ + + + + + + + + +
+ +
+ +
+

➕ أضف إعلانك

+
+
+ + + +
+ + + +
+ + +
+ +
+ + + + +
+
+ 📍 يتحدد موقعك تلقائياً + +
+ +
+ + + +
+ + +
+
+
+ + +
+
+

🚨 بلّغ الآن

+

شارك معلومة عاجلة وأنقذ حياة - الموقع يُرفق تلقائياً

+
+
+
+ + + +
+
+ + +
+ + + +
+ +
+ + + + +
+
+
+ + يتحدد الموقع تلقائياً... +
+ +
+ +
+ +
+ +
+ +
+

💡 نصائح للتبليغ الفعّال

+
    +
  • ✅ كن دقيقاً في وصف المكان والحدث
  • +
  • ✅ اذكر الوقت التقريبي للحدث
  • +
  • ✅ الموقع يُرفق تلقائياً بدون أي ضغط
  • +
  • ✅ يمكنك النقر على الخريطة لتحديد الموقع يدوياً
  • +
  • ✅ لا تشارك معلومات شخصية حساسة
  • +
+
+
+
+ + +
+
+

🩸 بنك الدم

+

تبرع بالدم أو ابحث عن متبرع - أنقذ حياة الآن

+
+
+ +
+ + + +
+ + + + + + + + + +
+
+ + +
+
+

⚡ جدول الكهرباء

+

شارك جدول الانقطاع في حيّك - ساعد جيرانك

+
+
+ +
+
+
+ حالة الكهرباء في منطقتك + +
+
+ + +
+
+ + +
+

📋 أضف جدول حيّك

+
+ + + + +
+
+
+ + +
+
+ + +
+
+ + +
+ + +
+ +
+ + +
+
+
+ + +
+
+

🕌 أوقات الصلاة

+

أوقات دقيقة حسب موقعك الجغرافي

+
+
+ +
+
الصلاة القادمة
+
+
--:--:--
+
+
+ + +
+
🌙
الفجر
+
🌅
الشروق
+
☀️
الظهر
+
🌤️
العصر
+
🌆
المغرب
+
🌃
العشاء
+
+ + +
+ 📍 يتحدد الموقع تلقائياً... + +
+ + +
+ + +
+ +
+
+
+
🔔 منبّه أوقات الصلاة
+
غير مفعّل
+
+ +
+ +
+
+
+ + +
+
+

🏥 دليل المستشفيات

+

ابحث عن أقرب مستشفى أو عيادة وقيّمها

+
+
+ + + +
+ + +
+
+ + +
+
+

📰 الأخبار المحلية

+

أخبار مجتمعية موثوقة من السكان أنفسهم

+
+
+ + + +
+ + + + + + + +
+ +
+ +
+

📝 شارك خبراً

+ + +
+ + +
+
+ + +
+ +
+
+
+ + +
+
+

🚗 مشاركة التنقل

+

شارك رحلتك أو ابحث عن مشوار

+
+
+ + + +
+ +
+

➕ أضف رحلة

+
+
+ + +
+
+ + +
+
+
+ + +
+
+ + +
+ + + +
+
+
+ + +
+
+

🌦️ الطقس

+

حالة الطقس حسب موقعك الجغرافي

+
+
+ +
+
⏳ جاري تحديد الطقس...
+
+ +
+ 📍 يتحدد الموقع تلقائياً... + +
+ +
+ +
+
+ + + + +
+ +
+
+ +
+
+
+ + +
+
+

💧 خريطة المياه

+

تقارير انقطاع المياه وأماكن التوزيع

+
+
+ +
+
💧
+
+ حالة المياه في منطقتك + +
+
+ + +
+
+ +
+ +
+

📋 أبلّغ عن انقطاع

+
+ + + +
+ + + + +
+
+
+ + +
+
+

🏘️ مجموعات الأحياء

+

تواصل مع جيرانك — نظافة، اجتماعات، مبادرات، ترشيحات

+
+
+ + + + + + + + +
+
+
+
+
+
+
+ +
+
+ + +
+
+

🎓 مجموعات التعلم

+

تعلّم مع الآخرين وشارك معرفتك

+
+
+ + +
+ + + +
+

➕ أنشئ مجموعة

+ +
+
🎨 أيقونة المجموعة
+
+ 🎓 + 📚 + 🔬 + 🧪 + 💻 + 🌍 + 🎨 + + 🎵 + 📊 +
+ +
+ + + +
+ + +
+ + +
+ + +
+ +
+
+
+ + +
+
+

📦 طلبات المساعدة

+

اطلب مساعدة أو قدّمها لمن يحتاج

+
+
+ +
+ + + + + + + + +
+ +
+ +
+

📦 اطلب مساعدة

+ + + +
+ + +
+ +
+ + +
+ +
+
+
+ + +
+
+

🗳️ استطلاعات الرأي

+

شارك برأيك في قضايا مجتمعك

+
+
+ +
+ +
+

➕ أنشئ استطلاعاً

+ +
+ + +
+ +
+ +
+ + +
+
+ +
+
+
+ + +
+
+

📊 لوحة الإحصاءات

+

إحصاءات التطبيق والمجتمع بشكل مباشر

+
+
+ +
+ +
+ +
+
0
🟢 متصل الآن
+
0
📢 بلاغات
+
0
❤️ أرواح أُنقذت
+
0
🏙️ مدينة نشطة
+
+ +
+ +
+

🏆 أكثر المناطق نشاطاً

+
+
+ +
+

⚡ آخر 24 ساعة

+
+
+ +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + 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 = '
📄' + escHtml(msg.mediaName || 'ملف') + '
'; + } + 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 = '
📄' + escHtml(msg.mediaName || 'ملف') + '
'; + } + 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) + '' + + '' + + '' + + '
'; +} +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 => + '
' + + '
' + (catIcons[v.category] || '📌') + ' ' + escHtml(v.title) + '
' + escHtml(v.category || 'أخرى') + '
' + + (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 ? 'صورة المنتج' : '') + + '' + + (m.price ? '' : '') + + (m.desc ? '' : '') + + '' + + '
' + + '' + + '
' + + ''; + 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 '
' + + '
' + + '
' + avatarText + typeTag + '
' + + '
' + + '
' + escHtml(p.name || 'غير محدد') + (p.verified ? ' ' : '') + (isOnline ? ' ' : '') + '
' + + // Job/company line + ((p.jobTitle || p.company) ? '
' + (p.jobTitle ? '💼 ' + escHtml(p.jobTitle) : '') + (p.jobTitle && p.company ? ' • ' : '') + (p.company ? '🏢 ' + escHtml(p.company) : '') + '
' : '') + + '
📍 ' + escHtml(p.area || 'غير محدد') + (d !== null ? ' • 📡 ' + (d < 1 ? '<1' : Math.round(d)) + ' كم' : '') + '
' + + (p.bio ? '
' + escHtml(p.bio.substring(0, 90)) + (p.bio.length > 90 ? '...' : '') + '
' : '') + + '
' + + + // الرقم المعلن - يُعرض دائماً إذا وُجد + (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 = `avatar`; + } 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 = + '
' + + '
' + avatarText + '
' + + '
' + + '
' + escHtml(name || 'مستخدم') + (profile.verified ? ' ' : '') + (isOnline ? ' ' : '') + '
' + + (jobTitle ? '
💼 ' + escHtml(jobTitle) + (company ? ' • 🏢 ' + escHtml(company) : '') + '
' : (company ? '
🏢 ' + escHtml(company) + '
' : '')) + + '
📍 ' + escHtml(area || 'غير محدد') + (d !== null ? ' • 📡 ' + (d < 1 ? '<1' : Math.round(d)) + ' كم منك' : '') + '
' + + '
' + + + // الرقم المعلن + (phone ? '
📢' + escHtml(phone) + '📞 اتصال
' : '') + + + // نبذة + (bio ? '
' + escHtml(bio) + '
' : '') + + + // معلومات التواصل + '
' + + (whatsapp ? '💬 واتساب' : '') + + (telegram ? '✈️ تيليغرام' : '') + + (website ? '🌐 الموقع' : '') + + '
' + + + // أزرار الإجراءات + '
' + + (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 + '
' + + '
' + + '
' + escHtml(ot.name || 'مستخدم') + '' + + (lastMsg ? '' + timeAgo(lastMsg.time) + '' : '') + '
' + + (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 = + '
' + + '
' + avatarText + '
' + + '
' + + '
' + escHtml(otherName || 'مستخدم') + '
' + + '
● متصل
' + + '
' + + '
💬ابدأ المحادثة...
' + + '
' + + '' + + '' + + '
'; + 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 = '
📄' + escHtml(msg.mediaName || 'ملف') + '
'; + } + 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 = + '
' + + '
' + escHtml(title) + '
📍 ' + escHtml(sub) + '
' + + '
' + + '
💬ابدأ المحادثة...
' + + '
' + + '
' + + '' + + '' + + '
' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '
' + + '' + + '
'; + 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 = '
📄' + escHtml(msg.mediaName || 'ملف') + '
'; + } + 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 = + '
' + + '
' + (cityName || userLocationName || 'موقعك الحالي') + '
' + + '
' + icon + '
' + + '
' + temp + '°C
' + + '
' + desc + '
' + + '
' + + '
' + + '
🌡️' + 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) + '
' : '') + + '
' + + '' + + '' + + '
' + + '
'; + }).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 = '
📄' + escHtml(m.mediaName||'ملف') + '
'; + } + // 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 = '
📄' + escHtml(m.mediaName||'ملف') + '
'; + } + 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 ? '
' + + (h.contact ? '📞 ' + escHtml(h.contact) + '' : '') + + '' + + ((h.lat && h.lng) ? '' : '') + + '' + + '
' : '') + + '
'; + }).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.address ? `
📍 ${escHtml(h.address)}
` : ''} + ${h.phone ? `
📞 ${escHtml(h.phone)}
` : ''} +
+
+ ${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}%
+
+
+ + + +
+
`; +} + +/* ============================================================ + 🚗 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) ? `` : ''} +
+
+ + + +
+
`; +} + +/* ============================================================ + 🎓 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 = '
📄' + escHtml(m.mediaName || 'ملف') + '
'; + } + + 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 += '
📄' + escHtml(m.mediaName||'ملف') + '
'; + } + 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 '
' + + '
' + + '
' + emoji + '
' + + '
' + + '
' + escHtml(g.name) + '
' + + '
' + escHtml(g.subject) + '
' + + '
' + + '
' + escHtml(g.level||'عام') + '
' + + '
' + + '
' + + '👥 ' + 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 + '
' : '') + + '
' + + '' + + '' + + '
' + + '
'; +} + + +/* ============================================================ + 📷 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) => ` + `).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 = ` + + ${escHtml(caption)} + ${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 => ``).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 ? '✓ منضم' : ''} +
+
+ +
+ ${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]||'💬'} + + ${_timeAgo(p.ts)} +
+
${p.text}
+ ${meetInfo} + +
`; + }).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.length} عضو
` + + 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