diff --git a/.env.example b/.env.example index a96f16f..64cc0f5 100644 --- a/.env.example +++ b/.env.example @@ -16,3 +16,8 @@ VITE_FIREBASE_APP_ID=your-app-id # Customize the password reset email template in Firebase Console: # Authentication → Templates → Password reset # You can modify the email subject, body, and sender name + +# Google Gemini API Key (Optional - for AI Blog Generator) +# Get your free API key from: https://aistudio.google.com/app/apikey +# The AI Blog Generator works without this key using demo mode +VITE_GEMINI_API_KEY=your-gemini-api-key-here diff --git a/.github/workflows/duplicate-issue.yml b/.github/workflows/duplicate-issue.yml index d1e00d6..d2ac612 100644 --- a/.github/workflows/duplicate-issue.yml +++ b/.github/workflows/duplicate-issue.yml @@ -1,4 +1,4 @@ -name: 🔁 ECWoC'26 Duplicate Issue Detector +name: 🔁 Duplicate Issue Detector on: issues: @@ -12,7 +12,7 @@ jobs: runs-on: ubuntu-latest steps: - - name: Check for Duplicate Issues (ECWoC'26) + - name: Check for Duplicate Issues uses: actions/github-script@v7 with: github-token: ${{ secrets.GITHUB_TOKEN }} @@ -43,7 +43,7 @@ jobs: (newBody && oldBody && oldBody.includes(newBody)) ) { const comment = ` - ## 🔁 Possible Duplicate Issue (ECWoC'26) + ## 🔁 Possible Duplicate Issue Hi @${newIssue.user.login} 👋, @@ -55,11 +55,10 @@ jobs: - If this is the same problem, please continue the discussion on the existing issue. - If it's different, kindly explain **how this issue is unique** so maintainers can review it. - Keeping issues clean helps everyone contribute better during **ECWoC'26** 🚀 + Keeping issues clean helps everyone contribute better and faster. Thanks for your understanding! 🙌 --- - 🤖 *Automated duplicate detection for ECWoC'26* `; await github.rest.issues.createComment({ @@ -69,13 +68,6 @@ jobs: body: comment }); - await github.rest.issues.addLabels({ - owner, - repo, - issue_number: newIssue.number, - labels: ['ECWoC26', 'possible-duplicate', 'automated'] - }); - break; } } \ No newline at end of file diff --git "a/.github/workflows/issue-create-automate-message.yml\342\200\216\342\200\216" "b/.github/workflows/issue-create-automate-message.yml\342\200\216\342\200\216" index 02200ad..4819339 100644 --- "a/.github/workflows/issue-create-automate-message.yml\342\200\216\342\200\216" +++ "b/.github/workflows/issue-create-automate-message.yml\342\200\216\342\200\216" @@ -17,8 +17,17 @@ jobs: script: | const issueNumber = context.issue.number; - const commentBody1 = `### Thank you for raising this issue!\n We'll review it as soon as possible. We truly appreciate your contributions! ✨\n\n> Meanwhile make sure you've visited the README.md, CONTRIBUTING.md, and CODE_OF_CONDUCT.md before creating a PR for this. Also, please do NOT create a PR until this issue has been assigned to you. 😊`; - + const commentBody1 = + "### Thank you for raising this issue! 🙌\n" + + "We'll review it as soon as possible. We truly appreciate your contribution! ✨\n\n" + + "> 📌 **Before moving ahead:**\n" + + "> - Please make sure you've gone through **README.md**, **CONTRIBUTING.md**, and **CODE_OF_CONDUCT.md**.\n" + + "---\n\n" + + "### 🌟 Support & Explore\n" + + "If you like this repository, don’t forget to **⭐ star it** — it really helps and motivates maintainers!\n\n" + + "- 🛠️ New project ideas\n" + + "- 📚 Learning resources\n" + + "Happy contributing! 😊"; await github.rest.issues.createComment({ owner: context.repo.owner, @@ -27,5 +36,4 @@ jobs: body: commentBody1 }); - console.log('Comment added successfully.'); - \ No newline at end of file + console.log("Comment added successfully."); \ No newline at end of file diff --git a/.github/workflows/pr-create-automate-message.yml b/.github/workflows/pr-create-automate-message.yml index 8c90343..23ea646 100644 --- a/.github/workflows/pr-create-automate-message.yml +++ b/.github/workflows/pr-create-automate-message.yml @@ -1,30 +1,47 @@ -name: Auto Comment on PR +name: Welcome PR Author on: pull_request_target: types: [opened] permissions: - issues: write pull-requests: write + issues: write jobs: - comment: + welcome-pr: runs-on: ubuntu-latest steps: - - name: Comment on PR - uses: actions/github-script@v6 + - name: Welcome new PR + uses: actions/github-script@v7 with: script: | - const prNumber = context.issue.number; + const prAuthor = context.payload.pull_request.user.login; + const prNumber = context.payload.pull_request.number; + + const welcomeMessage = + "## 🎉 Thanks for the PR, @" + prAuthor + "!\n\n" + + "We really appreciate you taking the time to contribute to **CryptoHub**! 💙\n\n" + + "---\n\n" + + "### ⭐ Love this project?\n" + + "**Please give us a star!** It helps the project grow and reach more developers! 🌟\n\n" + + "🔗 https://github.com/KaranUnique/CryptoHub" + + "---\n\n" + + "### ✅ PR Checklist\n" + + "Before we review, please ensure:\n" + + "- [ ] Your code follows the project's coding standards\n" + + "- [ ] All file changes are accurate and intentional\n" + + "- [ ] You've tested your changes locally\n" + + "- [ ] Any review comments have been addressed\n\n" + + "---\n\n" + + "We'll review your PR as soon as possible. Keep up the great work! ✨"; - const commentBody = `### Thanks for creating a PR for your Issue! ☺️\n\nWe'll review it as soon as possible.\nIn the meantime, please double-check the **file changes** and ensure that **all commits** are accurate.\n\nIf there are any **unresolved review comments**, feel free to resolve them. 🙌🏼`; await github.rest.issues.createComment({ owner: context.repo.owner, repo: context.repo.repo, issue_number: prNumber, - body: commentBody + body: welcomeMessage }); - console.log('Comment added successfully.'); \ No newline at end of file + console.log("✅ Welcome comment added to PR #" + prNumber); \ No newline at end of file diff --git "a/.github/workflows/unassign-inactive-contributors.yml\342\200\216" "b/.github/workflows/unassign-inactive-contributors.yml\342\200\216" index 77a2871..9f1142c 100644 --- "a/.github/workflows/unassign-inactive-contributors.yml\342\200\216" +++ "b/.github/workflows/unassign-inactive-contributors.yml\342\200\216" @@ -1,9 +1,10 @@ -name: Unassign inactive contributors +name: Unassign Inactive Contributors on: schedule: - # Runs every 1 hour + # Runs every hour - cron: "0 * * * *" + workflow_dispatch: # Allow manual trigger for testing permissions: issues: write @@ -11,23 +12,23 @@ permissions: jobs: unassign-inactive: runs-on: ubuntu-latest - steps: - - name: Check and warn inactive contributors + - name: Check and unassign inactive contributors uses: actions/github-script@v7 with: script: | const now = new Date(); + const WARNING_HOURS = 15; + const UNASSIGN_HOURS = 30; - const WARNING_AFTER_HOURS = 15; // Send warning - const UNASSIGN_AFTER_HOURS = 30; // Unassign after warning window + console.log('🔍 Checking for inactive assigned issues...'); const issues = await github.paginate( github.rest.issues.listForRepo, { owner: context.repo.owner, repo: context.repo.repo, - state: "open", + state: 'open', per_page: 100, } ); @@ -36,48 +37,49 @@ jobs: // Skip PRs if (issue.pull_request) continue; - // Skip issues with no assignees + // Skip unassigned issues if (!issue.assignees || issue.assignees.length === 0) continue; - const lastUpdated = new Date(issue.updated_at); - const diffHours = (now - lastUpdated) / (1000 * 60 * 60); + const assignedAt = new Date(issue.updated_at); + const hoursInactive = (now - assignedAt) / (1000 * 60 * 60); + + console.log(`Issue #${issue.number}: ${hoursInactive.toFixed(1)} hours inactive`); - // Fetch comments to check for existing warning - const comments = await github.rest.issues.listComments({ + // Get comments to check for warning + const { data: comments } = await github.rest.issues.listComments({ owner: context.repo.owner, repo: context.repo.repo, issue_number: issue.number, }); - const warningComment = comments.data.find(c => - c.body.includes("⚠️ Inactivity Warning") + const hasWarning = comments.some(c => + c.body && c.body.includes('⏰ Inactivity Warning') ); - const assigneesMention = issue.assignees - .map(a => `@${a.login}`) - .join(", "); + const assigneeNames = issue.assignees.map(a => `@${a.login}`).join(', '); + const assigneeLogins = issue.assignees.map(a => a.login); - // 🔔 Send warning after 15 hours - if (diffHours >= WARNING_AFTER_HOURS && !warningComment) { + // Send warning after 15 hours (if not already warned) + if (hoursInactive >= WARNING_HOURS && hoursInactive < UNASSIGN_HOURS && !hasWarning) { await github.rest.issues.createComment({ owner: context.repo.owner, repo: context.repo.repo, issue_number: issue.number, - body: `⚠️ **Inactivity Warning** + body: `⏰ **Inactivity Warning** -${assigneesMention}, this issue has been inactive for **15 hours**. + Hi ${assigneeNames}! 👋 -Please provide an update within the next **15 hours**, or you may be **automatically unassigned**.` - }); + This issue has been inactive for **${Math.floor(hoursInactive)} hours**. - console.log(`Warning sent for issue #${issue.number}`); - continue; - } + Please provide an update within the next **15 hours**, or you may be **automatically unassigned** to give others a chance to contribute. - // 🚫 Unassign after 30 hours (only if warning was sent) - if (diffHours >= UNASSIGN_AFTER_HOURS && warningComment) { - const assigneeLogins = issue.assignees.map(a => a.login); + 💡 *If you need more time, just leave a comment to reset the timer!*` + }); + console.log(`⚠️ Warning sent for issue #${issue.number}`); + } + // Unassign after 30 hours (only if warning was sent) + if (hoursInactive >= UNASSIGN_HOURS && hasWarning) { await github.rest.issues.removeAssignees({ owner: context.repo.owner, repo: context.repo.repo, @@ -89,12 +91,16 @@ Please provide an update within the next **15 hours**, or you may be **automatic owner: context.repo.owner, repo: context.repo.repo, issue_number: issue.number, - body: `ℹ️ **Unassigned due to inactivity** + body: `🔓 **Unassigned Due to Inactivity** -You have been automatically unassigned after **30 hours of inactivity**. -Feel free to reassign yourself if you wish to continue working on this issue.` - }); + This issue has been unassigned after **30+ hours of inactivity**. + + The issue is now open for others to work on! 🙌 - console.log(`Unassigned contributors from issue #${issue.number}`); + *If you'd like to continue working on this, feel free to comment and request reassignment.*` + }); + console.log(`🚫 Unassigned issue #${issue.number}`); } - } \ No newline at end of file + } + + console.log('✅ Inactivity check complete!'); \ No newline at end of file diff --git a/CryptoHub b/CryptoHub deleted file mode 160000 index a0a03e1..0000000 --- a/CryptoHub +++ /dev/null @@ -1 +0,0 @@ -Subproject commit a0a03e1970c064296f411d32475148256987630b diff --git a/README.md b/README.md index 4ad2fb2..a46c0b9 100644 --- a/README.md +++ b/README.md @@ -323,6 +323,14 @@ npm run dev http://localhost:5173 ``` +

🧪 Running Tests

+ +This project uses **Vitest** for testing. To execute the test suite: + +```bash +npm test +``` + ## 🐳 Docker Setup (Alternative) For a containerized development environment: diff --git a/eslint.config.js b/eslint.config.js index cee1e2c..abb3462 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -2,6 +2,7 @@ import js from '@eslint/js' import globals from 'globals' import reactHooks from 'eslint-plugin-react-hooks' import reactRefresh from 'eslint-plugin-react-refresh' +import react from 'eslint-plugin-react' import { defineConfig, globalIgnores } from 'eslint/config' export default defineConfig([ @@ -13,6 +14,9 @@ export default defineConfig([ reactHooks.configs['recommended-latest'], reactRefresh.configs.vite, ], + plugins: { + react, + }, languageOptions: { ecmaVersion: 2020, globals: globals.browser, @@ -23,6 +27,7 @@ export default defineConfig([ }, }, rules: { + 'react/jsx-uses-vars': 'error', 'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }], }, }, diff --git a/package-lock.json b/package-lock.json index 6bd9ed6..347a8ec 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,9 +13,10 @@ "axios": "^1.13.2", "chart.js": "^4.5.1", "clsx": "^2.1.1", - "firebase": "^12.7.0", + "firebase": "^12.9.0", "framer-motion": "^12.24.0", "jspdf": "^4.0.0", + "lenis": "^1.3.17", "lucide-react": "^0.562.0", "react": "^18.3.1", "react-chartjs-2": "^5.3.1", @@ -30,22 +31,42 @@ }, "devDependencies": { "@eslint/js": "^9.33.0", + "@testing-library/dom": "^10.4.1", + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/react": "^16.3.2", "@types/react": "^19.1.10", "@types/react-dom": "^19.1.7", - "@vitejs/plugin-react": "^5.0.0", + "@vitejs/plugin-react": "^4.3.1", "autoprefixer": "^10.4.23", "eslint": "^9.33.0", + "eslint-plugin-react": "^7.37.5", "eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-refresh": "^0.4.20", "globals": "^16.3.0", + "jsdom": "^28.0.0", "postcss": "^8.5.6", "tailwindcss": "^3.4.19", - "vite": "^7.1.2" + "vite": "^5.4.2", + "vitest": "^4.0.18" }, "engines": { - "node": "24.x" + "node": "20.x" } }, + "node_modules/@acemir/cssom": { + "version": "0.9.31", + "resolved": "https://registry.npmjs.org/@acemir/cssom/-/cssom-0.9.31.tgz", + "integrity": "sha512-ZnR3GSaH+/vJ0YlHau21FjfLYjMpYVIzTD8M8vIEQvIGxeOXyXdzCI140rrCY862p/C/BbzWsjc1dgnM9mkoTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@adobe/css-tools": { + "version": "4.4.4", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.4.tgz", + "integrity": "sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg==", + "dev": true, + "license": "MIT" + }, "node_modules/@alloc/quick-lru": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", @@ -59,6 +80,61 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@asamuzakjp/css-color": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-4.1.2.tgz", + "integrity": "sha512-NfBUvBaYgKIuq6E/RBLY1m0IohzNHAYyaJGuTK79Z23uNwmz2jl1mPsC5ZxCCxylinKhT1Amn5oNTlx1wN8cQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@csstools/css-calc": "^3.0.0", + "@csstools/css-color-parser": "^4.0.1", + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0", + "lru-cache": "^11.2.5" + } + }, + "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": { + "version": "11.2.6", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.6.tgz", + "integrity": "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@asamuzakjp/dom-selector": { + "version": "6.7.8", + "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-6.7.8.tgz", + "integrity": "sha512-stisC1nULNc9oH5lakAj8MH88ZxeGxzyWNDfbdCxvJSJIvDsHNZqYvscGTgy/ysgXWLJPt6K/4t0/GjvtKcFJQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/nwsapi": "^2.3.9", + "bidi-js": "^1.0.3", + "css-tree": "^3.1.0", + "is-potential-custom-element-name": "^1.0.1", + "lru-cache": "^11.2.5" + } + }, + "node_modules/@asamuzakjp/dom-selector/node_modules/lru-cache": { + "version": "11.2.6", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.6.tgz", + "integrity": "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@asamuzakjp/nwsapi": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz", + "integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==", + "dev": true, + "license": "MIT" + }, "node_modules/@babel/code-frame": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", @@ -350,299 +426,414 @@ "node": ">=6.9.0" } }, + "node_modules/@csstools/color-helpers": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.0.1.tgz", + "integrity": "sha512-NmXRccUJMk2AWA5A7e5a//3bCIMyOu2hAtdRYrhPPHjDxINuCwX1w6rnIZ4xjLcp0ayv6h8Pc3X0eJUGiAAXHQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@csstools/css-calc": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.0.1.tgz", + "integrity": "sha512-bsDKIP6f4ta2DO9t+rAbSSwv4EMESXy5ZIvzQl1afmD6Z1XHkVu9ijcG9QR/qSgQS1dVa+RaQ/MfQ7FIB/Dn1Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.0.1.tgz", + "integrity": "sha512-vYwO15eRBEkeF6xjAno/KQ61HacNhfQuuU/eGwH67DplL0zD5ZixUa563phQvUelA07yDczIXdtmYojCphKJcw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^6.0.1", + "@csstools/css-calc": "^3.0.0" + }, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz", + "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-syntax-patches-for-csstree": { + "version": "1.0.27", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.0.27.tgz", + "integrity": "sha512-sxP33Jwg1bviSUXAV43cVYdmjt2TLnLXNqCWl9xmxHawWVjGz/kEbdkr7F9pxJNBN2Mh+dq0crgItbW6tQvyow==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0" + }, + "node_modules/@csstools/css-tokenizer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz", + "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + } + }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz", - "integrity": "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", "cpu": [ "ppc64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "aix" ], "engines": { - "node": ">=18" + "node": ">=12" } }, "node_modules/@esbuild/android-arm": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.2.tgz", - "integrity": "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", "cpu": [ "arm" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "android" ], "engines": { - "node": ">=18" + "node": ">=12" } }, "node_modules/@esbuild/android-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.2.tgz", - "integrity": "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", "cpu": [ "arm64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "android" ], "engines": { - "node": ">=18" + "node": ">=12" } }, "node_modules/@esbuild/android-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.2.tgz", - "integrity": "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", "cpu": [ "x64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "android" ], "engines": { - "node": ">=18" + "node": ">=12" } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz", - "integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", "cpu": [ "arm64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "darwin" ], "engines": { - "node": ">=18" + "node": ">=12" } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.2.tgz", - "integrity": "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", "cpu": [ "x64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "darwin" ], "engines": { - "node": ">=18" + "node": ">=12" } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.2.tgz", - "integrity": "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", "cpu": [ "arm64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "freebsd" ], "engines": { - "node": ">=18" + "node": ">=12" } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.2.tgz", - "integrity": "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", "cpu": [ "x64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "freebsd" ], "engines": { - "node": ">=18" + "node": ">=12" } }, "node_modules/@esbuild/linux-arm": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.2.tgz", - "integrity": "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", "cpu": [ "arm" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=18" + "node": ">=12" } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz", - "integrity": "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", "cpu": [ "arm64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=18" + "node": ">=12" } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.2.tgz", - "integrity": "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", "cpu": [ "ia32" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=18" + "node": ">=12" } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.2.tgz", - "integrity": "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", "cpu": [ "loong64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=18" + "node": ">=12" } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.2.tgz", - "integrity": "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", "cpu": [ "mips64el" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=18" + "node": ">=12" } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz", - "integrity": "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", "cpu": [ "ppc64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=18" + "node": ">=12" } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz", - "integrity": "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", "cpu": [ "riscv64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=18" + "node": ">=12" } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz", - "integrity": "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", "cpu": [ "s390x" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=18" + "node": ">=12" } }, "node_modules/@esbuild/linux-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.2.tgz", - "integrity": "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", "cpu": [ "x64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=18" + "node": ">=12" } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.2.tgz", - "integrity": "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz", + "integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==", "cpu": [ "arm64" ], @@ -657,26 +848,25 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.2.tgz", - "integrity": "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", "cpu": [ "x64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "netbsd" ], "engines": { - "node": ">=18" + "node": ">=12" } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.2.tgz", - "integrity": "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz", + "integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==", "cpu": [ "arm64" ], @@ -691,26 +881,25 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.2.tgz", - "integrity": "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", "cpu": [ "x64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "openbsd" ], "engines": { - "node": ">=18" + "node": ">=12" } }, "node_modules/@esbuild/openharmony-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.2.tgz", - "integrity": "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz", + "integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==", "cpu": [ "arm64" ], @@ -725,71 +914,67 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz", - "integrity": "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", "cpu": [ "x64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "sunos" ], "engines": { - "node": ">=18" + "node": ">=12" } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.2.tgz", - "integrity": "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", "cpu": [ "arm64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "win32" ], "engines": { - "node": ">=18" + "node": ">=12" } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.2.tgz", - "integrity": "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", "cpu": [ "ia32" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "win32" ], "engines": { - "node": ">=18" + "node": ">=12" } }, "node_modules/@esbuild/win32-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz", - "integrity": "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", "cpu": [ "x64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "win32" ], "engines": { - "node": ">=18" + "node": ">=12" } }, "node_modules/@eslint-community/eslint-utils": { @@ -949,10 +1134,28 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@exodus/bytes": { + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.12.0.tgz", + "integrity": "sha512-BuCOHA/EJdPN0qQ5MdgAiJSt9fYDHbghlgrj33gRdy/Yp1/FMCDhU6vJfcKrLC0TPWGSrfH3vYXBQWmFHxlddw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@noble/hashes": "^1.8.0 || ^2.0.0" + }, + "peerDependenciesMeta": { + "@noble/hashes": { + "optional": true + } + } + }, "node_modules/@firebase/ai": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/@firebase/ai/-/ai-2.6.1.tgz", - "integrity": "sha512-qJd9bpABqsanFnwdbjZEDbKKr1jRtuUZ+cHyNBLWsxobH4pd73QncvuO3XlMq4eKBLlg1f5jNdFpJ3G3ABu2Tg==", + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@firebase/ai/-/ai-2.8.0.tgz", + "integrity": "sha512-grWYGFPsSo+pt+6CYeKR0kWnUfoLLS3xgWPvNrhAS5EPxl6xWq7+HjDZqX24yLneETyl45AVgDsTbVgxeWeRfg==", "license": "Apache-2.0", "dependencies": { "@firebase/app-check-interop-types": "0.3.3", @@ -1008,9 +1211,9 @@ "license": "Apache-2.0" }, "node_modules/@firebase/app": { - "version": "0.14.6", - "resolved": "https://registry.npmjs.org/@firebase/app/-/app-0.14.6.tgz", - "integrity": "sha512-4uyt8BOrBsSq6i4yiOV/gG6BnnrvTeyymlNcaN/dKvyU1GoolxAafvIvaNP1RCGPlNab3OuE4MKUQuv2lH+PLQ==", + "version": "0.14.8", + "resolved": "https://registry.npmjs.org/@firebase/app/-/app-0.14.8.tgz", + "integrity": "sha512-WiE9uCGRLUnShdjb9iP20sA3ToWrBbNXr14/N5mow7Nls9dmKgfGaGX5cynLvrltxq2OrDLh1VDNaUgsnS/k/g==", "license": "Apache-2.0", "dependencies": { "@firebase/component": "0.7.0", @@ -1074,12 +1277,12 @@ "license": "Apache-2.0" }, "node_modules/@firebase/app-compat": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/@firebase/app-compat/-/app-compat-0.5.6.tgz", - "integrity": "sha512-YYGARbutghQY4zZUWMYia0ib0Y/rb52y72/N0z3vglRHL7ii/AaK9SA7S/dzScVOlCdnbHXz+sc5Dq+r8fwFAg==", + "version": "0.5.8", + "resolved": "https://registry.npmjs.org/@firebase/app-compat/-/app-compat-0.5.8.tgz", + "integrity": "sha512-4De6SUZ36zozl9kh5rZSxKWULpgty27rMzZ6x+xkoo7+NWyhWyFdsdvhFsWhTw/9GGj0wXIcbTjwHYCUIUuHyg==", "license": "Apache-2.0", "dependencies": { - "@firebase/app": "0.14.6", + "@firebase/app": "0.14.8", "@firebase/component": "0.7.0", "@firebase/logger": "0.5.0", "@firebase/util": "1.13.0", @@ -1229,9 +1432,9 @@ } }, "node_modules/@firebase/firestore": { - "version": "4.9.3", - "resolved": "https://registry.npmjs.org/@firebase/firestore/-/firestore-4.9.3.tgz", - "integrity": "sha512-RVuvhcQzs1sD5Osr2naQS71H0bQMbSnib16uOWAKk3GaKb/WBPyCYSr2Ry7MqlxDP/YhwknUxECL07lw9Rq1nA==", + "version": "4.11.0", + "resolved": "https://registry.npmjs.org/@firebase/firestore/-/firestore-4.11.0.tgz", + "integrity": "sha512-Zb88s8rssBd0J2Tt+NUXMPt2sf+Dq7meatKiJf5t9oto1kZ8w9gK59Koe1uPVbaKfdgBp++N/z0I4G/HamyEhg==", "license": "Apache-2.0", "dependencies": { "@firebase/component": "0.7.0", @@ -1250,13 +1453,13 @@ } }, "node_modules/@firebase/firestore-compat": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/@firebase/firestore-compat/-/firestore-compat-0.4.3.tgz", - "integrity": "sha512-1ylF/njF68Pmb6p0erP0U78XQv1w77Wap4bUmqZ7ZVkmN1oMgplyu0TyirWtCBoKFRV2+SUZfWXvIij/z39LYg==", + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/@firebase/firestore-compat/-/firestore-compat-0.4.5.tgz", + "integrity": "sha512-yVX1CkVvqBI4qbA56uZo42xFA4TNU0ICQ+9AFDvYq9U9Xu6iAx9lFDAk/tN+NGereQQXXCSnpISwc/oxsQqPLA==", "license": "Apache-2.0", "dependencies": { "@firebase/component": "0.7.0", - "@firebase/firestore": "4.9.3", + "@firebase/firestore": "4.11.0", "@firebase/firestore-types": "3.0.3", "@firebase/util": "1.13.0", "tslib": "^2.1.0" @@ -1454,9 +1657,9 @@ "license": "Apache-2.0" }, "node_modules/@firebase/remote-config": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/@firebase/remote-config/-/remote-config-0.7.0.tgz", - "integrity": "sha512-dX95X6WlW7QlgNd7aaGdjAIZUiQkgWgNS+aKNu4Wv92H1T8Ue/NDUjZHd9xb8fHxLXIHNZeco9/qbZzr500MjQ==", + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@firebase/remote-config/-/remote-config-0.8.0.tgz", + "integrity": "sha512-sJz7C2VACeE257Z/3kY9Ap2WXbFsgsDLfaGfZmmToKAK39ipXxFan+vzB9CSbF6mP7bzjyzEnqPcMXhAnYE6fQ==", "license": "Apache-2.0", "dependencies": { "@firebase/component": "0.7.0", @@ -1470,14 +1673,14 @@ } }, "node_modules/@firebase/remote-config-compat": { - "version": "0.2.20", - "resolved": "https://registry.npmjs.org/@firebase/remote-config-compat/-/remote-config-compat-0.2.20.tgz", - "integrity": "sha512-P/ULS9vU35EL9maG7xp66uljkZgcPMQOxLj3Zx2F289baTKSInE6+YIkgHEi1TwHoddC/AFePXPpshPlEFkbgg==", + "version": "0.2.21", + "resolved": "https://registry.npmjs.org/@firebase/remote-config-compat/-/remote-config-compat-0.2.21.tgz", + "integrity": "sha512-9+lm0eUycxbu8GO25JfJe4s6R2xlDqlVt0CR6CvN9E6B4AFArEV4qfLoDVRgIEB7nHKwvH2nYRocPWfmjRQTnw==", "license": "Apache-2.0", "dependencies": { "@firebase/component": "0.7.0", "@firebase/logger": "0.5.0", - "@firebase/remote-config": "0.7.0", + "@firebase/remote-config": "0.8.0", "@firebase/remote-config-types": "0.5.0", "@firebase/util": "1.13.0", "tslib": "^2.1.0" @@ -1808,11 +2011,10 @@ } }, "node_modules/@rolldown/pluginutils": { - "version": "1.0.0-beta.53", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.53.tgz", - "integrity": "sha512-vENRlFU4YbrwVqNDZ7fLvy+JR1CRkyr01jhSiDpE1u6py3OMzQfztQU2jxykW3ALNxO4kSlqIDeYyD0Y9RcQeQ==", - "dev": true, - "license": "MIT" + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.54.0", @@ -2122,6 +2324,13 @@ "win32" ] }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, "node_modules/@tanstack/query-core": { "version": "5.90.17", "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.90.17.tgz", @@ -2148,6 +2357,88 @@ "react": "^18 || ^19" } }, + "node_modules/@testing-library/dom": { + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", + "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "picocolors": "1.1.1", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@testing-library/jest-dom": { + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz", + "integrity": "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@adobe/css-tools": "^4.4.0", + "aria-query": "^5.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.6.3", + "picocolors": "^1.1.1", + "redent": "^3.0.0" + }, + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", + "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@testing-library/react": { + "version": "16.3.2", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.2.tgz", + "integrity": "sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@testing-library/dom": "^10.0.0", + "@types/react": "^18.0.0 || ^19.0.0", + "@types/react-dom": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@types/aria-query": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -2193,6 +2484,24 @@ "@babel/types": "^7.28.2" } }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -2257,26 +2566,109 @@ "optional": true }, "node_modules/@vitejs/plugin-react": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.1.2.tgz", - "integrity": "sha512-EcA07pHJouywpzsoTUqNh5NwGayl2PPVEJKUSinGGSxFGYn+shYbqMGBg6FXDqgXum9Ou/ecb+411ssw8HImJQ==", + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", "dev": true, - "license": "MIT", "dependencies": { - "@babel/core": "^7.28.5", + "@babel/core": "^7.28.0", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", - "@rolldown/pluginutils": "1.0.0-beta.53", + "@rolldown/pluginutils": "1.0.0-beta.27", "@types/babel__core": "^7.20.5", - "react-refresh": "^0.18.0" + "react-refresh": "^0.17.0" }, "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": "^14.18.0 || >=16.0.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, + "node_modules/@vitest/expect": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.18.tgz", + "integrity": "sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.0.18", + "@vitest/utils": "4.0.18", + "chai": "^6.2.1", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.0.18.tgz", + "integrity": "sha512-P24GK3GulZWC5tz87ux0m8OADrQIUVDPIjjj65vBXYG17ZeU3qD7r+MNZ1RNv4l8CGU2vtTRqixrOi9fYk/yKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.0.18.tgz", + "integrity": "sha512-rpk9y12PGa22Jg6g5M3UVVnTS7+zycIGk9ZNGN+m6tZHKQb7jrP7/77WfZy13Y/EUDd52NDsLRQhYKtv7XfPQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.0.18", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.0.18.tgz", + "integrity": "sha512-PCiV0rcl7jKQjbgYqjtakly6T1uwv/5BQ9SwBLekVg/EaYeQFPiXcgrC2Y7vDMA8dM1SUEAEV82kgSQIlXNMvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.0.18", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.0.18.tgz", + "integrity": "sha512-cbQt3PTSD7P2OARdVW3qWER5EGq7PHlvE+QfzSC0lbwO+xnt7+XH06ZzFjFRgzUX//JmpxrCu92VdwvEPlWSNw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.0.18.tgz", + "integrity": "sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.0.18", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, "node_modules/acorn": { "version": "8.15.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", @@ -2300,6 +2692,16 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, "node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -2387,6 +2789,174 @@ "dev": true, "license": "Python-2.0" }, + "node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", + "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "is-array-buffer": "^3.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-includes": { + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz", + "integrity": "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.0", + "es-object-atoms": "^1.1.1", + "get-intrinsic": "^1.3.0", + "is-string": "^1.1.1", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.findlast": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz", + "integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flat": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz", + "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flatmap": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz", + "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.tosorted": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz", + "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3", + "es-errors": "^1.3.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", + "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "is-array-buffer": "^3.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/async-function": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", + "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -2430,6 +3000,22 @@ "postcss": "^8.1.0" } }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/axios": { "version": "1.13.2", "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz", @@ -2468,6 +3054,16 @@ "baseline-browser-mapping": "dist/cli.js" } }, + "node_modules/bidi-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", + "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "require-from-string": "^2.0.2" + } + }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", @@ -2539,6 +3135,25 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/call-bind": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "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", @@ -2552,6 +3167,23 @@ "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==", + "dev": true, + "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/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -2613,6 +3245,16 @@ "node": ">=10.0.0" } }, + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -2800,6 +3442,27 @@ "utrie": "^1.0.2" } }, + "node_modules/css-tree": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.1.0.tgz", + "integrity": "sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "mdn-data": "2.12.2", + "source-map-js": "^1.0.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/css.escape": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "dev": true, + "license": "MIT" + }, "node_modules/cssesc": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", @@ -2813,52 +3476,199 @@ "node": ">=4" } }, + "node_modules/cssstyle": { + "version": "5.3.7", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-5.3.7.tgz", + "integrity": "sha512-7D2EPVltRrsTkhpQmksIu+LxeWAIEk6wRDMJ1qljlv+CKHJM+cJLlfhWIzNA44eAsHXSNe3+vO6DW1yCYx8SuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^4.1.1", + "@csstools/css-syntax-patches-for-csstree": "^1.0.21", + "css-tree": "^3.1.0", + "lru-cache": "^11.2.4" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/cssstyle/node_modules/lru-cache": { + "version": "11.2.6", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.6.tgz", + "integrity": "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, "node_modules/csstype": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", "license": "MIT" }, - "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "node_modules/data-urls": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz", + "integrity": "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==", "dev": true, "license": "MIT", "dependencies": { - "ms": "^2.1.3" + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.0" }, "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" } }, - "node_modules/deep-is": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "node_modules/data-view-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", + "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", "dev": true, - "license": "MIT" - }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, "engines": { - "node": ">=0.4.0" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/didyoumean": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", - "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "node_modules/data-view-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", + "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", "dev": true, - "license": "Apache-2.0" + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/inspect-js" + } + }, + "node_modules/data-view-byte-offset": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", + "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true, + "license": "MIT" + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "dev": true, + "license": "Apache-2.0" }, "node_modules/dlv": { "version": "1.1.3", @@ -2867,6 +3677,26 @@ "dev": true, "license": "MIT" }, + "node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "dev": true, + "license": "MIT" + }, "node_modules/dompurify": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.1.tgz", @@ -2904,6 +3734,88 @@ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "license": "MIT" }, + "node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-abstract": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.1.tgz", + "integrity": "sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.2", + "arraybuffer.prototype.slice": "^1.0.4", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "data-view-buffer": "^1.0.2", + "data-view-byte-length": "^1.0.2", + "data-view-byte-offset": "^1.0.1", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-set-tostringtag": "^2.1.0", + "es-to-primitive": "^1.3.0", + "function.prototype.name": "^1.1.8", + "get-intrinsic": "^1.3.0", + "get-proto": "^1.0.1", + "get-symbol-description": "^1.1.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "internal-slot": "^1.1.0", + "is-array-buffer": "^3.0.5", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.2", + "is-negative-zero": "^2.0.3", + "is-regex": "^1.2.1", + "is-set": "^2.0.3", + "is-shared-array-buffer": "^1.0.4", + "is-string": "^1.1.1", + "is-typed-array": "^1.1.15", + "is-weakref": "^1.1.1", + "math-intrinsics": "^1.1.0", + "object-inspect": "^1.13.4", + "object-keys": "^1.1.1", + "object.assign": "^4.1.7", + "own-keys": "^1.0.1", + "regexp.prototype.flags": "^1.5.4", + "safe-array-concat": "^1.1.3", + "safe-push-apply": "^1.0.0", + "safe-regex-test": "^1.1.0", + "set-proto": "^1.0.0", + "stop-iteration-iterator": "^1.1.0", + "string.prototype.trim": "^1.2.10", + "string.prototype.trimend": "^1.0.9", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.3", + "typed-array-byte-length": "^1.0.3", + "typed-array-byte-offset": "^1.0.4", + "typed-array-length": "^1.0.7", + "unbox-primitive": "^1.1.0", + "which-typed-array": "^1.1.19" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/es-define-property": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", @@ -2922,6 +3834,41 @@ "node": ">= 0.4" } }, + "node_modules/es-iterator-helpers": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.2.2.tgz", + "integrity": "sha512-BrUQ0cPTB/IwXj23HtwHjS9n7O4h9FX94b4xc5zlTHxeLgTAdzYUDyy6KdExAl9lbN5rtfe44xpjpmj9grxs5w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.1", + "es-errors": "^1.3.0", + "es-set-tostringtag": "^2.1.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.3.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "iterator.prototype": "^1.1.5", + "safe-array-concat": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, "node_modules/es-object-atoms": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", @@ -2949,46 +3896,73 @@ "node": ">= 0.4" } }, + "node_modules/es-shim-unscopables": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz", + "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-to-primitive": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", + "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7", + "is-date-object": "^1.0.5", + "is-symbol": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/esbuild": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz", - "integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", "dev": true, "hasInstallScript": true, - "license": "MIT", "bin": { "esbuild": "bin/esbuild" }, "engines": { - "node": ">=18" + "node": ">=12" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.27.2", - "@esbuild/android-arm": "0.27.2", - "@esbuild/android-arm64": "0.27.2", - "@esbuild/android-x64": "0.27.2", - "@esbuild/darwin-arm64": "0.27.2", - "@esbuild/darwin-x64": "0.27.2", - "@esbuild/freebsd-arm64": "0.27.2", - "@esbuild/freebsd-x64": "0.27.2", - "@esbuild/linux-arm": "0.27.2", - "@esbuild/linux-arm64": "0.27.2", - "@esbuild/linux-ia32": "0.27.2", - "@esbuild/linux-loong64": "0.27.2", - "@esbuild/linux-mips64el": "0.27.2", - "@esbuild/linux-ppc64": "0.27.2", - "@esbuild/linux-riscv64": "0.27.2", - "@esbuild/linux-s390x": "0.27.2", - "@esbuild/linux-x64": "0.27.2", - "@esbuild/netbsd-arm64": "0.27.2", - "@esbuild/netbsd-x64": "0.27.2", - "@esbuild/openbsd-arm64": "0.27.2", - "@esbuild/openbsd-x64": "0.27.2", - "@esbuild/openharmony-arm64": "0.27.2", - "@esbuild/sunos-x64": "0.27.2", - "@esbuild/win32-arm64": "0.27.2", - "@esbuild/win32-ia32": "0.27.2", - "@esbuild/win32-x64": "0.27.2" + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" } }, "node_modules/escalade": { @@ -3073,6 +4047,39 @@ } } }, + "node_modules/eslint-plugin-react": { + "version": "7.37.5", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz", + "integrity": "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-includes": "^3.1.8", + "array.prototype.findlast": "^1.2.5", + "array.prototype.flatmap": "^1.3.3", + "array.prototype.tosorted": "^1.1.4", + "doctrine": "^2.1.0", + "es-iterator-helpers": "^1.2.1", + "estraverse": "^5.3.0", + "hasown": "^2.0.2", + "jsx-ast-utils": "^2.4.1 || ^3.0.0", + "minimatch": "^3.1.2", + "object.entries": "^1.1.9", + "object.fromentries": "^2.0.8", + "object.values": "^1.2.1", + "prop-types": "^15.8.1", + "resolve": "^2.0.0-next.5", + "semver": "^6.3.1", + "string.prototype.matchall": "^4.0.12", + "string.prototype.repeat": "^1.0.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" + } + }, "node_modules/eslint-plugin-react-hooks": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz", @@ -3096,6 +4103,30 @@ "eslint": ">=8.40" } }, + "node_modules/eslint-plugin-react/node_modules/resolve": { + "version": "2.0.0-next.6", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.6.tgz", + "integrity": "sha512-3JmVl5hMGtJ3kMmB3zi3DL25KfkCEyy3Tw7Gmw7z5w8M9WlwoPFnIvwChzu1+cF3iaK3sp18hhPz8ANeimdJfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", + "node-exports-info": "^1.6.0", + "object-keys": "^1.1.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/eslint-scope": { "version": "8.4.0", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", @@ -3180,6 +4211,16 @@ "node": ">=4.0" } }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, "node_modules/esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", @@ -3190,6 +4231,16 @@ "node": ">=0.10.0" } }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -3324,26 +4375,26 @@ } }, "node_modules/firebase": { - "version": "12.7.0", - "resolved": "https://registry.npmjs.org/firebase/-/firebase-12.7.0.tgz", - "integrity": "sha512-ZBZg9jFo8uH4Emd7caOqtalKJfDGHnHQSrCPiqRAdTFQd0wL3ERilUBfhnhBLnlernugkN/o7nJa0p+sE71Izg==", + "version": "12.9.0", + "resolved": "https://registry.npmjs.org/firebase/-/firebase-12.9.0.tgz", + "integrity": "sha512-CwwTYoqZg6KxygPOaaJqIc4aoLvo0RCRrXoln9GoxLE8QyAwTydBaSLGVlR4WPcuOgN3OEL0tJLT1H4IU/dv7w==", "license": "Apache-2.0", "dependencies": { - "@firebase/ai": "2.6.1", + "@firebase/ai": "2.8.0", "@firebase/analytics": "0.10.19", "@firebase/analytics-compat": "0.2.25", - "@firebase/app": "0.14.6", + "@firebase/app": "0.14.8", "@firebase/app-check": "0.11.0", "@firebase/app-check-compat": "0.4.0", - "@firebase/app-compat": "0.5.6", + "@firebase/app-compat": "0.5.8", "@firebase/app-types": "0.9.3", "@firebase/auth": "1.12.0", "@firebase/auth-compat": "0.6.2", "@firebase/data-connect": "0.3.12", "@firebase/database": "1.1.0", "@firebase/database-compat": "2.1.0", - "@firebase/firestore": "4.9.3", - "@firebase/firestore-compat": "0.4.3", + "@firebase/firestore": "4.11.0", + "@firebase/firestore-compat": "0.4.5", "@firebase/functions": "0.13.1", "@firebase/functions-compat": "0.4.1", "@firebase/installations": "0.6.19", @@ -3352,8 +4403,8 @@ "@firebase/messaging-compat": "0.2.23", "@firebase/performance": "0.7.9", "@firebase/performance-compat": "0.2.22", - "@firebase/remote-config": "0.7.0", - "@firebase/remote-config-compat": "0.2.20", + "@firebase/remote-config": "0.8.0", + "@firebase/remote-config-compat": "0.2.21", "@firebase/storage": "0.14.0", "@firebase/storage-compat": "0.4.0", "@firebase/util": "1.13.0" @@ -3424,6 +4475,22 @@ } } }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/form-data": { "version": "4.0.5", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", @@ -3505,26 +4572,67 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/gensync": { - "version": "1.0.0-beta.2", - "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "node_modules/function.prototype.name": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz", + "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", "dev": true, "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "functions-have-names": "^1.2.3", + "hasown": "^2.0.2", + "is-callable": "^1.2.7" + }, "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "license": "ISC", - "engines": { - "node": "6.* || 8.* || >= 10.*" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/get-intrinsic": { + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/generator-function": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", + "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "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==", @@ -3561,6 +4669,24 @@ "node": ">= 0.4" } }, + "node_modules/get-symbol-description": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", + "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -3587,6 +4713,23 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/goober": { "version": "2.1.18", "resolved": "https://registry.npmjs.org/goober/-/goober-2.1.18.tgz", @@ -3608,6 +4751,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/has-bigints": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", + "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -3618,6 +4774,35 @@ "node": ">=8" } }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", + "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.0" + }, + "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", @@ -3657,6 +4842,19 @@ "node": ">= 0.4" } }, + "node_modules/html-encoding-sniffer": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz", + "integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.6.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, "node_modules/html2canvas": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz", @@ -3677,6 +4875,34 @@ "integrity": "sha512-Pysuw9XpUq5dVc/2SMHpuTY01RFl8fttgcyunjL7eEMhGM3cI4eOmiCycJDVCo/7O7ClfQD3SaI6ftDzqOXYMA==", "license": "MIT" }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/idb": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/idb/-/idb-7.1.1.tgz", @@ -3720,6 +4946,31 @@ "node": ">=0.8.19" } }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/internal-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", + "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "hasown": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/invariant": { "version": "2.2.4", "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", @@ -3735,6 +4986,60 @@ "integrity": "sha512-DRebOWuqDvxunfkNJAlc3IzWIPD5xVxwUNbHr7xKB8E6aLJxIPfNX3CoMJghcFjpv6RWQsrcJbghtEwSPoJqMA==", "license": "MIT" }, + "node_modules/is-array-buffer": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", + "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-async-function": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", + "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "async-function": "^1.0.0", + "call-bound": "^1.0.3", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bigint": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", + "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-bigints": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-binary-path": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", @@ -3748,6 +5053,36 @@ "node": ">=8" } }, + "node_modules/is-boolean-object": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", + "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-core-module": { "version": "2.16.1", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", @@ -3764,6 +5099,41 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-data-view": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", + "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", + "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -3774,6 +5144,22 @@ "node": ">=0.10.0" } }, + "node_modules/is-finalizationregistry": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", + "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", @@ -3783,6 +5169,26 @@ "node": ">=8" } }, + "node_modules/is-generator-function": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", + "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.4", + "generator-function": "^2.0.0", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -3796,23 +5202,243 @@ "node": ">=0.10.0" } }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "node_modules/is-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", "dev": true, "license": "MIT", "engines": { - "node": ">=0.12.0" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "node_modules/is-negative-zero": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", "dev": true, - "license": "ISC" - }, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-number-object": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", + "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-set": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", + "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-string": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", + "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", + "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-symbols": "^1.1.0", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakmap": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", + "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakset": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", + "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/iterator.prototype": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz", + "integrity": "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "get-proto": "^1.0.0", + "has-symbols": "^1.1.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/jiti": { "version": "1.21.7", "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", @@ -3842,6 +5468,46 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsdom": { + "version": "28.0.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-28.0.0.tgz", + "integrity": "sha512-KDYJgZ6T2TKdU8yBfYueq5EPG/EylMsBvCaenWMJb2OXmjgczzwveRCoJ+Hgj1lXPDyasvrgneSn4GBuR1hYyA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@acemir/cssom": "^0.9.31", + "@asamuzakjp/dom-selector": "^6.7.6", + "@exodus/bytes": "^1.11.0", + "cssstyle": "^5.3.7", + "data-urls": "^7.0.0", + "decimal.js": "^10.6.0", + "html-encoding-sniffer": "^6.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.6", + "is-potential-custom-element-name": "^1.0.1", + "parse5": "^8.0.0", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^6.0.0", + "undici": "^7.20.0", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^8.0.1", + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.0", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, "node_modules/jsesc": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", @@ -3906,6 +5572,22 @@ "html2canvas": "^1.0.0-rc.5" } }, + "node_modules/jsx-ast-utils": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", + "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-includes": "^3.1.6", + "array.prototype.flat": "^1.3.1", + "object.assign": "^4.1.4", + "object.values": "^1.1.6" + }, + "engines": { + "node": ">=4.0" + } + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -3916,6 +5598,32 @@ "json-buffer": "3.0.1" } }, + "node_modules/lenis": { + "version": "1.3.17", + "resolved": "https://registry.npmjs.org/lenis/-/lenis-1.3.17.tgz", + "integrity": "sha512-k9T9rgcxne49ggJOvXCraWn5dt7u2mO+BNkhyu6yxuEnm9c092kAW5Bus5SO211zUvx7aCCEtzy9UWr0RB+oJw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/darkroomengineering" + }, + "peerDependencies": { + "@nuxt/kit": ">=3.0.0", + "react": ">=17.0.0", + "vue": ">=3.0.0" + }, + "peerDependenciesMeta": { + "@nuxt/kit": { + "optional": true + }, + "react": { + "optional": true + }, + "vue": { + "optional": true + } + } + }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -4028,6 +5736,26 @@ "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true, + "license": "MIT", + "bin": { + "lz-string": "bin/bin.js" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -4037,6 +5765,13 @@ "node": ">= 0.4" } }, + "node_modules/mdn-data": { + "version": "2.12.2", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.12.2.tgz", + "integrity": "sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==", + "dev": true, + "license": "CC0-1.0" + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -4082,6 +5817,16 @@ "node": ">= 0.6" } }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -4155,6 +5900,25 @@ "dev": true, "license": "MIT" }, + "node_modules/node-exports-info": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/node-exports-info/-/node-exports-info-1.6.0.tgz", + "integrity": "sha512-pyFS63ptit/P5WqUkt+UUfe+4oevH+bFeIiPPdfb0pFeYEu/1ELnJu5l+5EcTKYL5M7zaAa7S8ddywgXypqKCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "array.prototype.flatmap": "^1.3.3", + "es-errors": "^1.3.0", + "object.entries": "^1.1.9", + "semver": "^6.3.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/node-releases": { "version": "2.0.27", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", @@ -4192,33 +5956,160 @@ "node": ">= 6" } }, - "node_modules/optionator": { - "version": "0.9.4", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", - "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "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==", "dev": true, "license": "MIT", - "dependencies": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.5" - }, "engines": { - "node": ">= 0.8.0" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", "dev": true, "license": "MIT", - "dependencies": { - "yocto-queue": "^0.1.0" - }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.entries": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.9.tgz", + "integrity": "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.fromentries": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", + "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.values": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz", + "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/obug": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", + "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT" + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/own-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", + "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.2.6", + "object-keys": "^1.1.1", + "safe-push-apply": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, "engines": { "node": ">=10" }, @@ -4261,6 +6152,19 @@ "node": ">=6" } }, + "node_modules/parse5": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.0.tgz", + "integrity": "sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -4288,6 +6192,13 @@ "dev": true, "license": "MIT" }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, "node_modules/performance-now": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", @@ -4335,6 +6246,16 @@ "node": ">= 6" } }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/postcss": { "version": "8.5.6", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", @@ -4508,6 +6429,53 @@ "node": ">= 0.8.0" } }, + "node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "dev": true, + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/prop-types/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "dev": true, + "license": "MIT" + }, "node_modules/protobufjs": { "version": "7.5.4", "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.4.tgz", @@ -4670,12 +6638,18 @@ "react": "*" } }, + "node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true, + "license": "MIT" + }, "node_modules/react-refresh": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", - "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==", + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", "dev": true, - "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -4745,6 +6719,43 @@ "node": ">=8.10.0" } }, + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/reflect.getprototypeof": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", + "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.1", + "which-builtin-type": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/regenerator-runtime": { "version": "0.13.11", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", @@ -4752,6 +6763,27 @@ "license": "MIT", "optional": true }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", + "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -4761,6 +6793,16 @@ "node": ">=0.10.0" } }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/resolve": { "version": "1.22.11", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", @@ -4879,6 +6921,26 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/safe-array-concat": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", + "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "has-symbols": "^1.1.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -4899,6 +6961,54 @@ ], "license": "MIT" }, + "node_modules/safe-push-apply": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", + "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, "node_modules/scheduler": { "version": "0.23.2", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", @@ -4918,35 +7028,167 @@ "semver": "bin/semver.js" } }, - "node_modules/shallowequal": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz", - "integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==", - "license": "MIT" - }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", "dev": true, "license": "MIT", "dependencies": { - "shebang-regex": "^3.0.0" + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" }, "engines": { - "node": ">=8" + "node": ">= 0.4" } }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "node_modules/set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", "dev": true, "license": "MIT", - "engines": { + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-proto": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", + "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/shallowequal": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz", + "integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==", + "license": "MIT" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { "node": ">=8" } }, + "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==", + "dev": true, + "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.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "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==", + "dev": true, + "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==", + "dev": true, + "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/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -4957,6 +7199,13 @@ "node": ">=0.10.0" } }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, "node_modules/stackblur-canvas": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/stackblur-canvas/-/stackblur-canvas-2.7.0.tgz", @@ -4967,6 +7216,27 @@ "node": ">=0.1.14" } }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/stop-iteration-iterator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", + "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "internal-slot": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", @@ -4981,6 +7251,104 @@ "node": ">=8" } }, + "node_modules/string.prototype.matchall": { + "version": "4.0.12", + "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", + "integrity": "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.6", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "regexp.prototype.flags": "^1.5.3", + "set-function-name": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.repeat": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz", + "integrity": "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" + } + }, + "node_modules/string.prototype.trim": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", + "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-data-property": "^1.1.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-object-atoms": "^1.0.0", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", + "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", @@ -4993,6 +7361,19 @@ "node": ">=8" } }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -5065,6 +7446,13 @@ "node": ">=12.0.0" } }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, "node_modules/tailwind-merge": { "version": "3.4.0", "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.4.0.tgz", @@ -5146,6 +7534,23 @@ "node": ">=0.8" } }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz", + "integrity": "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/tinyglobby": { "version": "0.2.15", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", @@ -5194,6 +7599,36 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/tinyrainbow": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.0.3.tgz", + "integrity": "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tldts": { + "version": "7.0.23", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.23.tgz", + "integrity": "sha512-ASdhgQIBSay0R/eXggAkQ53G4nTJqTXqC2kbaBbdDwM7SkjyZyO0OaaN1/FH7U/yCeqOHDwFO5j8+Os/IS1dXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^7.0.23" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "7.0.23", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.23.tgz", + "integrity": "sha512-0g9vrtDQLrNIiCj22HSe9d4mLVG3g5ph5DZ8zCKBr4OtrspmNB6ss7hVyzArAeE88ceZocIEGkyW1Ime7fxPtQ==", + "dev": true, + "license": "MIT" + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -5207,6 +7642,32 @@ "node": ">=8.0" } }, + "node_modules/tough-cookie": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.0.tgz", + "integrity": "sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^7.0.5" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz", + "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/ts-interface-checker": { "version": "0.1.13", "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", @@ -5233,34 +7694,141 @@ "node": ">= 0.8.0" } }, - "node_modules/undici-types": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", - "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", - "license": "MIT" - }, - "node_modules/update-browserslist-db": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", - "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "node_modules/typed-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], "license": "MIT", "dependencies": { - "escalade": "^3.2.0", + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", + "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", + "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.15", + "reflect.getprototypeof": "^1.0.9" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz", + "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "is-typed-array": "^1.1.13", + "possible-typed-array-names": "^1.0.0", + "reflect.getprototypeof": "^1.0.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/unbox-primitive": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", + "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-bigints": "^1.0.2", + "has-symbols": "^1.1.0", + "which-boxed-primitive": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/undici": { + "version": "7.21.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.21.0.tgz", + "integrity": "sha512-Hn2tCQpoDt1wv23a68Ctc8Cr/BHpUSfaPYrkajTXOS9IKpxVRx/X5m1K2YkbK2ipgZgxXSgsUinl3x+2YdSSfg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "license": "MIT" + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "bin": { @@ -5270,37 +7838,665 @@ "browserslist": ">= 4.21.0" } }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/utrie": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz", + "integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==", + "license": "MIT", + "optional": true, + "dependencies": { + "base64-arraybuffer": "^1.0.2" + } + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vitest": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.0.18.tgz", + "integrity": "sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.0.18", + "@vitest/mocker": "4.0.18", + "@vitest/pretty-format": "4.0.18", + "@vitest/runner": "4.0.18", + "@vitest/snapshot": "4.0.18", + "@vitest/spy": "4.0.18", + "@vitest/utils": "4.0.18", + "es-module-lexer": "^1.7.0", + "expect-type": "^1.2.2", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^3.10.0", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.0.3", + "vite": "^6.0.0 || ^7.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.0.18", + "@vitest/browser-preview": "4.0.18", + "@vitest/browser-webdriverio": "4.0.18", + "@vitest/ui": "4.0.18", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/vitest/node_modules/@esbuild/aix-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", + "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/android-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz", + "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/android-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz", + "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/android-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz", + "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/darwin-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz", + "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/darwin-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz", + "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz", + "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/freebsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz", + "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz", + "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz", + "integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz", + "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-loong64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz", + "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-mips64el": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz", + "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz", + "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-riscv64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz", + "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-s390x": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz", + "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz", + "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/netbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz", + "integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==", + "cpu": [ + "x64" + ], "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "punycode": "^2.1.0" + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" } }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "node_modules/vitest/node_modules/@esbuild/openbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz", + "integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==", + "cpu": [ + "x64" + ], "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } }, - "node_modules/utrie": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz", - "integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==", + "node_modules/vitest/node_modules/@esbuild/sunos-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz", + "integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/win32-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz", + "integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/win32-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz", + "integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/win32-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz", + "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==", + "cpu": [ + "x64" + ], + "dev": true, "license": "MIT", "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@vitest/mocker": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.0.18.tgz", + "integrity": "sha512-HhVd0MDnzzsgevnOWCBj5Otnzobjy5wLBe4EdeeFGv8luMsGcYqDuFRMcttKWZA5vVO8RFjexVovXvAM4JoJDQ==", + "dev": true, + "license": "MIT", "dependencies": { - "base64-arraybuffer": "^1.0.2" + "@vitest/spy": "4.0.18", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } } }, - "node_modules/vite": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.0.tgz", - "integrity": "sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg==", + "node_modules/vitest/node_modules/esbuild": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", + "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.3", + "@esbuild/android-arm": "0.27.3", + "@esbuild/android-arm64": "0.27.3", + "@esbuild/android-x64": "0.27.3", + "@esbuild/darwin-arm64": "0.27.3", + "@esbuild/darwin-x64": "0.27.3", + "@esbuild/freebsd-arm64": "0.27.3", + "@esbuild/freebsd-x64": "0.27.3", + "@esbuild/linux-arm": "0.27.3", + "@esbuild/linux-arm64": "0.27.3", + "@esbuild/linux-ia32": "0.27.3", + "@esbuild/linux-loong64": "0.27.3", + "@esbuild/linux-mips64el": "0.27.3", + "@esbuild/linux-ppc64": "0.27.3", + "@esbuild/linux-riscv64": "0.27.3", + "@esbuild/linux-s390x": "0.27.3", + "@esbuild/linux-x64": "0.27.3", + "@esbuild/netbsd-arm64": "0.27.3", + "@esbuild/netbsd-x64": "0.27.3", + "@esbuild/openbsd-arm64": "0.27.3", + "@esbuild/openbsd-x64": "0.27.3", + "@esbuild/openharmony-arm64": "0.27.3", + "@esbuild/sunos-x64": "0.27.3", + "@esbuild/win32-arm64": "0.27.3", + "@esbuild/win32-ia32": "0.27.3", + "@esbuild/win32-x64": "0.27.3" + } + }, + "node_modules/vitest/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/vitest/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/vitest/node_modules/vite": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", + "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", "dev": true, "license": "MIT", "dependencies": { @@ -5372,35 +8568,17 @@ } } }, - "node_modules/vite/node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", "dev": true, "license": "MIT", - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" + "dependencies": { + "xml-name-validator": "^5.0.0" }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "node_modules/vite/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", - "dev": true, - "license": "MIT", "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" + "node": ">=18" } }, "node_modules/web-vitals": { @@ -5409,6 +8587,16 @@ "integrity": "sha512-r4DIlprAGwJ7YM11VZp4R884m0Vmgr6EAKe3P+kO0PPj3Unqyvv59rczf6UiGcb9Z8QxZVcqKNwv/g0WNdWwsw==", "license": "Apache-2.0" }, + "node_modules/webidl-conversions": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz", + "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20" + } + }, "node_modules/websocket-driver": { "version": "0.7.4", "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", @@ -5432,6 +8620,31 @@ "node": ">=0.8.0" } }, + "node_modules/whatwg-mimetype": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz", + "integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/whatwg-url": { + "version": "16.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-16.0.0.tgz", + "integrity": "sha512-9CcxtEKsf53UFwkSUZjG+9vydAsFO4lFHBpJUtjBcoJOCJpKnSJNwCw813zrYJHpCJ7sgfbtOe0V5Ku7Pa1XMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.11.0", + "tr46": "^6.0.0", + "webidl-conversions": "^8.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -5448,6 +8661,112 @@ "node": ">= 8" } }, + "node_modules/which-boxed-primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", + "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-bigint": "^1.1.0", + "is-boolean-object": "^1.2.1", + "is-number-object": "^1.1.1", + "is-string": "^1.1.1", + "is-symbol": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-builtin-type": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", + "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "function.prototype.name": "^1.1.6", + "has-tostringtag": "^1.0.2", + "is-async-function": "^2.0.0", + "is-date-object": "^1.1.0", + "is-finalizationregistry": "^1.1.0", + "is-generator-function": "^1.0.10", + "is-regex": "^1.2.1", + "is-weakref": "^1.0.2", + "isarray": "^2.0.5", + "which-boxed-primitive": "^1.1.0", + "which-collection": "^1.0.2", + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-collection": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.20", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.20.tgz", + "integrity": "sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", @@ -5475,6 +8794,23 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", diff --git a/package.json b/package.json index c75dba1..fa41ea7 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,7 @@ "dev": "vite", "build": "vite build", "lint": "eslint .", + "test": "vitest", "preview": "vite preview" }, "dependencies": { @@ -15,9 +16,10 @@ "axios": "^1.13.2", "chart.js": "^4.5.1", "clsx": "^2.1.1", - "firebase": "^12.7.0", + "firebase": "^12.9.0", "framer-motion": "^12.24.0", "jspdf": "^4.0.0", + "lenis": "^1.3.17", "lucide-react": "^0.562.0", "react": "^18.3.1", "react-chartjs-2": "^5.3.1", @@ -32,19 +34,25 @@ }, "devDependencies": { "@eslint/js": "^9.33.0", + "@testing-library/dom": "^10.4.1", + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/react": "^16.3.2", "@types/react": "^19.1.10", "@types/react-dom": "^19.1.7", - "@vitejs/plugin-react": "^5.0.0", + "@vitejs/plugin-react": "^4.3.1", "autoprefixer": "^10.4.23", "eslint": "^9.33.0", + "eslint-plugin-react": "^7.37.5", "eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-refresh": "^0.4.20", "globals": "^16.3.0", + "jsdom": "^28.0.0", "postcss": "^8.5.6", "tailwindcss": "^3.4.19", - "vite": "^7.1.2" + "vite": "^5.4.2", + "vitest": "^4.0.18" }, "engines": { - "node": "24.x" + "node": "20.x" } } diff --git a/src/App.css b/src/App.css index e69de29..027624b 100644 --- a/src/App.css +++ b/src/App.css @@ -0,0 +1,95 @@ +.app { + min-height: 100vh; + width: 100%; + overflow-x: hidden; +} + +.app-container { + width: 100%; + max-width: 1400px; + margin: 0 auto; + padding: 86px 1.5rem 0 1.5rem; /* added top padding to account for fixed navbar */ + box-sizing: border-box; +} + +/* Responsive breakpoints */ +@media (max-width: 1440px) { + .app-container { + max-width: 1200px; + padding: 0 1.25rem; + } +} + +@media (max-width: 1024px) { + .app-container { + max-width: 100%; + padding: 0 1rem; + } +} + +@media (max-width: 768px) { + .app-container { + padding: 0 0.75rem; + } +} + +@media (max-width: 480px) { + .app-container { + padding: 0 0.5rem; + } +} + +/* Dashboard container - uses full width */ +.app-dashboard-container { + width: 100%; + max-width: none; + margin: 0; + padding: 0; + box-sizing: border-box; +} + +/* Ensure dashboard pages use full width */ +.dashboard-layout, +.dashboard-content { + width: 100% !important; + max-width: none !important; +} + +/* Override any tight constraints from components */ +.container, +.max-w-7xl { + max-width: 100% !important; +} + +/* Dashboard specific overrides */ +.app-dashboard-container .container, +.app-dashboard-container .max-w-7xl, +.app-dashboard-container .max-w-6xl, +.app-dashboard-container .max-w-5xl, +.app-dashboard-container .max-w-4xl { + max-width: none !important; + width: 100% !important; +} + +/* Market overview and table specific styles */ +.market-overview, +.coin-table, +.dashboard-table { + width: 100% !important; + max-width: none !important; +} + +/* Responsive grid improvements */ +.grid { + width: 100%; +} + +/* Table responsive improvements */ +.table-container { + width: 100%; + overflow-x: auto; +} + +table { + min-width: 100%; +} diff --git a/src/App.jsx b/src/App.jsx index 54fd89a..69f8083 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,40 +1,215 @@ -import React, { useEffect, useContext } from "react"; -import Navbar from "@/components/Navbar"; +import React, { + useEffect, + useContext, + useRef, + lazy, + Suspense, + useMemo, + useState, +} from "react"; +import Lenis from "lenis"; +import Navbar from "@/components/Layout/Navbar"; import { Routes, Route, useLocation } from "react-router-dom"; import Home from "@/pages/Home/Home"; -import CoinWrapper from "@/pages/Home/Coin/CoinWrapper"; -import Footer from "@/components/Footer"; -import Pricing from "@/components/Pricing"; -import Blog from "@/components/Blog"; -import Features from "@/components/Features"; -import Signup from "@/components/Signup"; -import Login from "@/components/Login"; -import BlogDetail from "@/components/BlogDetail"; -import DashboardLayout from "@/pages/Dashboard/DashboardLayout"; -import DashboardContent from "@/pages/Dashboard/DashboardContent"; -import MarketOverview from "@/pages/Dashboard/MarketOverview"; -import Leaderboard from "@/components/Leaderboard"; -import ChangePassword from "@/components/ChangePassword"; -import ForgotPassword from "@/components/ForgotPassword"; -import PrivateRoute from "@/components/PrivateRoute"; -import { AuthProvider } from "@/context/AuthContext"; +import Footer from "@/components/Layout/Footer"; +import PrivateRoute from "@/components/Auth/PrivateRoute"; +import { AuthProvider } from "@/context/AuthProvider"; import { ThemeProvider } from "@/context/ThemeContext"; -import Contributors from "@/components/Contributors"; +import { CoinContextProvider } from "@/context/CoinContext"; +import Contributors from "@/components/Sections/Contributors"; import AOS from "aos"; import "aos/dist/aos.css"; -import { CoinContext } from "@/context/CoinContext"; -import LoadingSpinner from "@/components/LoadingSpinner"; +import { CoinContext } from "@/context/CoinContextInstance"; +import LoadingSpinner from "@/components/Common/LoadingSpinner"; +import RouteLoadingFallback from "@/components/Common/RouteLoadingFallback"; +import ErrorBoundary from "@/components/Common/ErrorBoundary"; import { Toaster } from "react-hot-toast"; -import ScrollToTop from "@/components/ScrollToTop"; +import ScrollToTop from "@/components/Layout/ScrollToTop"; +import "./App.css"; +import PageNotFound from "@/components/Common/PageNotFound"; +import CryptoChatbot from "./CryptoChatbot/CryptoChatbot"; +import Feedback from "./pages/Feedback"; +import { validateFirebase, getFirebaseErrorInfo } from "@/utils/firebaseValidation"; +import { auth, db } from "@/firebase"; +import FirebaseError from "@/components/Common/FirebaseError"; +import RateLimitIndicator from "@/components/Common/RateLimitIndicator"; + +// Lazy-loaded Auth Components (Phase 2: Code Splitting) +const Signup = lazy(() => import("@/components/Auth/Signup")); +const Login = lazy(() => import("@/components/Auth/Login")); +const ForgotPassword = lazy(() => import("@/components/Auth/ForgotPassword")); +const EmailVerification = lazy(() => import("@/components/Auth/EmailVerification")); + +// Lazy-loaded Dashboard Components (Phase 3: Code Splitting) +const DashboardLayout = lazy(() => import("@/pages/Dashboard/DashboardLayout")); +const DashboardContent = lazy(() => import("@/pages/Dashboard/DashboardContent")); +const MarketOverview = lazy(() => import("@/pages/Dashboard/MarketOverview")); +const Leaderboard = lazy(() => import("@/components/Dashboard/Leaderboard")); +const ChangePassword = lazy(() => import("@/components/Auth/ChangePassword")); +const SavedInsights = lazy(() => import("@/pages/SavedInsights")); +const Profile = lazy(() => import("@/pages/Dashboard/Profile")); + +// Lazy-loaded Page Components (Phase 4: Code Splitting) +const Pricing = lazy(() => import("@/components/Sections/Pricing")); +const Blog = lazy(() => import("@/components/Sections/Blog")); +const Features = lazy(() => import("@/components/Sections/Features")); +const BlogDetail = lazy(() => import("@/components/Sections/BlogDetail")); +const Contributors = lazy(() => import("@/components/Sections/Contributors")); +const ContactUs = lazy(() => import("@/components/Sections/ContactUs")); +const FAQ = lazy(() => import("@/components/Sections/FAQ")); +const About = lazy(() => import("@/components/Sections/About")); +const Feedback = lazy(() => import("./pages/Feedback")); + +// Lazy-loaded Crypto Page Components (Phase 5: Code Splitting) +const TrendingCoins = lazy(() => import("@/pages/TrendingCoins")); +const NewListings = lazy(() => import("@/pages/NewListings")); +const TopGainers = lazy(() => import("./pages/TopGainers")); +const TopLosers = lazy(() => import("./pages/TopLosers")); +const ApiAccess = lazy(() => import("./pages/ApiAccess")); +const CoinWrapper = lazy(() => import("@/pages/Home/Coin/CoinWrapper")); +const AIBlogPage = lazy(() => import("./pages/AIBlog/AIBlogPage")); + +// Lazy-loaded Legal Page Components (Phase 6: Code Splitting) +const PrivacyPolicy = lazy(() => import("@/components/Legal/PrivacyPolicy.jsx")); +const TermsOfService = lazy(() => import("@/components/Legal/TermsOfService.jsx")); +const CookiePolicy = lazy(() => import("@/components/Legal/CookiePolicy.jsx")); const App = () => { + const lenisRef = useRef(null); const { isLoading } = useContext(CoinContext); const location = useLocation(); - const isDashboard = location.pathname === "/dashboard" || - location.pathname === "/leaderboard" || - location.pathname === "/market-overview" || - location.pathname === "/change-password" || - location.pathname.startsWith("/coin/"); + const [firebaseStatus, setFirebaseStatus] = useState({ + validated: false, + hasError: false, + errorInfo: null, + showError: false, + retrying: false, + retryAttempt: 0, + }); + + // Firebase validation on app startup (Phase 5: App Startup Integration) + useEffect(() => { + const validateFirebaseOnStartup = async () => { + try { + const validationResult = await validateFirebase( + { db, auth }, + { + skipConnectivityTest: false, + skipPermissionTest: true, // Skip on startup to avoid delays + } + ); + + if (!validationResult.isValid) { + // Extract first error from validation result + const firstError = validationResult.errors[0]; + const errorType = firstError?.type || 'UNKNOWN'; + const errorMessages = { + 'NOT_CONFIGURED': 'Firebase is not configured. Please check your environment variables.', + 'INVALID_CONFIG': 'Firebase configuration is invalid. Please verify your credentials.', + 'CONNECTION_FAILED': 'Could not connect to Firebase. Please check your internet connection.', + 'NETWORK_ERROR': 'Network error connecting to Firebase. Please try again.', + 'SERVICE_UNAVAILABLE': 'Firebase service is temporarily unavailable.', + 'PERMISSION_DENIED': 'Permission denied accessing Firebase resources.', + 'UNKNOWN': 'An unknown error occurred with Firebase.' + }; + + const errorInfo = { + title: errorType.split('_').map(w => w.charAt(0) + w.slice(1).toLowerCase()).join(' '), + message: errorMessages[errorType] || errorMessages['UNKNOWN'], + userAction: 'You can still browse crypto data, but authentication features will be limited.', + developerAction: 'Check Firebase configuration and network connectivity', + code: errorType, + context: 'App Initialization', + details: validationResult + }; + + setFirebaseStatus({ + validated: true, + hasError: true, + errorInfo: errorInfo, + showError: true, + }); + + // Log for developers + if (import.meta.env.DEV) { + console.warn("Firebase Validation Failed:", errorInfo); + console.warn("Details:", validationResult.details); + } + } else { + setFirebaseStatus({ + validated: true, + hasError: false, + errorInfo: null, + showError: false, + }); + + if (import.meta.env.DEV) { + console.log("✅ Firebase validated successfully"); + } + } + } catch (error) { + const errorInfo = getFirebaseErrorInfo(error, "Firebase Validation"); + setFirebaseStatus({ + validated: true, + hasError: true, + errorInfo: errorInfo, + showError: true, + }); + + if (import.meta.env.DEV) { + console.error("Firebase validation error:", errorInfo); + } + } + }; + + validateFirebaseOnStartup(); + }, []); + + useEffect(() => { + const lenis = new Lenis({ + smoothWheel: true, + lerp: 0.08, + wheelMultiplier: 1, + smoothTouch: false, + }); + + lenisRef.current = lenis; + + let animationFrameId; + + const raf = (time) => { + lenis.raf(time); + animationFrameId = requestAnimationFrame(raf); + }; + + animationFrameId = requestAnimationFrame(raf); + + return () => { + cancelAnimationFrame(animationFrameId); + lenis.destroy(); + lenisRef.current = null; + }; + }, []); + + const dashboardRoutes = useMemo( + () => [ + "/dashboard", + "/leaderboard", + "/market-overview", + "/change-password", + "/saved-insights", + "/profile", + ], + [], + ); + + const authRoutes = useMemo( + () => ["/login", "/signup", "/forgot-password", "/verify-email"], + [], + ); + + const isDashboard = dashboardRoutes.includes(location.pathname); + const isAuthPage = authRoutes.includes(location.pathname); useEffect(() => { AOS.init({ @@ -43,6 +218,86 @@ const App = () => { }); }, []); + // Handle Firebase error retry (Phase 6: Retry Mechanisms) + const handleFirebaseRetry = async () => { + setFirebaseStatus((prev) => ({ + ...prev, + showError: false, + retrying: true, + retryAttempt: 0 + })); + + // Re-run validation with retry tracking + try { + const validationResult = await validateFirebase( + { db, auth }, + { + skipConnectivityTest: false, + skipPermissionTest: true, + useRetry: true // Enable retry in validation + } + ); + + if (!validationResult.isValid) { + // Extract first error from validation result + const firstError = validationResult.errors[0]; + const errorType = firstError?.type || 'UNKNOWN'; + const errorMessages = { + 'NOT_CONFIGURED': 'Firebase is not configured. Please check your environment variables.', + 'INVALID_CONFIG': 'Firebase configuration is invalid. Please verify your credentials.', + 'CONNECTION_FAILED': 'Could not connect to Firebase. Please check your internet connection.', + 'NETWORK_ERROR': 'Network error connecting to Firebase. Please try again.', + 'SERVICE_UNAVAILABLE': 'Firebase service is temporarily unavailable.', + 'PERMISSION_DENIED': 'Permission denied accessing Firebase resources.', + 'UNKNOWN': 'An unknown error occurred with Firebase.' + }; + + const errorInfo = { + title: errorType.split('_').map(w => w.charAt(0) + w.slice(1).toLowerCase()).join(' '), + message: errorMessages[errorType] || errorMessages['UNKNOWN'], + userAction: 'You can still browse crypto data, but authentication features will be limited.', + developerAction: 'Check Firebase configuration and network connectivity', + code: errorType, + context: 'App Initialization', + details: validationResult + }; + + setFirebaseStatus({ + validated: true, + hasError: true, + errorInfo: errorInfo, + showError: true, + retrying: false, + retryAttempt: 0, + }); + } else { + setFirebaseStatus({ + validated: true, + hasError: false, + errorInfo: null, + showError: false, + retrying: false, + retryAttempt: 0, + }); + } + } catch (error) { + const errorInfo = getFirebaseErrorInfo(error, "Firebase Validation"); + setFirebaseStatus({ + validated: true, + hasError: true, + errorInfo: errorInfo, + showError: true, + retrying: false, + retryAttempt: 0, + }); + } + }; + + // Dismiss Firebase error and continue with degraded features + const handleFirebaseDismiss = () => { + setFirebaseStatus((prev) => ({ ...prev, showError: false })); + }; + return ( <> { }, }} /> + {/* Firebase Error Display (Phase 5: App Startup Integration) */} + {firebaseStatus.showError && firebaseStatus.errorInfo && ( +
+ +
+ )}
- {/* Loading Spinner - will show when isLoading is true */} {isLoading && !isDashboard && } +
+ {!isDashboard && } + + }> + + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + } /> - {!isDashboard && } - - } /> - } /> - } /> - {/* Blog detail route supporting both slug and id patterns */} - } /> - } /> - - } /> - } /> - } /> - } /> - } /> - - {/* Dashboard Layout with nested routes - all share the same sidebar */} - - - - }> - } /> - } /> - } /> - } /> - - - {/* Coin route - accessible to all but shows sidebar if logged in */} - } /> - - {/* Add 404 Route if you implemented it earlier */} - {/* } /> */} - - {!isDashboard &&
} -
- + } /> + } /> + } /> + } /> + + + + } + /> + } /> + + + + } + > + } /> + } /> + } /> + } /> + } /> + } /> + + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + + +
+ + + +
); }; -export default App; \ No newline at end of file +export default App; diff --git a/src/CryptoChatbot/CryptoChatbot.jsx b/src/CryptoChatbot/CryptoChatbot.jsx new file mode 100644 index 0000000..f085c86 --- /dev/null +++ b/src/CryptoChatbot/CryptoChatbot.jsx @@ -0,0 +1,626 @@ +import { useState, useRef, useEffect, useCallback } from "react"; +import { processMessage } from "./chatEngine"; + +// ─── Markdown-lite renderer (bold, tables, newlines) ────────────── +function renderMarkdown(text) { + if (!text) return ""; + + // Process tables + if (text.includes("|")) { + const lines = text.split("\n"); + const processed = []; + let tableRows = []; + let inTable = false; + + for (const line of lines) { + const trimmed = line.trim(); + if (trimmed.startsWith("|") && trimmed.endsWith("|")) { + if (trimmed.replace(/[|\-\s]/g, "").length === 0) { + // separator row + continue; + } + inTable = true; + const cells = trimmed + .split("|") + .filter(Boolean) + .map((c) => c.trim()); + tableRows.push(cells); + } else { + if (inTable) { + processed.push({ type: "table", rows: tableRows }); + tableRows = []; + inTable = false; + } + processed.push({ type: "text", content: trimmed }); + } + } + if (inTable) processed.push({ type: "table", rows: tableRows }); + + return processed.map((block, i) => { + if (block.type === "table") { + return ( +
+ + + + {block.rows[0]?.map((cell, j) => ( + + ))} + + + + {block.rows.slice(1).map((row, ri) => ( + + {row.map((cell, j) => ( + + ))} + + ))} + +
{renderInline(cell)}
{renderInline(cell)}
+
+ ); + } + if (!block.content) return
; + return ( + + {renderInline(block.content)} +
+
+ ); + }); + } + + // No table — render line by line + return text.split("\n").map((line, i) => ( + + {renderInline(line)} +
+
+ )); +} + +function renderInline(text) { + // Bold: **text** + const parts = text.split(/(\*\*[^*]+\*\*)/g); + return parts.map((part, i) => { + if (part.startsWith("**") && part.endsWith("**")) { + return {part.slice(2, -2)}; + } + // Italic: _text_ + if (part.startsWith("_") && part.endsWith("_") && part.length > 2) { + return {part.slice(1, -1)}; + } + return {part}; + }); +} + +// ─── Suggested Prompts ──────────────────────────────────────────── +const QUICK_PROMPTS = [ + "How's the market?", + "Top gainers today", + "Price of BTC", + "What's trending?", + "Compare ETH vs SOL", +]; + +// ─── Main Component ─────────────────────────────────────────────── +export default function CryptoChatbot() { + const [isOpen, setIsOpen] = useState(false); + const [messages, setMessages] = useState([ + { + role: "bot", + text: "Hey! 👋 I'm **CryptoBot** — your on-chain assistant. Ask me about prices, market trends, gainers, losers, or anything crypto!\n\nTry one of the suggestions below, or just type naturally.", + timestamp: Date.now(), + }, + ]); + const [input, setInput] = useState(""); + const [isLoading, setIsLoading] = useState(false); + const messagesEndRef = useRef(null); + const inputRef = useRef(null); + + // Auto-scroll to bottom + useEffect(() => { + messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); + }, [messages]); + + // Focus input when chat opens + useEffect(() => { + if (isOpen) inputRef.current?.focus(); + }, [isOpen]); + + const sendMessage = useCallback( + async (text) => { + const trimmed = (text || input).trim(); + if (!trimmed || isLoading) return; + + const userMsg = { role: "user", text: trimmed, timestamp: Date.now() }; + setMessages((prev) => [...prev, userMsg]); + setInput(""); + setIsLoading(true); + + try { + const response = await processMessage(trimmed); + setMessages((prev) => [ + ...prev, + { role: "bot", text: response, timestamp: Date.now() }, + ]); + } catch { + setMessages((prev) => [ + ...prev, + { + role: "bot", + text: "Something went wrong. Please try again!", + timestamp: Date.now(), + }, + ]); + } finally { + setIsLoading(false); + } + }, + [input, isLoading], + ); + + const handleKeyDown = (e) => { + if (e.key === "Enter" && !e.shiftKey) { + e.preventDefault(); + sendMessage(); + } + }; + + return ( + <> + {/* ── Inline Styles ── */} + + + {/* ── Toggle Button ── */} + + + {/* ── Chat Window ── */} + {isOpen && ( +
+ {/* Header */} +
+
🤖
+
+

CryptoBot

+

+ + Powered by CoinGecko • Always free +

+
+
+ + {/* Messages */} +
+ {messages.map((msg, i) => ( +
+ {msg.role === "bot" ? renderMarkdown(msg.text) : msg.text} +
+ ))} + + {isLoading && ( +
+ + + +
+ )} + +
+
+ + {/* Quick Prompts */} + {messages.length <= 2 && ( +
+ {QUICK_PROMPTS.map((prompt) => ( + + ))} +
+ )} + + {/* Input */} +
+ setInput(e.target.value)} + onKeyDown={handleKeyDown} + disabled={isLoading} + /> + +
+
+ )} + + ); +} diff --git a/src/CryptoChatbot/chatEngine.js b/src/CryptoChatbot/chatEngine.js new file mode 100644 index 0000000..5029fba --- /dev/null +++ b/src/CryptoChatbot/chatEngine.js @@ -0,0 +1,814 @@ +/** + * Chat Engine — Intent Parser + Response Generator + * + * Entirely rule-based NLP. Zero API calls for understanding. + * Uses regex, keyword matching, and pattern recognition. + */ + +import { + getGlobalData, + getTopCoins, + getCoinPrice, + getCoinDetails, + getTrending, + searchCoin, + getGainersLosers, + compareCoins, + resolveCoinId, + formatINR, + formatPercent, +} from "./coinGeckoService"; +import { searchReports, searchEducation } from "./knowledgeBase"; + +// ─── Intent Definitions ─────────────────────────────────────────── + +const INTENT_PATTERNS = [ + { + intent: "greeting", + patterns: [ + /^(hi|hello|hey|howdy|sup|yo|hola|good\s*(morning|evening|afternoon|night)|namaste)/i, + ], + }, + { + intent: "help", + patterns: [/\b(help|what can you do|commands|features|menu|options)\b/i], + }, + { + intent: "market_overview", + patterns: [ + /\b(market|overall)\s*(overview|summary|status|update|today|now|doing|look)/i, + /\bhow('?s| is| are)\s*(the )?(market|crypto|things)/i, + /\b(green|red)\s*day/i, + /\bmarket\s*(cap|sentiment|mood)/i, + /\bwhat'?s\s*(happening|going on|up)\s*(in|with)?\s*(the )?(market|crypto)/i, + ], + }, + { + intent: "price_check", + patterns: [ + /\b(price|cost|value|rate|worth)\s*(of|for)?\s+(\w[\w\s]*)/i, + /\bhow\s*much\s*(is|does|for)\s+(\w[\w\s]*)/i, + /\b(\w+)\s*(price|cost|value|rate|kya hai|kitna)/i, + /^(btc|eth|sol|bnb|xrp|ada|doge|dot|matic|avax|link|shib|ltc|uni|atom|pepe|ton|trx|sui|near|apt|arb|op)\s*\??$/i, + ], + }, + { + intent: "top_gainers", + patterns: [ + /\b(top|best|biggest|highest)\s*(gainer|winner|performer|pump)/i, + /\bwho\s*(gained|pumped|went up|mooned|rallied)/i, + /\bgain(ed|ing|s)?\s*(the )?(most|highest|biggest)/i, + /\bwhich\s*(coin|crypto)\s*(gained|pumped|up|rallied)/i, + /\bpump(ed|ing)?\s*(the most|today|hard)/i, + ], + }, + { + intent: "top_losers", + patterns: [ + /\b(top|worst|biggest|highest)\s*(loser|dump|decline|drop|crash)/i, + /\bwho\s*(lost|dumped|dropped|crashed|went down|tanked)/i, + /\blos(t|ing|e|es)\s*(the )?(most|highest|biggest)/i, + /\bwhich\s*(coin|crypto)\s*(lost|dumped|dropped|crashed|tanked)/i, + /\bdump(ed|ing)?\s*(the most|today|hard)/i, + ], + }, + { + intent: "trending", + patterns: [ + /\b(trending|hot|popular|buzzing|viral|hype)/i, + /\bwhat'?s\s*(trending|hot|popular)/i, + /\btrend(s|ing)?\s*(today|now|right now|coins?)/i, + ], + }, + { + intent: "compare", + patterns: [ + /\bcompare\s+(\w+)\s*(and|vs|versus|with|or|&)\s*(\w+)/i, + /\b(\w+)\s*(vs|versus|compared to|or)\s*(\w+)/i, + ], + }, + { + intent: "coin_info", + patterns: [ + /\b(tell|info|about|details|what is|what'?s|explain)\s*(me )?(about )?\s*(\w[\w\s]*)/i, + /\bwhat\s*(is|are)\s+(\w[\w\s]*)/i, + ], + }, + { + intent: "report", + patterns: [ + /\b(report|analysis|vector|on-?chain|week\s*on|blog|research|article)\b/i, + /\blatest\s*(report|analysis|research)/i, + /\bwhat\s*(did|does|do)\s*(the )?(report|analysis|vector|blog)/i, + ], + }, + { + intent: "dominance", + patterns: [/\b(btc|bitcoin|eth|ethereum)?\s*dominance/i], + }, + { + intent: "investment", + patterns: [ + /\b(should\s*i|is\s*it\s*(good|wise|safe|smart|right)\s*to)\s*(buy|sell|invest|hold|trade|put money)/i, + /\b(invest|put money|put ₹|put rs|put \$)\s*(in|into|on)\s+/i, + /\b(profit|loss|return|gain|earn|make money|lose money)\s*(if|when|from|on|by)\s*(i )?(invest|buy|sell|hold|put)/i, + /\bhow\s*much\s*(profit|loss|return|gain|money|will i)\s*(will|can|do|if|from|on)/i, + /\b(will|can|could)\s*(i )?(make|earn|gain|lose|get)\s*(money|profit|return|₹|\$|rs)/i, + /\b(good|best|right|safe|wise)\s*(time|moment|opportunity)\s*to\s*(buy|sell|invest|enter|exit)/i, + /\bworth\s*(buying|selling|investing|holding)/i, + /\b(buy|sell|hold)\s*(or\s*(buy|sell|hold))?\s*\?/i, + ], + }, + { + intent: "prediction", + patterns: [ + /\b(will|can|could|shall)\s+(\w+)\s*(go|reach|hit|cross|touch|pump|dump|crash|moon|rise|fall|drop)/i, + /\b(\w+)\s*(price\s*)?(prediction|forecast|target|potential|future|outlook)/i, + /\b(where|what)\s*(will|would|could)\s+(\w+)\s*(be|go|reach|price)/i, + /\bwhen\s*(will|would|could)\s+(\w+)\s*(reach|hit|cross|touch|go to|moon)/i, + /\b(moon|lambo|100x|10x|1000x)\b/i, + ], + }, + { + intent: "best_coin", + patterns: [ + /\b(best|top|good|safest|most promising)\s*(coin|crypto|token|investment)\s*(to\s*(buy|invest|hold))?/i, + /\bwhich\s*(coin|crypto|token)\s*(should|to|can|do)\s*(i )?(buy|invest|hold|pick)/i, + /\bwhat\s*(should|to|can)\s*(i )?(buy|invest in|hold|pick)/i, + /\brecommend\s*(a )?(coin|crypto|token|investment)/i, + /\bsuggest\s*(a )?(coin|crypto|token)/i, + ], + }, + { + intent: "education", + patterns: [ + /\b(what\s*(is|are|does)|explain|meaning\s*of|define|eli5)\s+(.+)/i, + ], + }, +]; + +// ─── Intent Extraction ──────────────────────────────────────────── + +function extractIntent(message) { + const msg = message.trim().toLowerCase(); + + for (const { intent, patterns } of INTENT_PATTERNS) { + for (const pattern of patterns) { + const match = msg.match(pattern); + if (match) { + return { intent, match, raw: msg }; + } + } + } + + return { intent: "unknown", match: null, raw: msg }; +} + +// ─── Coin Name Extraction from Message ──────────────────────────── + +function extractCoinName(message) { + const msg = message.toLowerCase().trim(); + + // Remove common filler words + const cleaned = msg + .replace( + /\b(what|is|the|price|of|for|how|much|does|cost|tell|me|about|info|details|coin|crypto|token|check|get|show|current|today|now|please|can|you)\b/g, + "", + ) + .trim() + .replace(/\s+/g, " ") + .replace(/[?.!,]/g, "") + .trim(); + + if (cleaned.length > 0 && cleaned.length < 30) { + return cleaned; + } + return null; +} + +// ─── Extract Coin from Complex Sentences ────────────────────────── +// Handles: "should I invest in toncoin right now?" +// "how much profit if I invest on ethereum?" +// "will solana go up?" + +function extractCoinFromSentence(message) { + const msg = message.toLowerCase().trim(); + + // Try to find a known coin alias directly in the message + const KNOWN_COINS = [ + "bitcoin", + "btc", + "ethereum", + "eth", + "solana", + "sol", + "bnb", + "binance", + "xrp", + "ripple", + "cardano", + "ada", + "dogecoin", + "doge", + "polkadot", + "dot", + "polygon", + "matic", + "avalanche", + "avax", + "chainlink", + "link", + "shiba", + "shib", + "uniswap", + "uni", + "litecoin", + "ltc", + "cosmos", + "atom", + "near", + "stellar", + "xlm", + "algorand", + "algo", + "aptos", + "apt", + "arbitrum", + "arb", + "optimism", + "op", + "sui", + "ton", + "toncoin", + "tether", + "usdt", + "usdc", + "pepe", + "tron", + "trx", + "wif", + "dogwifhat", + ]; + + for (const coin of KNOWN_COINS) { + if (msg.includes(coin)) return coin; + } + + // Fallback: strip common filler words and see what's left + const stripped = msg + .replace( + /\b(should|would|could|will|can|do|did|does|is|are|was|were|i|we|you|they|it|if|the|a|an|in|on|at|to|of|for|and|or|but|how|much|many|what|when|where|which|who|why|not|no|yes|my|this|that|right now|right|now|today|tomorrow|currently|invest|investment|investing|buy|buying|sell|selling|hold|holding|trade|trading|put|money|profit|loss|return|gain|earn|make|lose|get|good|bad|best|worst|safe|wise|smart|time|moment|go up|go down|reach|hit|cross|moon|pump|dump|crash|rise|fall|drop|price|prediction|forecast|target|worth|think|believe|suggest|recommend)\b/g, + "", + ) + .replace(/[?.!,₹$]/g, "") + .trim() + .replace(/\s+/g, " ") + .trim(); + + if (stripped.length > 0 && stripped.length < 25) { + return stripped; + } + + return null; +} + +// ─── Extract Two Coins for Comparison ───────────────────────────── + +function extractCompareCoins(message) { + const patterns = [ + /compare\s+(\w+)\s*(?:and|vs|versus|with|or|&)\s*(\w+)/i, + /(\w+)\s*(?:vs|versus|compared\s*to)\s*(\w+)/i, + ]; + + for (const pattern of patterns) { + const match = message.match(pattern); + if (match) return [match[1], match[2]]; + } + return null; +} + +// ─── Response Generators ────────────────────────────────────────── + +async function handleGreeting() { + const greetings = [ + "Hey! 👋 I'm CryptoBot, your on-chain companion. Ask me about prices, market trends, top gainers, or anything crypto!", + "Hello! I can help you with crypto prices, market overviews, coin comparisons, and more. What do you want to know?", + "Namaste! 🙏 Ready to talk crypto. Try asking me 'How's the market?' or 'What's the price of ETH?'", + ]; + return greetings[Math.floor(Math.random() * greetings.length)]; +} + +function handleHelp() { + return `Here's what I can help you with: + +🔹 **Market Overview** — "How's the market today?" +🔹 **Price Check** — "What's the price of BTC?" +🔹 **Top Gainers** — "Which coin gained the most?" +🔹 **Top Losers** — "Who lost the most today?" +🔹 **Trending** — "What's trending right now?" +🔹 **Compare** — "Compare ETH vs SOL" +🔹 **Coin Info** — "Tell me about Cardano" +🔹 **Reports** — "What does the latest report say?" +🔹 **Learn** — "What is DeFi?" / "Explain staking" + +Just type naturally — I'll figure out what you mean!`; +} + +async function handleMarketOverview() { + try { + const [global, coins] = await Promise.all([ + getGlobalData(), + getTopCoins("inr", 10), + ]); + + const btc = coins.find((c) => c.id === "bitcoin"); + const eth = coins.find((c) => c.id === "ethereum"); + + const sentiment = + global.marketCapChangePercent24h >= 0 ? "green 🟢" : "red 🔴"; + + const topMover = [...coins].sort( + (a, b) => + Math.abs(b.price_change_percentage_24h || 0) - + Math.abs(a.price_change_percentage_24h || 0), + )[0]; + + return `The market is **${sentiment}** today. + +📊 **Total Market Cap:** ${formatINR(global.totalMarketCap)} (${formatPercent(global.marketCapChangePercent24h)} 24h) +₿ **BTC Dominance:** ${global.btcDominance?.toFixed(1)}% +${btc ? `\n**Bitcoin:** ${formatINR(btc.current_price)} (${formatPercent(btc.price_change_percentage_24h)})` : ""} +${eth ? `**Ethereum:** ${formatINR(eth.current_price)} (${formatPercent(eth.price_change_percentage_24h)})` : ""} +${topMover ? `\n🔥 **Biggest mover in top 10:** ${topMover.name} (${formatPercent(topMover.price_change_percentage_24h)})` : ""}`; + } catch { + return "Couldn't fetch market data right now. Try again in a moment!"; + } +} + +async function handlePriceCheck(message) { + const coinName = extractCoinName(message); + if (!coinName) + return "Which coin's price would you like to check? Try something like 'price of BTC' or 'ETH price'."; + + const coinId = resolveCoinId(coinName); + try { + const data = await getCoinPrice(coinId, "inr"); + const coinData = data[coinId]; + + if (!coinData) { + // Try search + const results = await searchCoin(coinName); + if (results.length > 0) { + const suggestion = results[0]; + const retryData = await getCoinPrice(suggestion.id, "inr"); + const retryInfo = retryData[suggestion.id]; + if (retryInfo) { + return `**${suggestion.name} (${suggestion.symbol?.toUpperCase()})** +💰 Price: ${formatINR(retryInfo.inr)} +📈 24h Change: ${formatPercent(retryInfo.inr_24h_change)} +📊 Market Cap: ${formatINR(retryInfo.inr_market_cap)} +💹 24h Volume: ${formatINR(retryInfo.inr_24h_vol)}`; + } + } + return `Couldn't find "${coinName}". Try the full name (e.g., "bitcoin") or symbol (e.g., "BTC").`; + } + + return `**${coinName.toUpperCase()}** +💰 Price: ${formatINR(coinData.inr)} +📈 24h Change: ${formatPercent(coinData.inr_24h_change)} +📊 Market Cap: ${formatINR(coinData.inr_market_cap)} +💹 24h Volume: ${formatINR(coinData.inr_24h_vol)}`; + } catch { + return "Couldn't fetch the price right now. Please try again!"; + } +} + +async function handleGainers() { + try { + const { gainers } = await getGainersLosers("inr", 5); + const list = gainers + .map( + (c, i) => + `${i + 1}. **${c.name}** (${c.symbol.toUpperCase()}) — ${formatINR(c.current_price)} (${formatPercent(c.price_change_percentage_24h)})`, + ) + .join("\n"); + + return `🚀 **Top 5 Gainers (24h):**\n\n${list}`; + } catch { + return "Couldn't fetch gainers right now. Try again in a moment!"; + } +} + +async function handleLosers() { + try { + const { losers } = await getGainersLosers("inr", 5); + const list = losers + .map( + (c, i) => + `${i + 1}. **${c.name}** (${c.symbol.toUpperCase()}) — ${formatINR(c.current_price)} (${formatPercent(c.price_change_percentage_24h)})`, + ) + .join("\n"); + + return `📉 **Top 5 Losers (24h):**\n\n${list}`; + } catch { + return "Couldn't fetch losers right now. Try again in a moment!"; + } +} + +async function handleTrending() { + try { + const trending = await getTrending(); + if (!trending || trending.length === 0) + return "No trending data available right now."; + + const list = trending + .slice(0, 7) + .map( + (c, i) => + `${i + 1}. **${c.name}** (${c.symbol?.toUpperCase()}) — Rank #${c.marketCapRank || "N/A"}`, + ) + .join("\n"); + + return `🔥 **Trending Coins Right Now:**\n\n${list}`; + } catch { + return "Couldn't fetch trending coins. Try again shortly!"; + } +} + +async function handleCompare(message) { + const coins = extractCompareCoins(message); + if (!coins) return 'Try: "Compare BTC vs ETH" or "SOL vs AVAX"'; + + const [name1, name2] = coins; + const id1 = resolveCoinId(name1); + const id2 = resolveCoinId(name2); + + try { + const { coin1, coin2 } = await compareCoins(id1, id2, "inr"); + + if (!coin1 && !coin2) + return `Couldn't find data for either ${name1} or ${name2}. Make sure you're using the correct coin names.`; + if (!coin1) return `Couldn't find ${name1}. Try the full name or symbol.`; + if (!coin2) return `Couldn't find ${name2}. Try the full name or symbol.`; + + return `⚖️ **${coin1.name} vs ${coin2.name}** + +| | ${coin1.symbol.toUpperCase()} | ${coin2.symbol.toUpperCase()} | +|---|---|---| +| **Price** | ${formatINR(coin1.current_price)} | ${formatINR(coin2.current_price)} | +| **24h Change** | ${formatPercent(coin1.price_change_percentage_24h)} | ${formatPercent(coin2.price_change_percentage_24h)} | +| **Market Cap** | ${formatINR(coin1.market_cap)} | ${formatINR(coin2.market_cap)} | +| **Volume** | ${formatINR(coin1.total_volume)} | ${formatINR(coin2.total_volume)} | +| **Rank** | #${coin1.market_cap_rank} | #${coin2.market_cap_rank} | + +${ + (coin1.price_change_percentage_24h || 0) > + (coin2.price_change_percentage_24h || 0) + ? `📈 ${coin1.name} is outperforming ${coin2.name} today.` + : `📈 ${coin2.name} is outperforming ${coin1.name} today.` +}`; + } catch { + return "Couldn't compare those coins right now. Try again shortly!"; + } +} + +async function handleCoinInfo(message) { + const coinName = extractCoinName(message); + if (!coinName) return "Which coin would you like to know about?"; + + // First check education base + const eduMatches = searchEducation(coinName); + if (eduMatches.length > 0 && !resolveCoinId(coinName)) { + return `📖 **${eduMatches[0].term}**\n\n${eduMatches[0].answer}`; + } + + const coinId = resolveCoinId(coinName); + try { + const details = await getCoinDetails(coinId); + if (!details || !details.market_data) + return `Couldn't find detailed info for "${coinName}". Try the full name or symbol!`; + + const md = details.market_data; + const desc = details.description?.en + ? details.description.en.replace(/<[^>]*>/g, "").slice(0, 300) + "..." + : "No description available."; + + return `**${details.name} (${details.symbol?.toUpperCase()})** + +💰 Price: ${formatINR(md.current_price?.inr)} +📈 24h: ${formatPercent(md.price_change_percentage_24h)} +📅 7d: ${formatPercent(md.price_change_percentage_7d)} +📊 Market Cap: ${formatINR(md.market_cap?.inr)} (Rank #${details.market_cap_rank || "N/A"}) +💹 24h Volume: ${formatINR(md.total_volume?.inr)} +📦 Circulating: ${md.circulating_supply?.toLocaleString() || "N/A"} +🔒 Max Supply: ${md.max_supply?.toLocaleString() || "∞"} + +📝 ${desc}`; + } catch { + return `Couldn't fetch info for "${coinName}". Try the exact coin name!`; + } +} + +function handleReport(message) { + const results = searchReports(message); + + if (results.length === 0) { + return `Here are the available reports: + +📑 **The Bitcoin Vector #37** (Premium) — Analysis of BTC momentum and accumulation patterns entering 2026. +📑 **Week On-Chain #2 2026** (Free) — On-chain analysis showing exchange outflows and stabilization signs. + +Ask me something specific like "What does the Bitcoin Vector say about momentum?" or "Summarize the on-chain report."`; + } + + const report = results[0]; + return `📑 **${report.title}** (${report.type === "premium" ? "🔒 Premium" : "🆓 Free"}) + +${report.summary} + +${report.type === "premium" ? "\n_This is a premium report. Upgrade to access the full analysis._" : ""}`; +} + +async function handleDominance() { + try { + const global = await getGlobalData(); + return `📊 **Market Dominance:** +₿ Bitcoin: **${global.btcDominance?.toFixed(1)}%** +Ξ Ethereum: **${global.ethDominance?.toFixed(1)}%** +🪙 Others: **${(100 - (global.btcDominance || 0) - (global.ethDominance || 0)).toFixed(1)}%** + +${ + global.btcDominance > 55 + ? "BTC dominance is high — money is concentrated in Bitcoin. Altcoins may underperform until dominance drops." + : global.btcDominance < 45 + ? "BTC dominance is low — could signal alt season! Altcoins might be gaining traction." + : "BTC dominance is moderate — the market is relatively balanced between BTC and alts." +}`; + } catch { + return "Couldn't fetch dominance data right now."; + } +} + +function handleEducation(message) { + const matches = searchEducation(message); + if (matches.length > 0) { + return `📖 **${matches[0].term}**\n\n${matches[0].answer}`; + } + return null; // Fall through to unknown +} + +// ─── Investment Query Handler ───────────────────────────────────── + +async function handleInvestment(message) { + const coinName = extractCoinFromSentence(message); + + if (!coinName) { + return `I can't predict profits or tell you what to invest in — no one can with certainty! 🎯 + +But I **can** help you research. Try: +• "Price of BTC" — check current price and 24h trend +• "Compare ETH vs SOL" — side-by-side comparison +• "Top gainers today" — see what's performing well +• "What's trending?" — see what the market is buzzing about + +**⚠️ Disclaimer:** I'm a data bot, not a financial advisor. Always do your own research (DYOR) before investing.`; + } + + const coinId = resolveCoinId(coinName); + try { + const data = await getCoinPrice(coinId, "inr"); + let coinData = data[coinId]; + let resolvedName = coinName.toUpperCase(); + + if (!coinData) { + const results = await searchCoin(coinName); + if (results.length > 0) { + const retryData = await getCoinPrice(results[0].id, "inr"); + coinData = retryData[results[0].id]; + resolvedName = results[0].name; + } + } + + if (!coinData) { + return `Couldn't find data for "${coinName}". Try using the full coin name or symbol!\n\n**⚠️ Disclaimer:** I can show you data, but I can't predict future prices or profits.`; + } + + const change = coinData.inr_24h_change; + const trend = change >= 0 ? "up 📈" : "down 📉"; + const sentiment = + change > 3 + ? "strong bullish momentum" + : change > 0 + ? "slight positive movement" + : change > -3 + ? "slight negative movement" + : "strong bearish pressure"; + + return `I can't predict future profits, but here's what **${resolvedName}** looks like right now: + +💰 **Current Price:** ${formatINR(coinData.inr)} +📈 **24h Change:** ${formatPercent(change)} (trending ${trend}) +📊 **Market Cap:** ${formatINR(coinData.inr_market_cap)} +💹 **24h Volume:** ${formatINR(coinData.inr_24h_vol)} + +📋 **Current Momentum:** ${sentiment} + +**⚠️ Important:** Crypto is highly volatile. Past performance doesn't guarantee future returns. Never invest more than you can afford to lose. This is data, not financial advice — always DYOR! + +Want deeper research? Try: +• "Tell me about ${coinName}" — for detailed coin info +• "Compare ${coinName} vs BTC" — benchmark against Bitcoin`; + } catch { + return "Couldn't fetch that data right now. Try again in a moment!"; + } +} + +// ─── Prediction Query Handler ───────────────────────────────────── + +async function handlePrediction(message) { + const coinName = extractCoinFromSentence(message); + + if (!coinName) { + return `I can't predict future prices — and honestly, no one reliably can! 🔮 + +What I **can** do is show you current data to help you form your own view: +• "Price of BTC" — current price + 24h trend +• "Top gainers today" — what's performing right now +• "Bitcoin dominance" — market structure overview + +**⚠️ Be cautious** of anyone claiming to know exactly where a coin is going.`; + } + + const coinId = resolveCoinId(coinName); + try { + const details = await getCoinDetails(coinId); + if (!details || !details.market_data) { + return `Couldn't find data for "${coinName}".\n\n🔮 Even if I could, price predictions are unreliable — always DYOR!`; + } + + const md = details.market_data; + const change24h = md.price_change_percentage_24h || 0; + const change7d = md.price_change_percentage_7d || 0; + const change30d = md.price_change_percentage_30d || 0; + + let trendSummary; + if (change24h > 0 && change7d > 0 && change30d > 0) { + trendSummary = + "🟢 All timeframes are positive — the trend has been consistently upward recently."; + } else if (change24h < 0 && change7d < 0 && change30d < 0) { + trendSummary = + "🔴 All timeframes are negative — the trend has been consistently downward recently."; + } else { + trendSummary = + "🟡 Mixed signals across timeframes — the trend is uncertain."; + } + + return `I can't predict where **${details.name}** will go, but here's the recent trend data: + +💰 **Price:** ${formatINR(md.current_price?.inr)} +📈 **24h:** ${formatPercent(change24h)} +📅 **7d:** ${formatPercent(change7d)} +📆 **30d:** ${formatPercent(change30d)} +📊 **Rank:** #${details.market_cap_rank || "N/A"} + +${trendSummary} + +**⚠️ Remember:** Past trends don't predict the future. Crypto is volatile and unpredictable. This is data, not a forecast!`; + } catch { + return "Couldn't fetch trend data right now. Try again shortly!"; + } +} + +// ─── Best Coin Handler ──────────────────────────────────────────── + +async function handleBestCoin() { + try { + const [trending, { gainers }] = await Promise.all([ + getTrending(), + getGainersLosers("inr", 3), + ]); + + const trendList = + trending + ?.slice(0, 3) + .map( + (c, i) => + `${i + 1}. **${c.name}** (${c.symbol?.toUpperCase()}) — Rank #${c.marketCapRank || "N/A"}`, + ) + .join("\n") || "No trending data"; + + const gainerList = gainers + .map( + (c, i) => + `${i + 1}. **${c.name}** (${c.symbol.toUpperCase()}) — ${formatPercent(c.price_change_percentage_24h)}`, + ) + .join("\n"); + + return `I can't recommend specific investments, but here's what the data shows right now: + +🔥 **Trending Coins:** +${trendList} + +🚀 **Today's Top Gainers:** +${gainerList} + +**⚠️ Important:** Trending or gaining doesn't mean "best to buy." High performers today could drop tomorrow. Always: +• Do your own research (DYOR) +• Never invest more than you can lose +• Consider the project's fundamentals, not just price + +Want to research a specific coin? Try "Tell me about [coin name]"`; + } catch { + return "Couldn't fetch market data right now. Try again in a moment!"; + } +} + +// ─── Unknown Handler ────────────────────────────────────────────── + +function handleUnknown() { + const suggestions = [ + "I'm not sure I understood that. Here are some things you can try:", + "Hmm, I didn't quite get that. Maybe try one of these:", + "I'm best at crypto-related questions! Here are some ideas:", + ]; + + const suggestion = + suggestions[Math.floor(Math.random() * suggestions.length)]; + + return `${suggestion} + +• "How's the market today?" +• "Price of Bitcoin" +• "Top gainers today" +• "Compare ETH vs SOL" +• "What is DeFi?" +• "Latest report summary" + +Or type **help** to see all my capabilities!`; +} + +// ─── Main Message Handler ───────────────────────────────────────── + +export async function processMessage(message) { + if (!message || message.trim().length === 0) { + return "Go ahead, ask me something about crypto! Type **help** to see what I can do."; + } + + const { intent, raw } = extractIntent(message); + + switch (intent) { + case "greeting": + return handleGreeting(); + case "help": + return handleHelp(); + case "market_overview": + return handleMarketOverview(); + case "price_check": + return handlePriceCheck(raw); + case "top_gainers": + return handleGainers(); + case "top_losers": + return handleLosers(); + case "trending": + return handleTrending(); + case "compare": + return handleCompare(raw); + case "coin_info": + return handleCoinInfo(raw); + case "report": + return handleReport(raw); + case "dominance": + return handleDominance(); + case "investment": + return handleInvestment(raw); + case "prediction": + return handlePrediction(raw); + case "best_coin": + return handleBestCoin(); + case "education": { + const eduResult = handleEducation(raw); + if (eduResult) return eduResult; + // If no education match, try coin info + return handleCoinInfo(raw); + } + default: { + // Last resort: check education, then coin info + const edu = handleEducation(raw); + if (edu) return edu; + + // Try to treat it as a price check + const coinName = extractCoinName(raw); + if (coinName && resolveCoinId(coinName)) { + return handlePriceCheck(raw); + } + + // Check if message mentions any known coin — might be an investment question + const coinFromSentence = extractCoinFromSentence(raw); + if (coinFromSentence && resolveCoinId(coinFromSentence)) { + return handleInvestment(raw); + } + + return handleUnknown(); + } + } +} diff --git a/src/CryptoChatbot/coinGeckoService.js b/src/CryptoChatbot/coinGeckoService.js new file mode 100644 index 0000000..0d6ed22 --- /dev/null +++ b/src/CryptoChatbot/coinGeckoService.js @@ -0,0 +1,183 @@ +/** + * CoinGecko Free API Service + * No API key required — uses public endpoints + * Rate limit: ~10-30 calls/min (plenty for a chatbot) + * + * All requests are now routed through apiClient which provides: + * - Rate limiting (25 req/min queue — stays under CoinGecko's 30/min cap) + * - Exponential backoff with Retry-After header support + * - Two-tier caching (fresh 60s / stale 5min / offline 24hr) + * - Request deduplication (same URL = one in-flight request) + * - 15-second request timeout + */ + +import apiClient from '../utils/apiClient'; + +const BASE_URL = "/api/coingecko"; + +// ─── Market Overview (global stats) ─────────────────────────────── +export async function getGlobalData() { + const data = await apiClient.get(`${BASE_URL}/global`); + const g = data.data; + return { + totalMarketCap: g.total_market_cap?.usd, + totalVolume: g.total_volume?.usd, + marketCapChangePercent24h: g.market_cap_change_percentage_24h_usd, + btcDominance: g.market_cap_percentage?.btc, + ethDominance: g.market_cap_percentage?.eth, + activeCryptos: g.active_cryptocurrencies, + }; +} + +// ─── Top Coins by Market Cap ────────────────────────────────────── +export async function getTopCoins(currency = "inr", perPage = 50, page = 1) { + const url = `${BASE_URL}/coins/markets?vs_currency=${currency}&order=market_cap_desc&per_page=${perPage}&page=${page}&sparkline=false&price_change_percentage=1h,24h,7d`; + return apiClient.get(url); +} + +// ─── Single Coin Price ──────────────────────────────────────────── +export async function getCoinPrice(coinId, currency = "inr") { + const url = `${BASE_URL}/simple/price?ids=${coinId}&vs_currencies=${currency}&include_24hr_change=true&include_24hr_vol=true&include_market_cap=true`; + return apiClient.get(url); +} + +// ─── Coin Details ───────────────────────────────────────────────── +export async function getCoinDetails(coinId) { + const url = `${BASE_URL}/coins/${coinId}?localization=false&tickers=false&market_data=true&community_data=false&developer_data=false`; + return apiClient.get(url); +} + +// ─── Trending Coins ─────────────────────────────────────────────── +export async function getTrending() { + const data = await apiClient.get(`${BASE_URL}/search/trending`); + return data.coins?.map((c) => ({ + id: c.item.id, + name: c.item.name, + symbol: c.item.symbol, + marketCapRank: c.item.market_cap_rank, + priceBtc: c.item.price_btc, + thumb: c.item.thumb, + })); +} + +// ─── Search Coin by Name/Symbol ─────────────────────────────────── +export async function searchCoin(query) { + const url = `${BASE_URL}/search?query=${encodeURIComponent(query)}`; + const data = await apiClient.get(url); + return data.coins?.slice(0, 5) || []; +} + +// ─── Derived: Top Gainers & Losers ──────────────────────────────── +export async function getGainersLosers(currency = "inr", count = 5) { + const coins = await getTopCoins(currency, 100); + const sorted = [...coins].sort( + (a, b) => + (b.price_change_percentage_24h || 0) - + (a.price_change_percentage_24h || 0), + ); + return { + gainers: sorted.slice(0, count), + losers: sorted.slice(-count).reverse(), + }; +} + +// ─── Derived: Compare Two Coins ─────────────────────────────────── +export async function compareCoins(coinId1, coinId2, currency = "inr") { + const coins = await getTopCoins(currency, 250); + const coin1 = coins.find((c) => c.id === coinId1); + const coin2 = coins.find((c) => c.id === coinId2); + return { coin1, coin2 }; +} + +// ─── Coin ID Resolution ────────────────────────────────────────── +// Maps common names/symbols to CoinGecko IDs +const COIN_ALIASES = { + btc: "bitcoin", + bitcoin: "bitcoin", + eth: "ethereum", + ether: "ethereum", + ethereum: "ethereum", + sol: "solana", + solana: "solana", + bnb: "binancecoin", + binance: "binancecoin", + xrp: "ripple", + ripple: "ripple", + ada: "cardano", + cardano: "cardano", + doge: "dogecoin", + dogecoin: "dogecoin", + dot: "polkadot", + polkadot: "polkadot", + matic: "matic-network", + polygon: "matic-network", + avax: "avalanche-2", + avalanche: "avalanche-2", + link: "chainlink", + chainlink: "chainlink", + shib: "shiba-inu", + "shiba inu": "shiba-inu", + shiba: "shiba-inu", + uni: "uniswap", + uniswap: "uniswap", + ltc: "litecoin", + litecoin: "litecoin", + atom: "cosmos", + cosmos: "cosmos", + near: "near", + "near protocol": "near", + xlm: "stellar", + stellar: "stellar", + algo: "algorand", + algorand: "algorand", + apt: "aptos", + aptos: "aptos", + arb: "arbitrum", + arbitrum: "arbitrum", + op: "optimism", + optimism: "optimism", + sui: "sui", + ton: "the-open-network", + toncoin: "the-open-network", + usdt: "tether", + tether: "tether", + usdc: "usd-coin", + pepe: "pepe", + wif: "dogwifhat", + trx: "tron", + tron: "tron", +}; + +export function resolveCoinId(input) { + if (!input) return null; + const key = input.toLowerCase().trim(); + return COIN_ALIASES[key] || key; +} + +// ─── Utility: Format Currency ───────────────────────────────────── +export function formatINR(num) { + if (num === null || num === undefined) return "N/A"; + if (num >= 1e12) return `₹${(num / 1e12).toFixed(2)}T`; + if (num >= 1e9) return `₹${(num / 1e9).toFixed(2)}B`; + if (num >= 1e7) return `₹${(num / 1e7).toFixed(2)}Cr`; + if (num >= 1e5) return `₹${(num / 1e5).toFixed(2)}L`; + if (num >= 1) + return `₹${num.toLocaleString("en-IN", { maximumFractionDigits: 2 })}`; + return `₹${num.toFixed(6)}`; +} + +export function formatUSD(num) { + if (num === null || num === undefined) return "N/A"; + if (num >= 1e12) return `$${(num / 1e12).toFixed(2)}T`; + if (num >= 1e9) return `$${(num / 1e9).toFixed(2)}B`; + if (num >= 1e6) return `$${(num / 1e6).toFixed(2)}M`; + if (num >= 1) + return `$${num.toLocaleString("en-US", { maximumFractionDigits: 2 })}`; + return `$${num.toFixed(6)}`; +} + +export function formatPercent(num) { + if (num === null || num === undefined) return "N/A"; + const sign = num >= 0 ? "+" : ""; + return `${sign}${num.toFixed(2)}%`; +} diff --git a/src/CryptoChatbot/knowledgeBase.js b/src/CryptoChatbot/knowledgeBase.js new file mode 100644 index 0000000..a6f4eaf --- /dev/null +++ b/src/CryptoChatbot/knowledgeBase.js @@ -0,0 +1,174 @@ +/** + * Knowledge Base — Static crypto education + report index + * No API calls needed. Runs entirely client-side. + */ + +// ─── Crypto Education Dictionary ────────────────────────────────── +export const EDUCATION = { + "market cap": { + term: "Market Cap (Market Capitalization)", + answer: + "Market cap is the total value of a cryptocurrency. It's calculated by multiplying the current price by the circulating supply. For example, if a coin costs ₹100 and there are 1 million coins in circulation, the market cap is ₹10 crore. It's the most common way to rank cryptocurrencies — Bitcoin has the largest market cap.", + }, + "circulating supply": { + term: "Circulating Supply", + answer: + "Circulating supply is the number of coins that are currently available and trading in the market. It doesn't include coins that are locked, reserved, or not yet released. Think of it like the number of shares of a company that are actually available for trading.", + }, + "total supply": { + term: "Total Supply", + answer: + "Total supply is the total number of coins that exist right now, including locked or reserved ones. It's different from max supply (the absolute maximum that can ever exist) and circulating supply (what's currently tradable).", + }, + "max supply": { + term: "Max Supply", + answer: + "Max supply is the maximum number of coins that will ever exist. Bitcoin's max supply is 21 million — once all are mined, no more will be created. Some coins like Ethereum don't have a max supply cap.", + }, + volume: { + term: "Trading Volume", + answer: + "Volume is the total amount of a coin that has been traded in a given time period (usually 24 hours). High volume means lots of buying and selling activity — it indicates strong interest and usually better liquidity (easier to buy/sell without affecting the price much).", + }, + defi: { + term: "DeFi (Decentralized Finance)", + answer: + "DeFi is a system of financial applications built on blockchain networks. Instead of banks or brokers, smart contracts handle lending, borrowing, trading, and earning interest. Popular DeFi platforms include Uniswap, Aave, and Compound. It's like a financial system that runs 24/7 without intermediaries.", + }, + nft: { + term: "NFT (Non-Fungible Token)", + answer: + "NFTs are unique digital tokens on a blockchain that represent ownership of a specific item — like digital art, music, game items, or collectibles. Unlike Bitcoin where every coin is identical, each NFT is one-of-a-kind. They're mostly on Ethereum and Solana.", + }, + blockchain: { + term: "Blockchain", + answer: + "A blockchain is a digital ledger that records transactions across many computers. Once data is recorded, it can't be easily changed. Think of it as a shared Google Sheet that everyone can read but no single person controls. Bitcoin and Ethereum are the most well-known blockchains.", + }, + staking: { + term: "Staking", + answer: + "Staking is locking up your crypto to help validate transactions on a blockchain network. In return, you earn rewards (like interest). It's used in Proof-of-Stake blockchains like Ethereum, Solana, and Cardano. It's similar to earning interest in a savings account, but with higher risk and reward.", + }, + "gas fee": { + term: "Gas Fee", + answer: + "Gas fees are transaction costs on blockchain networks, most commonly Ethereum. Every action on the network (sending tokens, swapping on DEXs, minting NFTs) requires computational work, and gas fees compensate the validators who do that work. Fees spike when the network is congested.", + }, + wallet: { + term: "Crypto Wallet", + answer: + "A crypto wallet stores your private keys — the passwords that give you access to your cryptocurrency. Hot wallets (MetaMask, Trust Wallet) are connected to the internet for easy access. Cold wallets (Ledger, Trezor) are offline hardware devices for maximum security.", + }, + altcoin: { + term: "Altcoin", + answer: + "Altcoin means 'alternative coin' — any cryptocurrency other than Bitcoin. Ethereum, Solana, Cardano, and thousands of others are all altcoins. They often move together with Bitcoin but can have their own independent price action based on their own technology and adoption.", + }, + "btc dominance": { + term: "Bitcoin Dominance", + answer: + "BTC Dominance is the percentage of the total crypto market cap that belongs to Bitcoin. When dominance is high (~60%+), it means money is concentrated in Bitcoin. When it drops, it usually means altcoins are gaining traction — often called 'alt season'.", + }, + whale: { + term: "Whale", + answer: + "A whale is someone who holds a very large amount of cryptocurrency. Their trades can significantly move the market. Tracking whale wallets and their movements (large transfers to/from exchanges) is a popular on-chain analysis technique.", + }, + "on-chain": { + term: "On-Chain Analysis", + answer: + "On-chain analysis examines data directly from the blockchain — wallet movements, exchange flows, active addresses, miner activity, etc. Unlike traditional technical analysis (which looks at price charts), on-chain data shows what holders are actually doing. Exchange outflows, for example, suggest people are holding rather than selling.", + }, + "exchange outflows": { + term: "Exchange Outflows", + answer: + "Exchange outflows happen when crypto is moved from exchanges to private wallets. This is generally seen as bullish because it means people are taking coins off exchanges (where they could easily sell) and into cold storage (long-term holding). The opposite — exchange inflows — can signal selling pressure.", + }, + hodl: { + term: "HODL", + answer: + "HODL started as a typo for 'hold' in a famous 2013 Bitcoin forum post and became crypto culture. It means holding your cryptocurrency long-term regardless of price drops. 'Diamond hands' is the modern meme version of the same idea.", + }, + dex: { + term: "DEX (Decentralized Exchange)", + answer: + "A DEX lets you trade crypto directly from your wallet without a middleman. Unlike centralized exchanges (Binance, Coinbase), there's no company holding your funds. Uniswap (Ethereum), Raydium (Solana), and PancakeSwap (BNB Chain) are popular DEXs.", + }, + layer2: { + term: "Layer 2", + answer: + "Layer 2 solutions are built on top of existing blockchains (Layer 1) to make them faster and cheaper. For Ethereum, popular L2s include Arbitrum, Optimism, and Base. They bundle many transactions together and settle them on the main chain, reducing fees dramatically.", + }, + halving: { + term: "Bitcoin Halving", + answer: + "Bitcoin halving is an event that happens roughly every 4 years where the mining reward is cut in half. It reduces the rate of new Bitcoin creation, making it more scarce. Historically, halvings have preceded major bull runs. The most recent halving was in April 2024.", + }, +}; + +// ─── Report Summaries (from your blog) ──────────────────────────── +// Add your actual report content here over time. +export const REPORTS = [ + { + id: "bitcoin-vector-37", + title: "The Bitcoin Vector #37", + category: "VECTOR", + type: "premium", + date: "2026-01", + summary: + "Bitcoin enters 2026 attempting to stabilise after its Q4 drawdown. The Vector models suggest a subtle shift in momentum as long-term holders continue accumulating while short-term volatility remains elevated. Key support levels and resistance zones are analyzed with on-chain metrics pointing to a potential accumulation phase.", + keywords: [ + "bitcoin", "btc", "vector", "momentum", "Q4", "drawdown", + "accumulation", "support", "resistance", "long-term holders", + ], + }, + { + id: "week-on-chain-2-2026", + title: "Week On-Chain #2 2026", + category: "WEEK ON-CHAIN", + type: "free", + date: "2026-01", + summary: + "Bitcoin shows early signs of stabilization as exchange outflows pick up, indicating growing holder conviction. On-chain data reveals accumulation patterns across multiple cohorts, with whale addresses increasing their positions. Network activity metrics show steady usage despite the recent price correction.", + keywords: [ + "bitcoin", "btc", "on-chain", "exchange outflows", "stabilization", + "whale", "accumulation", "network activity", "holder", + ], + }, +]; + +// ─── Report Search ──────────────────────────────────────────────── +export function searchReports(query) { + const q = query.toLowerCase(); + const words = q.split(/\s+/); + + return REPORTS.map((report) => { + let score = 0; + const searchText = `${report.title} ${report.category} ${report.summary} ${report.keywords.join(" ")}`.toLowerCase(); + + for (const word of words) { + if (word.length < 3) continue; + if (searchText.includes(word)) score++; + if (report.keywords.some((k) => k.includes(word))) score += 2; + } + + return { ...report, score }; + }) + .filter((r) => r.score > 0) + .sort((a, b) => b.score - a.score); +} + +// ─── Education Search ───────────────────────────────────────────── +export function searchEducation(query) { + const q = query.toLowerCase(); + const matches = []; + + for (const [key, entry] of Object.entries(EDUCATION)) { + if (q.includes(key) || key.split(" ").some((word) => q.includes(word) && word.length > 3)) { + matches.push(entry); + } + } + + return matches; +} \ No newline at end of file diff --git a/src/components/AIBlog/AIBlogCard.css b/src/components/AIBlog/AIBlogCard.css new file mode 100644 index 0000000..90da326 --- /dev/null +++ b/src/components/AIBlog/AIBlogCard.css @@ -0,0 +1,253 @@ +/* ========== AI BLOG CARD ========== */ +.ai-blog-card { + background: rgba(255, 255, 255, 0.02); + border: 1px solid rgba(255, 255, 255, 0.08); + border-radius: 20px; + overflow: hidden; + margin-top: 2rem; + animation: cardSlideUp 0.6s cubic-bezier(0.16, 1, 0.3, 1) forwards; +} + +@keyframes cardSlideUp { + from { + opacity: 0; + transform: translateY(30px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +/* Card Header */ +.ai-blog-card__header { + padding: 2rem 2rem 1.5rem; + border-bottom: 1px solid rgba(255, 255, 255, 0.06); + position: relative; +} + +.ai-blog-card__badges { + display: flex; + align-items: center; + gap: 0.75rem; + margin-bottom: 1rem; +} + +.ai-blog-card__badge { + display: inline-flex; + align-items: center; + gap: 0.4rem; + padding: 0.35rem 0.85rem; + border-radius: 20px; + font-size: 0.75rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.04em; +} + +.ai-blog-card__badge--ai { + background: linear-gradient(135deg, rgba(157, 78, 221, 0.2), rgba(69, 89, 220, 0.2)); + color: #c084fc; + border: 1px solid rgba(157, 78, 221, 0.3); +} + +.ai-blog-card__badge--demo { + background: rgba(245, 158, 11, 0.15); + color: #f59e0b; + border: 1px solid rgba(245, 158, 11, 0.3); +} + +.ai-blog-card__badge--live { + background: rgba(34, 197, 94, 0.15); + color: #22c55e; + border: 1px solid rgba(34, 197, 94, 0.3); +} + +.ai-blog-card__timestamp { + font-size: 0.8rem; + color: #666; +} + +.ai-blog-card__title { + font-size: 1.8rem; + font-weight: 800; + line-height: 1.2; + margin: 0; + background: linear-gradient(135deg, #ffffff 0%, #c0c0c0 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +/* Card Body */ +.ai-blog-card__body { + padding: 2rem; +} + +/* Introduction */ +.ai-blog-card__intro { + padding: 1.25rem 1.5rem; + background: rgba(69, 89, 220, 0.06); + border-left: 3px solid #4559DC; + border-radius: 0 12px 12px 0; + margin-bottom: 2rem; +} + +.ai-blog-card__intro p { + color: #d0d0d0; + font-size: 1.05rem; + line-height: 1.7; + margin: 0; +} + +/* Key Points */ +.ai-blog-card__section-title { + display: flex; + align-items: center; + gap: 0.6rem; + font-size: 1.1rem; + font-weight: 700; + color: #ffffff; + margin-bottom: 1.25rem; + text-transform: uppercase; + letter-spacing: 0.05em; +} + +.ai-blog-card__section-icon { + font-size: 1.1rem; +} + +.ai-blog-card__key-points { + display: flex; + flex-direction: column; + gap: 1rem; + margin-bottom: 2rem; +} + +.ai-blog-card__point { + display: flex; + gap: 1rem; + padding: 1.25rem; + background: rgba(255, 255, 255, 0.03); + border-radius: 12px; + border: 1px solid rgba(255, 255, 255, 0.05); + transition: all 0.3s ease; +} + +.ai-blog-card__point:hover { + background: rgba(255, 255, 255, 0.05); + border-color: rgba(69, 89, 220, 0.2); + transform: translateX(4px); +} + +.ai-blog-card__point-number { + display: flex; + align-items: center; + justify-content: center; + min-width: 32px; + height: 32px; + border-radius: 8px; + background: linear-gradient(135deg, #4559DC, #6366f1); + color: #ffffff; + font-size: 0.8rem; + font-weight: 700; + flex-shrink: 0; +} + +.ai-blog-card__point-content h4 { + font-size: 1rem; + font-weight: 600; + color: #ffffff; + margin: 0 0 0.4rem 0; +} + +.ai-blog-card__point-content p { + font-size: 0.92rem; + line-height: 1.6; + color: #a0a0a0; + margin: 0; +} + +/* Summary */ +.ai-blog-card__summary { + padding: 1.5rem; + background: linear-gradient( + 135deg, + rgba(34, 197, 94, 0.06), + rgba(69, 89, 220, 0.06) + ); + border-radius: 12px; + border: 1px solid rgba(34, 197, 94, 0.15); +} + +.ai-blog-card__summary p { + color: #d0d0d0; + font-size: 0.95rem; + line-height: 1.7; + margin: 0; +} + +/* Footer */ +.ai-blog-card__footer { + display: flex; + align-items: center; + justify-content: space-between; + padding: 1.25rem 2rem; + border-top: 1px solid rgba(255, 255, 255, 0.06); + background: rgba(255, 255, 255, 0.01); +} + +.ai-blog-card__footer-text { + font-size: 0.8rem; + color: #666; + font-style: italic; +} + +.ai-blog-card__new-btn { + display: inline-flex; + align-items: center; + gap: 0.5rem; + padding: 0.6rem 1.2rem; + background: rgba(69, 89, 220, 0.1); + color: #4559DC; + border: 1px solid rgba(69, 89, 220, 0.3); + border-radius: 10px; + font-size: 0.85rem; + font-weight: 600; + cursor: pointer; + transition: all 0.2s ease; +} + +.ai-blog-card__new-btn:hover { + background: rgba(69, 89, 220, 0.2); + border-color: #4559DC; + transform: translateY(-2px); +} + +/* ========== RESPONSIVE ========== */ +@media (max-width: 768px) { + .ai-blog-card__header, + .ai-blog-card__body { + padding: 1.5rem; + } + + .ai-blog-card__title { + font-size: 1.4rem; + } + + .ai-blog-card__point { + flex-direction: column; + gap: 0.75rem; + } + + .ai-blog-card__point-number { + width: fit-content; + padding: 0 0.75rem; + } + + .ai-blog-card__footer { + flex-direction: column; + gap: 1rem; + text-align: center; + } +} diff --git a/src/components/AIBlog/AIBlogCard.jsx b/src/components/AIBlog/AIBlogCard.jsx new file mode 100644 index 0000000..c6275d8 --- /dev/null +++ b/src/components/AIBlog/AIBlogCard.jsx @@ -0,0 +1,113 @@ +import React from "react"; +import { motion } from "framer-motion"; +import "./AIBlogCard.css"; + +/** + * AIBlogCard — renders a structured AI-generated blog article + * as a premium styled card. + * + * Props: + * article: { title, introduction, keyPoints[], summary, isDemo } + * onNewArticle: () => void — callback to generate a new article + */ +export default function AIBlogCard({ article, onNewArticle }) { + if (!article) return null; + + const { title, introduction, keyPoints, summary, isDemo } = article; + const now = new Date().toLocaleDateString("en-US", { + year: "numeric", + month: "short", + day: "numeric", + }); + + return ( + + {/* Header */} +
+
+ + ✨ AI Generated + + {isDemo ? ( + + Demo Mode + + ) : ( + + Live AI + + )} + {now} +
+

{title}

+
+ + {/* Body */} +
+ {/* Introduction */} +
+

{introduction}

+
+ + {/* Key Points */} +
+ 🔑 + Key Points +
+
+ {keyPoints?.map((point, index) => ( + +
+ {String(index + 1).padStart(2, "0")} +
+
+

{point.heading}

+

{point.text}

+
+
+ ))} +
+ + {/* Summary */} +
+ + Summary +
+
+

{summary}

+
+
+ + {/* Footer */} +
+ + {isDemo + ? "This is demo content. Add a Gemini API key for live AI generation." + : "Generated by Google Gemini AI"} + + +
+
+ ); +} diff --git a/src/components/AIBlog/AIBlogGenerator.css b/src/components/AIBlog/AIBlogGenerator.css new file mode 100644 index 0000000..7ca1c93 --- /dev/null +++ b/src/components/AIBlog/AIBlogGenerator.css @@ -0,0 +1,258 @@ +/* ========== AI BLOG GENERATOR ========== */ +.ai-blog-generator { + max-width: 720px; + margin: 0 auto 3rem; +} + +/* Input Area */ +.ai-blog-generator__input-area { + display: flex; + gap: 1rem; + padding: 0.5rem; + background: rgba(255, 255, 255, 0.03); + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 16px; + transition: all 0.3s ease; +} + +.ai-blog-generator__input-area:focus-within { + border-color: rgba(69, 89, 220, 0.5); + background: rgba(255, 255, 255, 0.04); + box-shadow: 0 0 0 4px rgba(69, 89, 220, 0.08); +} + +.ai-blog-generator__input { + flex: 1; + padding: 1rem 1.25rem; + background: transparent; + border: none; + color: #f7f8fa; + font-size: 1rem; + font-family: inherit; + outline: none; +} + +.ai-blog-generator__input::placeholder { + color: #666; +} + +.ai-blog-generator__btn { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 0.6rem; + padding: 0.9rem 1.8rem; + background: linear-gradient(135deg, #4559DC, #6366f1); + color: #ffffff; + border: none; + border-radius: 12px; + font-size: 0.95rem; + font-weight: 600; + cursor: pointer; + transition: all 0.25s ease; + white-space: nowrap; + min-width: 160px; +} + +.ai-blog-generator__btn:hover:not(:disabled) { + transform: translateY(-2px); + box-shadow: 0 8px 25px rgba(69, 89, 220, 0.35); +} + +.ai-blog-generator__btn:active:not(:disabled) { + transform: translateY(0); +} + +.ai-blog-generator__btn:disabled { + opacity: 0.7; + cursor: not-allowed; +} + +/* Spinner */ +.ai-blog-generator__spinner { + width: 18px; + height: 18px; + border: 2.5px solid rgba(255, 255, 255, 0.3); + border-top-color: #ffffff; + border-radius: 50%; + animation: aiSpinner 0.7s linear infinite; +} + +@keyframes aiSpinner { + to { + transform: rotate(360deg); + } +} + +/* Suggestions */ +.ai-blog-generator__suggestions { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; + margin-top: 1rem; + padding: 0 0.5rem; +} + +.ai-blog-generator__suggestion { + padding: 0.4rem 0.9rem; + background: rgba(255, 255, 255, 0.04); + border: 1px solid rgba(255, 255, 255, 0.08); + border-radius: 20px; + color: #a0a0a0; + font-size: 0.8rem; + cursor: pointer; + transition: all 0.2s ease; +} + +.ai-blog-generator__suggestion:hover { + background: rgba(69, 89, 220, 0.1); + color: #4559DC; + border-color: rgba(69, 89, 220, 0.3); +} + +/* Error */ +.ai-blog-generator__error { + margin-top: 1rem; + padding: 0.75rem 1.25rem; + background: rgba(239, 68, 68, 0.1); + border: 1px solid rgba(239, 68, 68, 0.3); + border-radius: 10px; + color: #f87171; + font-size: 0.9rem; +} + +/* ========== AI BLOG PAGE ========== */ +.ai-blog-page { + background: #0a0a0a; + color: #f7f8fa; + min-height: 100vh; + font-family: "Inter", -apple-system, BlinkMacSystemFont, sans-serif; + padding: 0; + margin: 0; +} + +.ai-blog-page__container { + max-width: 1280px; + margin: 0 auto; + padding: 2rem 1.5rem 4rem; +} + +/* Hero */ +.ai-blog-page__hero { + text-align: center; + padding: 4rem 0 3rem; + margin-bottom: 2rem; + border-bottom: 1px solid rgba(255, 255, 255, 0.08); +} + +.ai-blog-page__hero-badge { + display: inline-flex; + align-items: center; + gap: 0.5rem; + padding: 0.6rem 1.4rem; + background: linear-gradient(135deg, + rgba(157, 78, 221, 0.1), + rgba(69, 89, 220, 0.1)); + border: 1px solid rgba(157, 78, 221, 0.3); + border-radius: 24px; + font-size: 0.85rem; + font-weight: 600; + color: #c084fc; + letter-spacing: 0.03em; + margin-bottom: 1.5rem; + backdrop-filter: blur(10px); +} + +.ai-blog-page__hero-title { + font-size: clamp(2.5rem, 5vw, 3.5rem); + font-weight: 800; + line-height: 1.1; + margin: 0.5rem 0 1rem; + background: linear-gradient(135deg, #00f3ff 0%, #4559DC 50%, #9d4edd 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +.ai-blog-page__hero-subtitle { + color: #a0a0a0; + font-size: 1.1rem; + line-height: 1.6; + max-width: 600px; + margin: 0 auto; +} + +/* History Section */ +.ai-blog-page__history { + margin-top: 4rem; +} + +.ai-blog-page__history-header { + display: flex; + align-items: center; + gap: 0.75rem; + margin-bottom: 2rem; + padding-bottom: 1rem; + border-bottom: 1px solid rgba(255, 255, 255, 0.08); +} + +.ai-blog-page__history-title { + font-size: 1.4rem; + font-weight: 700; + color: #ffffff; + margin: 0; +} + +.ai-blog-page__history-count { + padding: 0.2rem 0.65rem; + background: rgba(69, 89, 220, 0.15); + color: #4559DC; + border-radius: 10px; + font-size: 0.8rem; + font-weight: 600; +} + +.ai-blog-page__back-link { + display: inline-flex; + align-items: center; + gap: 0.5rem; + color: #a0a0a0; + font-size: 0.9rem; + font-weight: 500; + text-decoration: none; + margin-bottom: 2rem; + transition: color 0.2s ease; + cursor: pointer; + background: none; + border: none; + padding: 0; +} + +.ai-blog-page__back-link:hover { + color: #4559DC; +} + +/* ========== RESPONSIVE ========== */ +@media (max-width: 640px) { + .ai-blog-generator__input-area { + flex-direction: column; + gap: 0.75rem; + } + + .ai-blog-generator__btn { + width: 100%; + justify-content: center; + } + + .ai-blog-page__hero { + padding: 3rem 0 2rem; + } + + .ai-blog-page__hero-title { + font-size: 2rem; + } + + .ai-blog-page__container { + padding: 1.5rem 1rem 3rem; + } +} \ No newline at end of file diff --git a/src/components/AIBlog/AIBlogGenerator.jsx b/src/components/AIBlog/AIBlogGenerator.jsx new file mode 100644 index 0000000..9922710 --- /dev/null +++ b/src/components/AIBlog/AIBlogGenerator.jsx @@ -0,0 +1,152 @@ +import React, { useState } from "react"; +import { motion } from "framer-motion"; +import { generateBlogPost } from "../../services/aiBlogService"; +import AIBlogCard from "./AIBlogCard"; +import "./AIBlogGenerator.css"; + +const SUGGESTIONS = [ + "What is staking?", + "DeFi explained", + "Bitcoin basics", + "Solana overview", + "What are gas fees?", + "NFTs for beginners", +]; + +/** + * AIBlogGenerator — input + generate button + rendered output. + * Completely self-contained, no props required. + * + * Props (optional): + * onArticleGenerated(article) — callback when an article is produced + */ +export default function AIBlogGenerator({ onArticleGenerated }) { + const [topic, setTopic] = useState(""); + const [loading, setLoading] = useState(false); + const [article, setArticle] = useState(null); + const [error, setError] = useState(""); + + const handleGenerate = async () => { + const trimmed = topic.trim(); + if (!trimmed) { + setError("Please enter a crypto topic to generate an article."); + return; + } + + setError(""); + setLoading(true); + setArticle(null); + + try { + const result = await generateBlogPost(trimmed); + setArticle(result); + onArticleGenerated?.(result); + } catch (err) { + setError(err.message || "Something went wrong. Please try again."); + } finally { + setLoading(false); + } + }; + + const handleKeyDown = (e) => { + if (e.key === "Enter" && !loading) { + handleGenerate(); + } + }; + + const handleSuggestion = (text) => { + setTopic(text); + setError(""); + }; + + const handleNewArticle = () => { + setArticle(null); + setTopic(""); + }; + + return ( +
+ {/* Input + Button */} + + setTopic(e.target.value)} + onKeyDown={handleKeyDown} + disabled={loading} + autoComplete="off" + /> + + + + {/* Quick Suggestions */} + {!article && ( + + {SUGGESTIONS.map((s) => ( + + ))} + + )} + + {/* Error Message */} + {error && ( + + {error} + + )} + + {/* Generated Article Card */} + {article && ( + + )} +
+ ); +} diff --git a/src/components/ChangePassword.css b/src/components/Auth/ChangePassword.css similarity index 100% rename from src/components/ChangePassword.css rename to src/components/Auth/ChangePassword.css diff --git a/src/components/ChangePassword.jsx b/src/components/Auth/ChangePassword.jsx similarity index 87% rename from src/components/ChangePassword.jsx rename to src/components/Auth/ChangePassword.jsx index 33a5133..9b9e301 100644 --- a/src/components/ChangePassword.jsx +++ b/src/components/Auth/ChangePassword.jsx @@ -1,9 +1,9 @@ import React, { useState } from "react"; -import { useAuth } from "../context/AuthContext"; +import { useAuth } from "../../context/useAuth"; import { useNavigate } from "react-router-dom"; import { FiEye, FiEyeOff } from "react-icons/fi"; import "./ChangePassword.css"; -import { notifyError, notifySuccess } from "../utils/notify"; +import { notifyError, notifySuccess } from "../../utils/notify"; const ChangePassword = () => { const [currentPassword, setCurrentPassword] = useState(""); @@ -80,7 +80,6 @@ const ChangePassword = () => { // clear previous messages setError(""); - // validate form before API Call if (!validateForm()) { @@ -97,23 +96,21 @@ const ChangePassword = () => { setConfirmPassword(""); } catch (err) { console.error("Failed to change password. ", err); - let message="failed to change password .please try again."; + let message = "failed to change password .please try again."; // Handle specific Firebase errors if (err.code === "auth/wrong-password") { - message="Current password is incorrect."; + message = "Current password is incorrect."; } else if (err.code === "auth/weak-password") { - message="New password is too weak. Please use a stronger password."; + message = "New password is too weak. Please use a stronger password."; } else if (err.code === "auth/requires-recent-login") { - message="Session expired. Please log out and log in again."; + message = "Session expired. Please log out and log in again."; } else if (err.code === "auth/too-many-requests") { - message="Too many attempts. Please try again later."; - } - else if (err.code ==="auth/invalid-credential"){ - message="Invalid credentials provided."; - } - else { - message=err.message || "Failed to change password. Please try again."; + message = "Too many attempts. Please try again later."; + } else if (err.code === "auth/invalid-credential") { + message = "Invalid credentials provided."; + } else { + message = err.message || "Failed to change password. Please try again."; } notifyError(message); } finally { @@ -127,7 +124,6 @@ const ChangePassword = () => {

Change Password

Update your account password

- {/* Error Message */} {error &&
{error}
} @@ -150,6 +146,9 @@ const ChangePassword = () => { className="toggle-password" onClick={() => setShowCurrentPassword(!showCurrentPassword)} tabIndex={-1} + aria-label={ + showCurrentPassword ? "Hide password" : "Show password" + } > {showCurrentPassword ? : } @@ -174,6 +173,7 @@ const ChangePassword = () => { className="toggle-password" onClick={() => setShowNewPassword(!showNewPassword)} tabIndex={-1} + aria-label={showNewPassword ? "Hide password" : "Show password"} > {showNewPassword ? : } @@ -198,6 +198,9 @@ const ChangePassword = () => { className="toggle-password" onClick={() => setShowConfirmPassword(!showConfirmPassword)} tabIndex={-1} + aria-label={ + showConfirmPassword ? "Hide password" : "Show password" + } > {showConfirmPassword ? : } diff --git a/src/components/Auth/EmailVerification.css b/src/components/Auth/EmailVerification.css new file mode 100644 index 0000000..ec1cbac --- /dev/null +++ b/src/components/Auth/EmailVerification.css @@ -0,0 +1,237 @@ +.email-verification-page { + min-height: 100vh; + display: flex; + align-items: center; + justify-content: center; + padding: 2rem 1rem; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); +} + +.email-verification-container { + background: white; + border-radius: 1rem; + padding: 3rem 2rem; + max-width: 500px; + width: 100%; + box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3); + text-align: center; + animation: slideUp 0.5s ease-out; +} + +@keyframes slideUp { + from { + opacity: 0; + transform: translateY(30px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.email-verification-icon { + width: 80px; + height: 80px; + margin: 0 auto 1.5rem; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-size: 2.5rem; + color: white; +} + +.email-verification-title { + font-size: 1.875rem; + font-weight: 700; + color: #1a202c; + margin-bottom: 1rem; +} + +.email-verification-message { + font-size: 1rem; + color: #4a5568; + margin-bottom: 0.5rem; +} + +.email-verification-email { + font-size: 1.125rem; + font-weight: 600; + color: #667eea; + margin-bottom: 1.5rem; + word-break: break-word; +} + +.email-verification-instructions { + font-size: 0.9375rem; + color: #718096; + line-height: 1.6; + margin-bottom: 2rem; + padding: 0 1rem; +} + +.email-verification-actions { + display: flex; + flex-direction: column; + gap: 1rem; + margin-bottom: 2rem; +} + +.email-verification-btn { + display: flex; + align-items: center; + justify-content: center; + gap: 0.5rem; + padding: 0.875rem 1.5rem; + font-size: 1rem; + font-weight: 600; + border-radius: 0.5rem; + border: none; + cursor: pointer; + transition: all 0.3s ease; + width: 100%; +} + +.btn-primary { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; +} + +.btn-primary:hover:not(:disabled) { + transform: translateY(-2px); + box-shadow: 0 10px 20px rgba(102, 126, 234, 0.4); +} + +.btn-primary:active:not(:disabled) { + transform: translateY(0); +} + +.btn-primary:disabled { + opacity: 0.6; + cursor: not-allowed; +} + +.btn-secondary { + background: white; + color: #667eea; + border: 2px solid #667eea; +} + +.btn-secondary:hover:not(:disabled) { + background: #f7fafc; + transform: translateY(-2px); + box-shadow: 0 10px 20px rgba(102, 126, 234, 0.2); +} + +.btn-secondary:active:not(:disabled) { + transform: translateY(0); +} + +.btn-secondary:disabled { + opacity: 0.6; + cursor: not-allowed; +} + +.btn-text { + background: none; + border: none; + color: #667eea; + font-size: 0.875rem; + cursor: pointer; + padding: 0.5rem; + transition: color 0.3s ease; +} + +.btn-text:hover { + color: #764ba2; + text-decoration: underline; +} + +.email-verification-footer { + border-top: 1px solid #e2e8f0; + padding-top: 1.5rem; + display: flex; + flex-direction: column; + gap: 0.75rem; +} + +.email-verification-help { + font-size: 0.875rem; + color: #718096; +} + +.email-verification-help a { + color: #667eea; + text-decoration: none; + font-weight: 600; +} + +.email-verification-help a:hover { + text-decoration: underline; +} + +.spinning { + animation: spin 1s linear infinite; +} + +@keyframes spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + +/* Dark mode support */ +@media (prefers-color-scheme: dark) { + .email-verification-container { + background: #1a202c; + } + + .email-verification-title { + color: #f7fafc; + } + + .email-verification-message, + .email-verification-instructions, + .email-verification-help { + color: #a0aec0; + } + + .email-verification-footer { + border-top-color: #2d3748; + } + + .btn-secondary { + background: #2d3748; + color: #667eea; + border-color: #667eea; + } + + .btn-secondary:hover:not(:disabled) { + background: #374151; + } +} + +/* Responsive design */ +@media (max-width: 640px) { + .email-verification-container { + padding: 2rem 1.5rem; + } + + .email-verification-title { + font-size: 1.5rem; + } + + .email-verification-icon { + width: 60px; + height: 60px; + font-size: 2rem; + } + + .email-verification-instructions { + padding: 0; + } +} diff --git a/src/components/Auth/EmailVerification.jsx b/src/components/Auth/EmailVerification.jsx new file mode 100644 index 0000000..66da8e8 --- /dev/null +++ b/src/components/Auth/EmailVerification.jsx @@ -0,0 +1,148 @@ +import React, { useState } from "react"; +import { useNavigate } from "react-router-dom"; +import { useAuth } from "../../context/useAuth"; +import { FiMail, FiRefreshCw, FiCheckCircle } from "react-icons/fi"; +import { notifyError, notifySuccess, notifyInfo } from "../../utils/notify"; +import "./EmailVerification.css"; + +function EmailVerification() { + const { + currentUser, + sendVerificationEmail, + reloadUserVerificationStatus, + logout, + } = useAuth(); + const navigate = useNavigate(); + const [loading, setLoading] = useState(false); + const [checkingStatus, setCheckingStatus] = useState(false); + + const handleResendEmail = async () => { + setLoading(true); + try { + await sendVerificationEmail(); + notifySuccess("Verification email sent! Please check your inbox."); + } catch (error) { + console.error("Error sending verification email:", error); + + let errorMessage = "Failed to send verification email. Please try again."; + + if (error.code === "auth/too-many-requests") { + errorMessage = + "Too many requests. Please wait a few minutes before trying again."; + } + + notifyError(errorMessage); + } finally { + setLoading(false); + } + }; + + const handleCheckVerification = async () => { + setCheckingStatus(true); + try { + const isVerified = await reloadUserVerificationStatus(); + + if (isVerified) { + notifySuccess( + "Email verified successfully! Redirecting to dashboard...", + ); + setTimeout(() => { + navigate("/dashboard"); + }, 1500); + } else { + notifyInfo( + "Email not verified yet. Please check your inbox and click the verification link.", + ); + } + } catch (error) { + console.error("Error checking verification status:", error); + notifyError("Failed to check verification status. Please try again."); + } finally { + setCheckingStatus(false); + } + }; + + const handleLogout = async () => { + try { + await logout(); + navigate("/"); + } catch (error) { + console.error("Logout error:", error); + notifyError("Failed to logout. Please try again."); + } + }; + + return ( +
+
+
+ +
+ +

Verify Your Email

+ +

+ We've sent a verification email to: +

+ +

{currentUser?.email}

+ +

+ Please check your inbox and click the verification link to activate + your account. Don't forget to check your spam folder if you don't see + the email. +

+ +
+ + + +
+ +
+

+ Need help? Contact Support +

+ + +
+
+
+ ); +} + +export default EmailVerification; diff --git a/src/components/ForgotPassword.css b/src/components/Auth/ForgotPassword.css similarity index 100% rename from src/components/ForgotPassword.css rename to src/components/Auth/ForgotPassword.css diff --git a/src/components/ForgotPassword.jsx b/src/components/Auth/ForgotPassword.jsx similarity index 92% rename from src/components/ForgotPassword.jsx rename to src/components/Auth/ForgotPassword.jsx index 2cc2d56..64739c2 100644 --- a/src/components/ForgotPassword.jsx +++ b/src/components/Auth/ForgotPassword.jsx @@ -1,10 +1,10 @@ import React, { useState } from "react"; import { Link, useNavigate } from "react-router-dom"; -import { useAuth } from "../context/AuthContext"; +import { useAuth } from "../../context/useAuth"; import "./ForgotPassword.css"; import { motion } from "framer-motion"; import { FiMail, FiArrowLeft, FiCheckCircle } from "react-icons/fi"; -import { notifyError, notifySuccess } from "../utils/notify"; +import { notifyError, notifySuccess } from "../../utils/notify"; function ForgotPassword() { const navigate = useNavigate(); @@ -45,7 +45,7 @@ function ForgotPassword() { await resetPassword(email); setEmailSent(true); notifySuccess("Password reset email sent! Check your inbox."); - + // Redirect to login after 5 seconds setTimeout(() => navigate("/login"), 5000); } catch (error) { @@ -56,7 +56,9 @@ function ForgotPassword() { if (error.code === "auth/user-not-found") { // For security, we still show success message even if user not found setEmailSent(true); - notifySuccess("If an account exists with this email, a reset link has been sent."); + notifySuccess( + "If an account exists with this email, a reset link has been sent.", + ); setTimeout(() => navigate("/login"), 5000); return; } else if (error.code === "auth/too-many-requests") { @@ -91,7 +93,8 @@ function ForgotPassword() {

Forgot Password?

- No worries! Enter your email and we'll send you reset instructions. + No worries! Enter your email and we'll send you reset + instructions.

@@ -118,7 +121,11 @@ function ForgotPassword() { )}
- diff --git a/src/components/Login.css b/src/components/Auth/Login.css similarity index 95% rename from src/components/Login.css rename to src/components/Auth/Login.css index 8dcabff..ddd3366 100644 --- a/src/components/Login.css +++ b/src/components/Auth/Login.css @@ -98,10 +98,16 @@ .input-icon { position: absolute; - left: 15px; + left: 14px; color: var(--text-muted); - font-size: 1.1rem; - z-index: 10; + font-size: 1.2rem; + z-index: 5; + pointer-events: none; + width: 24px; + height: 24px; + display: flex; + align-items: center; + justify-content: center; } .auth-input { @@ -109,14 +115,18 @@ background: rgba(255, 255, 255, 0.03); border: 1px solid rgba(255, 255, 255, 0.1); border-radius: 12px; - padding: 12px 16px 12px 45px; - /* Space for icon */ + padding: 12px 50px 12px 60px; color: #fff; font-size: 1rem; outline: none; transition: all 0.3s ease; } +.input-with-icon .auth-input { + padding-left: 64px; + padding-right: 52px; +} + .auth-input:focus { border-color: var(--neon-cyan); box-shadow: 0 0 15px rgba(0, 243, 255, 0.1); diff --git a/src/components/Login.jsx b/src/components/Auth/Login.jsx similarity index 89% rename from src/components/Login.jsx rename to src/components/Auth/Login.jsx index 2aca274..823bfd0 100644 --- a/src/components/Login.jsx +++ b/src/components/Auth/Login.jsx @@ -1,11 +1,10 @@ import React, { useState } from "react"; import { Link, useNavigate } from "react-router-dom"; -import { useAuth } from "../context/AuthContext"; +import { useAuth } from "../../context/useAuth"; import "./Login.css"; -// eslint-disable-next-line no-unused-vars import { motion } from "framer-motion"; import { FiMail, FiLock, FiEye, FiEyeOff } from "react-icons/fi"; -import { notifyError, notifySuccess } from "../utils/notify"; +import { notifyError, notifySuccess, notifyInfo } from "../../utils/notify"; function Login() { const navigate = useNavigate(); @@ -64,7 +63,16 @@ function Login() { setErrors({}); try { - await login(formData.email, formData.password); + const userCredential = await login(formData.email, formData.password); + const user = userCredential.user; + + // Check if email is verified + if (!user.emailVerified) { + notifyInfo("Please verify your email address to continue."); + setTimeout(() => navigate("/verify-email"), 1500); + return; + } + notifySuccess("Logged in successfully"); setTimeout(() => navigate("/dashboard"), 1500); } catch (error) { @@ -98,7 +106,7 @@ function Login() { try { await loginWithGoogle(); notifySuccess("Logged in successfully with Google"); - setTimeout(() => navigate("/dashboard"), 1500); + setTimeout(() => navigate("/dashboard"), 1500); } catch (error) { console.error("Google login error:", error); let errorMessage = "Failed to login with Google. Please try again."; @@ -130,7 +138,8 @@ function Login() {

Welcome Back

- Access your CryptoHub Dashboard + Access your CryptoHub{" "} + Dashboard

@@ -176,6 +185,7 @@ function Login() { className="toggle-password" onClick={() => setShowPassword(!showPassword)} disabled={loading} + aria-label={showPassword ? "Hide password" : "Show password"} > {showPassword ? : } @@ -209,7 +219,11 @@ function Login() { onClick={handleGoogleLogin} disabled={loading} > - Google + Google Google Account @@ -226,4 +240,4 @@ function Login() { ); } -export default Login; \ No newline at end of file +export default Login; diff --git a/src/components/Logout.css b/src/components/Auth/Logout.css similarity index 100% rename from src/components/Logout.css rename to src/components/Auth/Logout.css diff --git a/src/components/Logout.jsx b/src/components/Auth/Logout.jsx similarity index 100% rename from src/components/Logout.jsx rename to src/components/Auth/Logout.jsx diff --git a/src/components/Auth/PrivateRoute.jsx b/src/components/Auth/PrivateRoute.jsx new file mode 100644 index 0000000..390828a --- /dev/null +++ b/src/components/Auth/PrivateRoute.jsx @@ -0,0 +1,42 @@ +import React from "react"; +import { Navigate, useLocation } from "react-router-dom"; +import { useAuth } from "../../context/useAuth"; + +const PrivateRoute = ({ children }) => { + const { currentUser, loading } = useAuth(); + const location = useLocation(); + + if (loading) { + return children; + } + + // If user is not logged in, redirect to login + if (!currentUser) { + return ; + } + + // Check if user needs email verification + // Allow access to verification page itself + const isVerificationPage = location.pathname === "/verify-email"; + + // Check if user is using email/password provider (not Google/other OAuth) + const isEmailPasswordUser = currentUser.providerData?.some( + (provider) => provider.providerId === "password", + ); + + // Redirect to verification page if: + // 1. User is using email/password authentication + // 2. Email is not verified + // 3. Not already on the verification page + if ( + isEmailPasswordUser && + !currentUser.emailVerified && + !isVerificationPage + ) { + return ; + } + + return children; +}; + +export default PrivateRoute; diff --git a/src/components/Auth/Signup.css b/src/components/Auth/Signup.css new file mode 100644 index 0000000..a22d6ef --- /dev/null +++ b/src/components/Auth/Signup.css @@ -0,0 +1,145 @@ +/* Signup.css - Fixed to work with existing Login.css */ +@import "./Login.css"; + +/* Signup specific overrides */ +.signup-card { + max-width: 480px; + padding: 40px 35px !important; /* Override any existing padding */ +} + +/* Fix form spacing for signup fields than login */ +.auth-form { + margin-top: 10px !important; +} + +/* Fix terms checkbox alignment */ +.terms-checkbox { + display: flex; + align-items: flex-start; /* Changed from center */ + gap: 10px; + margin: 15px 0 20px; + padding: 8px 0; + width: 100%; +} + +.terms-checkbox input[type="checkbox"] { + margin-top: 3px; /* Align with first line of text */ + width: 18px; + height: 18px; + accent-color: var(--neon-cyan); + cursor: pointer; + flex-shrink: 0; +} + +.terms-checkbox label { + font-size: 0.9rem; + color: var(--text-muted); + line-height: 1.5; + cursor: pointer; + user-select: none; + text-align: left; +} + +.terms-link { + color: var(--neon-cyan); + font-weight: 500; + text-decoration: none; + transition: color 0.2s ease; +} + +.terms-link:hover { + color: #fff; + text-decoration: underline; +} + +/* Ensure proper spacing between form groups */ +.form-group { + margin-bottom: 20px !important; +} + +/* Fix input icon position for signup */ +.input-with-icon { + position: relative; + width: 100%; +} + +.input-icon { + position: absolute; + left: 15px; + top: 50%; + transform: translateY(-50%); + color: var(--text-muted); + font-size: 1.1rem; + z-index: 10; +} + +/* Adjust padding for inputs with icons */ +.auth-input { + padding-left: 45px !important; /* Ensure space for icon */ + padding-right: 45px !important; /* Ensure space for toggle button */ +} + +/* Fix toggle password button positioning */ +.toggle-password { + position: absolute !important; + right: 15px; + top: 50%; + transform: translateY(-50%); + background: none; + border: none; + color: var(--text-muted); + cursor: pointer; + font-size: 1.1rem; + padding: 5px; + display: flex; + align-items: center; + justify-content: center; + z-index: 10; + transition: color 0.2s; +} + +.toggle-password:hover { + color: #fff; +} + +/* Fix error message spacing */ +.error-message { + display: block; + color: #ff3366; + font-size: 0.8rem; + margin-top: 5px !important; + margin-left: 5px; + min-height: 20px; /* Reserve space to prevent layout shift */ +} + +/* Fix button spacing */ +.btn-neon-purple.w-full { + margin-top: 5px !important; + margin-bottom: 5px !important; +} + +/* Fix divider spacing */ +.divider { + margin: 20px 0 !important; +} + +/* Fix Google button spacing */ +.google-signin-btn { + margin-top: 5px !important; +} + +/* Fix footer spacing */ +.auth-footer { + margin-top: 20px !important; +} + +/* Responsive adjustments for signup */ +@media (max-width: 500px) { + .signup-card { + padding: 30px 20px !important; + } + + .terms-checkbox label { + font-size: 0.85rem; + } +} \ No newline at end of file diff --git a/src/components/Signup.jsx b/src/components/Auth/Signup.jsx similarity index 81% rename from src/components/Signup.jsx rename to src/components/Auth/Signup.jsx index 29e4a74..318614d 100644 --- a/src/components/Signup.jsx +++ b/src/components/Auth/Signup.jsx @@ -1,11 +1,10 @@ import React, { useState } from "react"; import { Link, useNavigate } from "react-router-dom"; -import { useAuth } from "../context/AuthContext"; +import { useAuth } from "../../context/useAuth"; import "./Signup.css"; -// eslint-disable-next-line no-unused-vars import { motion } from "framer-motion"; import { FiUser, FiMail, FiLock, FiEye, FiEyeOff } from "react-icons/fi"; -import { notifyError, notifySuccess } from "../utils/notify"; +import { notifyError, notifySuccess } from "../../utils/notify"; function Signup() { const navigate = useNavigate(); @@ -15,7 +14,7 @@ function Signup() { fullName: "", email: "", password: "", - confirmPassword: "" + confirmPassword: "", }); const [errors, setErrors] = useState({}); @@ -63,7 +62,8 @@ function Signup() { } else if (formData.password.length < 8) { newErrors.password = "Password must be at least 8 characters"; } else if (!/(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/.test(formData.password)) { - newErrors.password = "Password must contain uppercase, lowercase and number"; + newErrors.password = + "Password must contain uppercase, lowercase and number"; } if (!formData.confirmPassword) { @@ -85,8 +85,10 @@ function Signup() { try { await signup(formData.email, formData.password, formData.fullName); - notifySuccess("Signed up successfully"); - setTimeout(() => navigate("/dashboard"), 1500); + notifySuccess( + "Account created! Please check your email to verify your account.", + ); + setTimeout(() => navigate("/verify-email"), 1500); } catch (error) { console.error("Signup error:", error); @@ -94,9 +96,11 @@ function Signup() { let errorMessage = "Failed to create account. Please try again."; if (error.code === "auth/email-already-in-use") { - errorMessage = "This email is already registered. Please login instead."; + errorMessage = + "This email is already registered. Please login instead."; } else if (error.code === "auth/weak-password") { - errorMessage = "Password is too weak. Please use a stronger password."; + errorMessage = + "Password is too weak. Please use a stronger password."; } else if (error.code === "auth/invalid-email") { errorMessage = "Invalid email address."; } else if (error.code === "auth/network-request-failed") { @@ -116,8 +120,8 @@ function Signup() { try { await loginWithGoogle(); - notifySuccess("Signed up successfully with Google"); - setTimeout(() => navigate("/dashboard"), 1500); + notifySuccess("Signed up successfully with Google"); + setTimeout(() => navigate("/dashboard"), 1500); } catch (error) { console.error("Google signup error:", error); let errorMessage = "Failed to sign up with Google. Please try again."; @@ -128,7 +132,7 @@ function Signup() { errorMessage = "Network error. Please check your connection"; } - notifyError(errorMessage); + notifyError(errorMessage); } finally { setLoading(false); } @@ -148,7 +152,8 @@ function Signup() {

Create Account

- Join CryptoHub and start tracking today + Join CryptoHub and + start tracking today

@@ -169,7 +174,9 @@ function Signup() { autoComplete="name" /> - {errors.fullName && {errors.fullName}} + {errors.fullName && ( + {errors.fullName} + )}
@@ -188,7 +195,9 @@ function Signup() { autoComplete="email" />
- {errors.email && {errors.email}} + {errors.email && ( + {errors.email} + )}
@@ -211,11 +220,14 @@ function Signup() { className="toggle-password" onClick={() => setShowPassword(!showPassword)} disabled={loading} + aria-label={showPassword ? "Hide password" : "Show password"} > {showPassword ? : }
- {errors.password && {errors.password}} + {errors.password && ( + {errors.password} + )}
@@ -238,6 +250,9 @@ function Signup() { className="toggle-password" onClick={() => setShowConfirmPassword(!showConfirmPassword)} disabled={loading} + aria-label={ + showConfirmPassword ? "Hide password" : "Show password" + } > {showConfirmPassword ? : } @@ -251,13 +266,21 @@ function Signup() {
- @@ -271,7 +294,11 @@ function Signup() { onClick={handleGoogleSignup} disabled={loading} > - Google + Google Google Account @@ -289,4 +316,4 @@ function Signup() { ); } -export default Signup; \ No newline at end of file +export default Signup; diff --git a/src/components/Blog.jsx b/src/components/Blog.jsx deleted file mode 100644 index 80625ab..0000000 --- a/src/components/Blog.jsx +++ /dev/null @@ -1,680 +0,0 @@ -import React from "react"; -import "./Blog.css"; -import { useNavigate } from "react-router-dom"; -import { motion } from "framer-motion"; - -// Import all images -import image1 from "../assets/1.png"; -import image2 from "../assets/2.png"; -import image3 from "../assets/3.png"; -import image4 from "../assets/4.png"; -import image5 from "../assets/5.png"; -import image6 from "../assets/6.png"; -import image7 from "../assets/7.png"; -import image8 from "../assets/8.png"; -import image9 from "../assets/9.png"; -import image10 from "../assets/10.png"; -import image11 from "../assets/11.png"; -import image12 from "../assets/12.png"; -import image13 from "../assets/13.png"; -import image14 from "../assets/14.png"; -import image15 from "../assets/15.png"; -import image16 from "../assets/16.png"; -import image17 from "../assets/17.png"; -import image18 from "../assets/18.png"; -import image19 from "../assets/19.png"; -import image20 from "../assets/20.png"; -import image21 from "../assets/21.png"; -import image22 from "../assets/22.png"; -import image23 from "../assets/23.png"; -import image24 from "../assets/24.png"; -import image25 from "../assets/25.png"; -import image26 from "../assets/26.png"; -import image27 from "../assets/27.png"; -import image29 from "../assets/29.png"; -import image30 from "../assets/30.png"; - -// Create array of imported images -const imageUrls = [ - image1, image2, image3, image4, image5, image6, image7, image8, image9, image10, - image11, image12, image13, image14, image15, image16, image17, image18, image19, image20, - image21, image22, image23, image24, image25, image26, image27, image29, image30,//image28 -]; - -// Categories and tags for variation -const categories = ["Vector", "Market Pulse", "Week On-chain", "Research", "Partner Reports", "Market Vectors", "On-chain Analysis", "Market Intelligence"]; -const tags = ["Premium", "Free", "Featured"]; -const badgeColors = ["#4559DC", "#22c55e", "#9d4edd", "#f59e0b"]; - -// Real Glassnode blog posts data -export const generateBlogPosts = () => { - return [ - { - id: 1, - title: "The Bitcoin Vector #37", - excerpt: "Bitcoin enters 2026 attempting to stabilise after its Q4 drawdown. The Vector models suggest a subtle shift in momentum as long-term holders resume accumulation.", - date: "Jan 10, 2026", - readTime: "12 min read", - image: image1, - category: "Vector", - tag: "Premium", - badgeColor: "#4559DC", - isFeatured: true, - gradient: "linear-gradient(135deg, #4559DC30, #22c55e20)", - content: { - toc: ["Market Overview", "On-chain Metrics", "Supply Dynamics", "Price Action Analysis", "Forward Outlook"], - sections: [ - { - heading: "Market Overview", - text: "Bitcoin enters the new year with cautious optimism as markets attempt to stabilize following the Q4 2025 drawdown. The Vector framework indicates subtle shifts in market structure that professional traders should monitor closely." - }, - { - heading: "On-chain Metrics", - text: "Long-term holder supply has resumed growth after a period of distribution, suggesting renewed conviction from Bitcoin's most steadfast investors. Exchange balances continue their multi-year decline, with only 11.5% of circulating supply remaining on exchanges." - }, - { - heading: "Supply Dynamics", - text: "The percentage of supply held in profit has recovered to 85%, indicating most holders remain in profit despite recent volatility. Realized capitalization growth suggests organic capital inflow rather than speculative trading." - } - ] - } - }, - { - id: 2, - title: "Week On-Chain #2 2026", - excerpt: "Bitcoin shows early signs of stabilization as exchange outflows accelerate. Network fundamentals remain strong despite price volatility.", - date: "Jan 9, 2026", - readTime: "8 min read", - image: image2, - category: "Week On-chain", - tag: "Free", - badgeColor: "#22c55e", - isFeatured: true, - gradient: "linear-gradient(135deg, #22c55e30, #9d4edd20)", - content: { - toc: ["Weekly Summary", "Exchange Flows", "Miner Activity", "Network Health", "Trading Volume"], - sections: [ - { - heading: "Weekly Summary", - text: "The second week of 2026 shows Bitcoin attempting to establish a new equilibrium. Exchange net outflows totaled 15,000 BTC this week, the highest since November 2025." - }, - { - heading: "Exchange Flows", - text: "Major exchanges recorded significant outflows, particularly from institutional custody solutions. This suggests accumulation by long-term investors despite uncertain price action." - } - ] - } - }, - { - id: 3, - title: "Market Pulse: January 2026", - excerpt: "Bitcoin volatility compresses as options markets signal uncertainty. Dealer gamma positioning suggests potential for explosive moves.", - date: "Jan 8, 2026", - readTime: "10 min read", - image: image3, - category: "Market Pulse", - tag: "Premium", - badgeColor: "#9d4edd", - isFeatured: true, - gradient: "linear-gradient(135deg, #9d4edd30, #f59e0b20)", - content: { - toc: ["Volatility Analysis", "Options Positioning", "Liquidity Conditions", "Market Sentiment", "Risk Assessment"], - sections: [ - { - heading: "Volatility Analysis", - text: "Bitcoin's 30-day realized volatility has compressed to 45%, approaching yearly lows. This compression often precedes significant directional moves." - }, - { - heading: "Options Positioning", - text: "Dealer gamma exposure is turning positive near current price levels, creating potential for accelerated moves should key technical levels break." - } - ] - } - }, - { - id: 4, - title: "Ethereum: The Merge Anniversary Report", - excerpt: "One year post-Merge: analyzing Ethereum's transition to proof-of-stake and its impact on supply dynamics, security, and network economics.", - date: "Jan 7, 2026", - readTime: "15 min read", - image: image4, - category: "Research", - tag: "Premium", - badgeColor: "#f59e0b", - isFeatured: false, - gradient: "linear-gradient(135deg, #f59e0b30, #4559DC20)" - }, - { - id: 5, - title: "Altcoin Vector #35: Layer 2 Ecosystem", - excerpt: "Deep dive into Ethereum Layer 2 scaling solutions: Arbitrum, Optimism, zkSync, and StarkNet adoption metrics and value capture analysis.", - date: "Jan 6, 2026", - readTime: "14 min read", - image: image5, - category: "Vector", - tag: "Premium", - badgeColor: "#4559DC", - isFeatured: false, - gradient: "linear-gradient(135deg, #4559DC30, #22c55e20)" - }, - { - id: 6, - title: "Bitcoin Mining Report: Q4 2025", - excerpt: "Analysis of Bitcoin mining industry post-halving: hash rate trends, miner revenue, and the transition to sustainable energy sources.", - date: "Jan 5, 2026", - readTime: "11 min read", - image: image6, - category: "Research", - tag: "Free", - badgeColor: "#22c55e", - isFeatured: false, - gradient: "linear-gradient(135deg, #22c55e30, #9d4edd20)" - }, - { - id: 7, - title: "DeFi Liquidity Dynamics 2026", - excerpt: "Comprehensive analysis of DeFi liquidity patterns across Ethereum, Solana, and emerging L2 ecosystems. TVL concentration and yield opportunities.", - date: "Jan 4, 2026", - readTime: "13 min read", - image: image7, - category: "Research", - tag: "Premium", - badgeColor: "#9d4edd", - isFeatured: false, - gradient: "linear-gradient(135deg, #9d4edd30, #f59e0b20)" - }, - { - id: 8, - title: "Institutional Adoption Tracker", - excerpt: "Monthly update on institutional Bitcoin and Ethereum investments: ETF flows, corporate treasuries, and regulated product growth.", - date: "Jan 3, 2026", - readTime: "9 min read", - image: image8, - category: "Market Intelligence", - tag: "Free", - badgeColor: "#f59e0b", - isFeatured: false, - gradient: "linear-gradient(135deg, #f59e0b30, #4559DC20)" - }, - { - id: 9, - title: "NFT Market Analysis: 2025 Review", - excerpt: "Year-end review of NFT market dynamics: trading volumes, collection performance, and the rise of utility-based NFTs.", - date: "Jan 2, 2026", - readTime: "10 min read", - image: image9, - category: "Research", - tag: "Free", - badgeColor: "#4559DC", - isFeatured: false, - gradient: "linear-gradient(135deg, #4559DC30, #22c55e20)" - }, - { - id: 10, - title: "Stablecoin Supply Analysis", - excerpt: "Tracking stablecoin supply changes as a proxy for liquidity conditions and capital rotation within crypto markets.", - date: "Jan 1, 2026", - readTime: "7 min read", - image: image10, - category: "Market Intelligence", - tag: "Free", - badgeColor: "#22c55e", - isFeatured: false, - gradient: "linear-gradient(135deg, #22c55e30, #9d4edd20)" - }, - { - id: 11, - title: "Bitcoin Macro Indicators", - excerpt: "Combining on-chain data with traditional macro indicators to forecast Bitcoin's performance in different economic regimes.", - date: "Dec 31, 2025", - readTime: "16 min read", - image: image11, - category: "Research", - tag: "Premium", - badgeColor: "#9d4edd", - isFeatured: false, - gradient: "linear-gradient(135deg, #9d4edd30, #f59e0b20)" - }, - { - id: 12, - title: "Lightning Network Growth Report", - excerpt: "Analysis of Bitcoin Lightning Network adoption: capacity growth, channel dynamics, and real-world payment usage.", - date: "Dec 30, 2025", - readTime: "12 min read", - image: image12, - category: "Research", - tag: "Free", - badgeColor: "#f59e0b", - isFeatured: false, - gradient: "linear-gradient(135deg, #f59e0b30, #4559DC20)" - }, - { - id: 13, - title: "Crypto Derivatives Landscape", - excerpt: "Comprehensive overview of crypto derivatives markets: futures, options, and perpetual swaps across major exchanges.", - date: "Dec 29, 2025", - readTime: "14 min read", - image: image13, - category: "Market Intelligence", - tag: "Premium", - badgeColor: "#4559DC", - isFeatured: false, - gradient: "linear-gradient(135deg, #4559DC30, #22c55e20)" - }, - { - id: 14, - title: "Ethereum Staking Economics", - excerpt: "Deep dive into Ethereum staking yields, validator economics, and the impact of restaking protocols on network security.", - date: "Dec 28, 2025", - readTime: "13 min read", - image: image14, - category: "Research", - tag: "Premium", - badgeColor: "#22c55e", - isFeatured: false, - gradient: "linear-gradient(135deg, #22c55e30, #9d4edd20)" - }, - { - id: 15, - title: "Cross-Chain Bridge Security", - excerpt: "Analysis of security practices and vulnerabilities in major cross-chain bridges following recent exploit incidents.", - date: "Dec 27, 2025", - readTime: "11 min read", - image: image15, - category: "Research", - tag: "Free", - badgeColor: "#9d4edd", - isFeatured: false, - gradient: "linear-gradient(135deg, #9d4edd30, #f59e0b20)" - }, - { - id: 16, - title: "Bitcoin Halving Impact Study", - excerpt: "Historical analysis of previous Bitcoin halvings and data-driven projections for the 2024 halving's market impact.", - date: "Dec 26, 2025", - readTime: "15 min read", - image: image16, - category: "Research", - tag: "Premium", - badgeColor: "#f59e0b", - isFeatured: false, - gradient: "linear-gradient(135deg, #f59e0b30, #4559DC20)" - }, - { - id: 17, - title: "Regulatory Developments Tracker", - excerpt: "Monthly update on global crypto regulatory developments and their potential market implications.", - date: "Dec 25, 2025", - readTime: "8 min read", - image: image17, - category: "Market Intelligence", - tag: "Free", - badgeColor: "#4559DC", - isFeatured: false, - gradient: "linear-gradient(135deg, #4559DC30, #22c55e20)" - }, - { - id: 18, - title: "Smart Contract Audit Trends", - excerpt: "Analysis of smart contract security audit findings and emerging best practices in Web3 development.", - date: "Dec 24, 2025", - readTime: "12 min read", - image: image18, - category: "Research", - tag: "Premium", - badgeColor: "#22c55e", - isFeatured: false, - gradient: "linear-gradient(135deg, #22c55e30, #9d4edd20)" - }, - { - id: 19, - title: "Crypto Venture Capital Report", - excerpt: "Q4 2025 analysis of venture capital flows into crypto and blockchain startups across different verticals.", - date: "Dec 23, 2025", - readTime: "10 min read", - image: image19, - category: "Market Intelligence", - tag: "Free", - badgeColor: "#9d4edd", - isFeatured: false, - gradient: "linear-gradient(135deg, #9d4edd30, #f59e0b20)" - }, - { - id: 20, - title: "MEV (Miner Extractable Value) Research", - excerpt: "Comprehensive study of MEV in Ethereum and other PoS networks: detection, quantification, and mitigation strategies.", - date: "Dec 22, 2025", - readTime: "16 min read", - image: image20, - category: "Research", - tag: "Premium", - badgeColor: "#f59e0b", - isFeatured: false, - gradient: "linear-gradient(135deg, #f59e0b30, #4559DC20)" - }, - { - id: 21, - title: "Bitcoin Adoption Metrics", - excerpt: "Tracking Bitcoin adoption through on-chain metrics: active addresses, new entities, and transaction patterns.", - date: "Dec 21, 2025", - readTime: "9 min read", - image: image21, - category: "Market Intelligence", - tag: "Free", - badgeColor: "#4559DC", - isFeatured: false, - gradient: "linear-gradient(135deg, #4559DC30, #22c55e20)" - }, - { - id: 22, - title: "Zero-Knowledge Proof Applications", - excerpt: "Exploring practical applications of ZK-proofs in blockchain scalability, privacy, and interoperability solutions.", - date: "Dec 20, 2025", - readTime: "14 min read", - image: image22, - category: "Research", - tag: "Premium", - badgeColor: "#22c55e", - isFeatured: false, - gradient: "linear-gradient(135deg, #22c55e30, #9d4edd20)" - }, - { - id: 23, - title: "Crypto Market Correlation Study", - excerpt: "Analysis of correlation patterns between crypto assets and traditional financial markets under different regimes.", - date: "Dec 19, 2025", - readTime: "11 min read", - image: image23, - category: "Research", - tag: "Free", - badgeColor: "#9d4edd", - isFeatured: false, - gradient: "linear-gradient(135deg, #9d4edd30, #f59e0b20)" - }, - { - id: 24, - title: "DAO Governance Analysis", - excerpt: "Study of DAO governance patterns: voter participation, proposal success rates, and treasury management practices.", - date: "Dec 18, 2025", - readTime: "13 min read", - image: image24, - category: "Research", - tag: "Premium", - badgeColor: "#f59e0b", - isFeatured: false, - gradient: "linear-gradient(135deg, #f59e0b30, #4559DC20)" - }, - { - id: 25, - title: "Crypto Tax Reporting Guide", - excerpt: "Comprehensive guide to crypto tax reporting requirements across major jurisdictions for 2025 tax year.", - date: "Dec 17, 2025", - readTime: "10 min read", - image: image25, - category: "Partner Reports", - tag: "Free", - badgeColor: "#4559DC", - isFeatured: false, - gradient: "linear-gradient(135deg, #4559DC30, #22c55e20)" - }, - { - id: 26, - title: "Bitcoin Technical Analysis", - excerpt: "Combining on-chain data with technical analysis to identify key support and resistance levels for Bitcoin.", - date: "Dec 16, 2025", - readTime: "8 min read", - image: image26, - category: "Market Vectors", - tag: "Premium", - badgeColor: "#22c55e", - isFeatured: false, - gradient: "linear-gradient(135deg, #22c55e30, #9d4edd20)" - }, - { - id: 27, - title: "Web3 Social Media Trends", - excerpt: "Analysis of emerging Web3 social media platforms and their token economic models compared to traditional social media.", - date: "Dec 15, 2025", - readTime: "12 min read", - image: image27, - category: "Research", - tag: "Free", - badgeColor: "#9d4edd", - isFeatured: false, - gradient: "linear-gradient(135deg, #9d4edd30, #f59e0b20)" - }, - { - id: 28, - title: "Crypto Insurance Market", - excerpt: "Overview of the growing crypto insurance market: coverage options, premium trends, and risk assessment methodologies.", - date: "Dec 14, 2025", - readTime: "11 min read", - image: image27, - category: "Market Intelligence", - tag: "Premium", - badgeColor: "#f59e0b", - isFeatured: false, - gradient: "linear-gradient(135deg, #f59e0b30, #4559DC20)" - }, - { - id: 29, - title: "Bitcoin Layer 2 Solutions", - excerpt: "Comparative analysis of Bitcoin Layer 2 scaling solutions: Lightning Network, Stacks, Rootstock, and emerging protocols.", - date: "Dec 13, 2025", - readTime: "14 min read", - image: image29, - category: "Research", - tag: "Free", - badgeColor: "#4559DC", - isFeatured: false, - gradient: "linear-gradient(135deg, #4559DC30, #22c55e20)" - }, - { - id: 30, - title: "Crypto Market Forecast 2026", - excerpt: "Data-driven forecast for crypto markets in 2026 based on historical patterns, on-chain indicators, and macro trends.", - date: "Dec 12, 2025", - readTime: "15 min read", - image: image30, - category: "Research", - tag: "Premium", - badgeColor: "#22c55e", - isFeatured: false, - gradient: "linear-gradient(135deg, #22c55e30, #9d4edd20)" - } - ]; -}; - -export default function Blog() { - const navigate = useNavigate(); - const blogPosts = generateBlogPosts(); - const featuredPosts = blogPosts.filter(post => post.isFeatured); - const regularPosts = blogPosts.filter(post => !post.isFeatured); - - return ( -
-
- - {/* Hero Section - Glassnode Inspired */} -
- -
- - - - - On-Chain Market Intelligence -
-

- Professional-Grade Insights -

-

- Your portal to contextualised market analysis, and cutting edge research - for Bitcoin, Ethereum, DeFi and more. Access premium on-chain data and institutional-grade analysis. -

-
-
- - {/* Featured Reports */} -
-

- - - - Featured Reports -

- -
- {featuredPosts.map((post, index) => ( - navigate(`/blog/${post.id}`, { state: { post } })} - > -
-
- {post.title} - -
- {post.tag} -
-
- -
-
- {post.category} -
- {post.tag === "Premium" ? ( -
- - - - - Premium -
- ) : ( -
- - - - - Free -
- )} -
-
- -

{post.title}

-

{post.excerpt}

- -
-
- {post.date} - - {post.readTime} -
- - -
-
-
- ))} -
-
- - {/* All Posts Grid */} -
-
-

Latest Research & Analysis

-

30 comprehensive reports and insights

-
- -
- {regularPosts.map((post, index) => ( - navigate(`/blog/${post.id}`, { state: { post } })} - > -
- {post.title} -
- {post.tag} -
-
- -
-
- {post.category} - {post.date} -
- -

{post.title}

-

{post.excerpt}

- -
- {post.readTime} -
- Read Article - - - -
-
-
-
- ))} -
- - {/* Load More Button */} -
- - Load More Insights - - - - -
-
-
-
- ); -} \ No newline at end of file diff --git a/src/components/Common/ErrorBoundary.css b/src/components/Common/ErrorBoundary.css new file mode 100644 index 0000000..8f53301 --- /dev/null +++ b/src/components/Common/ErrorBoundary.css @@ -0,0 +1,172 @@ +.error-boundary { + min-height: 100vh; + display: flex; + align-items: center; + justify-content: center; + background: linear-gradient(135deg, #0a0a0a 0%, #1a1a2e 100%); + padding: 2rem; +} + +.error-boundary-card { + max-width: 600px; + width: 100%; + background: rgba(15, 15, 25, 0.95); + border: 1px solid rgba(139, 92, 246, 0.3); + border-radius: 16px; + padding: 3rem 2rem; + text-align: center; + backdrop-filter: blur(12px); + box-shadow: 0 8px 32px rgba(139, 92, 246, 0.1); +} + +.error-boundary-icon { + font-size: 4rem; + margin-bottom: 1.5rem; + animation: pulse 2s ease-in-out infinite; +} + +@keyframes pulse { + 0%, + 100% { + transform: scale(1); + opacity: 1; + } + 50% { + transform: scale(1.1); + opacity: 0.8; + } +} + +.error-boundary-title { + font-size: 2rem; + font-weight: 700; + color: #ffffff; + margin-bottom: 1rem; + background: linear-gradient(135deg, #8b5cf6 0%, #ec4899 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +.error-boundary-message { + font-size: 1.1rem; + color: rgba(255, 255, 255, 0.7); + margin-bottom: 2rem; + line-height: 1.6; +} + +.error-boundary-details { + margin: 2rem 0; + text-align: left; + background: rgba(0, 0, 0, 0.3); + border: 1px solid rgba(139, 92, 246, 0.2); + border-radius: 8px; + padding: 1rem; +} + +.error-boundary-details summary { + cursor: pointer; + color: #8b5cf6; + font-weight: 600; + margin-bottom: 0.5rem; + user-select: none; +} + +.error-boundary-details summary:hover { + color: #a78bfa; +} + +.error-boundary-stack { + font-family: "Courier New", monospace; + font-size: 0.85rem; + color: rgba(255, 255, 255, 0.8); + overflow-x: auto; + white-space: pre-wrap; + word-break: break-word; + margin-top: 0.5rem; +} + +.error-boundary-actions { + display: flex; + gap: 1rem; + justify-content: center; + flex-wrap: wrap; +} + +.error-boundary-btn { + padding: 0.75rem 1.5rem; + border-radius: 8px; + font-size: 1rem; + font-weight: 600; + cursor: pointer; + transition: all 0.3s ease; + text-decoration: none; + display: inline-block; + border: none; + font-family: inherit; +} + +.error-boundary-btn-primary { + background: linear-gradient(135deg, #8b5cf6 0%, #7c3aed 100%); + color: #ffffff; + box-shadow: 0 4px 15px rgba(139, 92, 246, 0.3); +} + +.error-boundary-btn-primary:hover { + background: linear-gradient(135deg, #7c3aed 0%, #6d28d9 100%); + box-shadow: 0 6px 20px rgba(139, 92, 246, 0.4); + transform: translateY(-2px); +} + +.error-boundary-btn-secondary { + background: rgba(139, 92, 246, 0.1); + color: #8b5cf6; + border: 1px solid rgba(139, 92, 246, 0.3); +} + +.error-boundary-btn-secondary:hover { + background: rgba(139, 92, 246, 0.2); + border-color: rgba(139, 92, 246, 0.5); + transform: translateY(-2px); +} + +.error-boundary-btn-link { + background: transparent; + color: rgba(255, 255, 255, 0.7); + border: 1px solid rgba(255, 255, 255, 0.2); +} + +.error-boundary-btn-link:hover { + color: #ffffff; + border-color: rgba(255, 255, 255, 0.4); + background: rgba(255, 255, 255, 0.05); + transform: translateY(-2px); +} + +/* Responsive Design */ +@media (max-width: 768px) { + .error-boundary { + padding: 1rem; + } + + .error-boundary-card { + padding: 2rem 1.5rem; + } + + .error-boundary-title { + font-size: 1.5rem; + } + + .error-boundary-message { + font-size: 1rem; + } + + .error-boundary-actions { + flex-direction: column; + gap: 0.75rem; + } + + .error-boundary-btn { + width: 100%; + } +} diff --git a/src/components/Common/ErrorBoundary.jsx b/src/components/Common/ErrorBoundary.jsx new file mode 100644 index 0000000..2bab50b --- /dev/null +++ b/src/components/Common/ErrorBoundary.jsx @@ -0,0 +1,93 @@ +import React from "react"; +import "./ErrorBoundary.css"; + +class ErrorBoundary extends React.Component { + constructor(props) { + super(props); + this.state = { + hasError: false, + error: null, + errorInfo: null, + }; + } + + static getDerivedStateFromError(error) { + // Update state so the next render will show the fallback UI + return { hasError: true }; + } + + componentDidCatch(error, errorInfo) { + // Log error details for debugging + console.error("ErrorBoundary caught an error:", error, errorInfo); + this.setState({ + error, + errorInfo, + }); + } + + handleRetry = () => { + // Reset error state and attempt to reload + this.setState({ + hasError: false, + error: null, + errorInfo: null, + }); + }; + + handleReload = () => { + // Full page reload as last resort + window.location.reload(); + }; + + render() { + if (this.state.hasError) { + return ( +
+
+
⚠️
+

Oops! Something went wrong

+

+ {this.props.fallbackMessage || + "We encountered an error while loading this page. Please try again."} +

+ + {process.env.NODE_ENV === "development" && this.state.error && ( +
+ Error Details (Development Only) +
+                  {this.state.error.toString()}
+                  {this.state.errorInfo?.componentStack}
+                
+
+ )} + +
+ + + + Go Home + +
+
+
+ ); + } + + return this.props.children; + } +} + +export default ErrorBoundary; diff --git a/src/components/Common/FirebaseError.css b/src/components/Common/FirebaseError.css new file mode 100644 index 0000000..7d8b0b5 --- /dev/null +++ b/src/components/Common/FirebaseError.css @@ -0,0 +1,429 @@ +/* Firebase Error Component - Cosmic Theme Integration */ + +.firebase-error-container { + position: relative; + width: 100%; + padding: 1rem; + animation: firebaseErrorSlideIn 0.3s ease-out; +} + +@keyframes firebaseErrorSlideIn { + from { + opacity: 0; + transform: translateY(-20px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.firebase-error-card { + max-width: 600px; + margin: 0 auto; + background: rgba(15, 15, 25, 0.95); + border-radius: 16px; + padding: 1.75rem; + backdrop-filter: blur(12px); + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3); + border: 1px solid rgba(139, 92, 246, 0.3); +} + +/* Error Severity Variants */ +.firebase-error-error .firebase-error-card { + border-color: rgba(239, 68, 68, 0.4); + box-shadow: 0 8px 32px rgba(239, 68, 68, 0.15); +} + +.firebase-error-warning .firebase-error-card { + border-color: rgba(251, 191, 36, 0.4); + box-shadow: 0 8px 32px rgba(251, 191, 36, 0.15); +} + +.firebase-error-info .firebase-error-card { + border-color: rgba(59, 130, 246, 0.4); + box-shadow: 0 8px 32px rgba(59, 130, 246, 0.15); +} + +/* Header */ +.firebase-error-header { + display: flex; + align-items: flex-start; + gap: 1rem; + margin-bottom: 1.25rem; +} + +.firebase-error-icon { + flex-shrink: 0; + width: 48px; + height: 48px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 12px; + background: rgba(139, 92, 246, 0.1); +} + +.firebase-error-error .firebase-error-icon { + background: rgba(239, 68, 68, 0.15); +} + +.firebase-error-warning .firebase-error-icon { + background: rgba(251, 191, 36, 0.15); +} + +.firebase-error-info .firebase-error-icon { + background: rgba(59, 130, 246, 0.15); +} + +.firebase-error-icon-symbol { + font-size: 1.75rem; + animation: firebaseIconPulse 2s ease-in-out infinite; +} + +@keyframes firebaseIconPulse { + 0%, 100% { + transform: scale(1); + opacity: 1; + } + 50% { + transform: scale(1.05); + opacity: 0.9; + } +} + +.firebase-error-header-content { + flex: 1; +} + +.firebase-error-title { + font-size: 1.25rem; + font-weight: 700; + color: #ffffff; + margin: 0 0 0.25rem 0; + background: linear-gradient(135deg, #8b5cf6 0%, #ec4899 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +.firebase-error-error .firebase-error-title { + background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +.firebase-error-warning .firebase-error-title { + background: linear-gradient(135deg, #fbbf24 0%, #f59e0b 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +.firebase-error-info .firebase-error-title { + background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +.firebase-error-context { + font-size: 0.875rem; + color: rgba(255, 255, 255, 0.6); + font-style: italic; +} + +.firebase-error-dismiss { + flex-shrink: 0; + width: 32px; + height: 32px; + border-radius: 8px; + background: rgba(255, 255, 255, 0.05); + border: 1px solid rgba(255, 255, 255, 0.1); + color: rgba(255, 255, 255, 0.7); + font-size: 1.25rem; + cursor: pointer; + transition: all 0.2s ease; + display: flex; + align-items: center; + justify-content: center; + font-family: inherit; +} + +.firebase-error-dismiss:hover { + background: rgba(255, 255, 255, 0.1); + border-color: rgba(255, 255, 255, 0.2); + color: #ffffff; + transform: scale(1.05); +} + +/* Body */ +.firebase-error-body { + margin-bottom: 1.25rem; +} + +.firebase-error-message { + font-size: 1rem; + color: rgba(255, 255, 255, 0.85); + line-height: 1.6; + margin: 0 0 1rem 0; +} + +.firebase-error-user-action { + background: rgba(139, 92, 246, 0.08); + border-left: 3px solid rgba(139, 92, 246, 0.5); + padding: 0.75rem 1rem; + border-radius: 6px; + margin-top: 1rem; +} + +.firebase-error-error .firebase-error-user-action { + background: rgba(239, 68, 68, 0.08); + border-left-color: rgba(239, 68, 68, 0.5); +} + +.firebase-error-warning .firebase-error-user-action { + background: rgba(251, 191, 36, 0.08); + border-left-color: rgba(251, 191, 36, 0.5); +} + +.firebase-error-info .firebase-error-user-action { + background: rgba(59, 130, 246, 0.08); + border-left-color: rgba(59, 130, 246, 0.5); +} + +.firebase-error-user-action strong { + display: block; + color: #8b5cf6; + font-size: 0.875rem; + font-weight: 600; + margin-bottom: 0.25rem; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.firebase-error-error .firebase-error-user-action strong { + color: #ef4444; +} + +.firebase-error-warning .firebase-error-user-action strong { + color: #fbbf24; +} + +.firebase-error-info .firebase-error-user-action strong { + color: #3b82f6; +} + +.firebase-error-user-action p { + margin: 0; + font-size: 0.9375rem; + color: rgba(255, 255, 255, 0.8); + line-height: 1.5; +} + +/* Developer Details */ +.firebase-error-developer-details { + background: rgba(0, 0, 0, 0.3); + border: 1px solid rgba(139, 92, 246, 0.2); + border-radius: 8px; + padding: 1rem; + margin-top: 1rem; +} + +.firebase-error-developer-summary { + cursor: pointer; + color: #8b5cf6; + font-weight: 600; + font-size: 0.9375rem; + margin-bottom: 0.75rem; + user-select: none; + list-style: none; + display: flex; + align-items: center; + gap: 0.5rem; +} + +.firebase-error-developer-summary:hover { + color: #a78bfa; +} + +.firebase-error-developer-summary::before { + content: "▶"; + display: inline-block; + transition: transform 0.2s ease; +} + +.firebase-error-developer-details[open] .firebase-error-developer-summary::before { + transform: rotate(90deg); +} + +.firebase-error-developer-content { + margin-top: 0.75rem; + padding-top: 0.75rem; + border-top: 1px solid rgba(139, 92, 246, 0.15); +} + +.firebase-error-developer-action, +.firebase-error-code, +.firebase-error-original { + margin-bottom: 0.75rem; +} + +.firebase-error-developer-action strong, +.firebase-error-code strong, +.firebase-error-original strong { + display: block; + color: rgba(255, 255, 255, 0.9); + font-size: 0.875rem; + margin-bottom: 0.25rem; +} + +.firebase-error-developer-action p { + margin: 0; + color: rgba(255, 255, 255, 0.7); + font-size: 0.875rem; + line-height: 1.5; +} + +.firebase-error-code code { + background: rgba(139, 92, 246, 0.15); + color: #a78bfa; + padding: 0.25rem 0.5rem; + border-radius: 4px; + font-family: "JetBrains Mono", "Courier New", monospace; + font-size: 0.875rem; +} + +.firebase-error-stack { + font-family: "JetBrains Mono", "Courier New", monospace; + font-size: 0.8125rem; + color: rgba(255, 255, 255, 0.7); + background: rgba(0, 0, 0, 0.4); + padding: 0.75rem; + border-radius: 6px; + overflow-x: auto; + white-space: pre-wrap; + word-break: break-word; + margin: 0.5rem 0 0 0; + line-height: 1.4; +} + +/* Action Buttons */ +.firebase-error-actions { + display: flex; + gap: 0.75rem; + flex-wrap: wrap; + margin-top: 1.5rem; +} + +.firebase-error-btn { + padding: 0.75rem 1.5rem; + border-radius: 10px; + font-size: 0.9375rem; + font-weight: 600; + cursor: pointer; + transition: all 0.3s ease; + border: none; + font-family: inherit; + display: flex; + align-items: center; + gap: 0.5rem; +} + +.firebase-error-btn-icon { + font-size: 1.125rem; +} + +.firebase-error-btn-primary { + background: linear-gradient(135deg, #8b5cf6 0%, #7c3aed 100%); + color: #ffffff; + box-shadow: 0 4px 15px rgba(139, 92, 246, 0.3); +} + +.firebase-error-btn-primary:hover { + background: linear-gradient(135deg, #7c3aed 0%, #6d28d9 100%); + box-shadow: 0 6px 20px rgba(139, 92, 246, 0.4); + transform: translateY(-2px); +} + +.firebase-error-btn-primary:active { + transform: translateY(0); +} + +.firebase-error-btn-secondary { + background: rgba(139, 92, 246, 0.1); + color: #8b5cf6; + border: 1px solid rgba(139, 92, 246, 0.3); +} + +.firebase-error-btn-secondary:hover { + background: rgba(139, 92, 246, 0.2); + border-color: rgba(139, 92, 246, 0.5); + transform: translateY(-2px); +} + +.firebase-error-btn-secondary:active { + transform: translateY(0); +} + +/* Responsive Design */ +@media (max-width: 768px) { + .firebase-error-container { + padding: 0.75rem; + } + + .firebase-error-card { + padding: 1.25rem; + } + + .firebase-error-header { + gap: 0.75rem; + } + + .firebase-error-icon { + width: 40px; + height: 40px; + } + + .firebase-error-icon-symbol { + font-size: 1.5rem; + } + + .firebase-error-title { + font-size: 1.125rem; + } + + .firebase-error-message { + font-size: 0.9375rem; + } + + .firebase-error-actions { + flex-direction: column; + } + + .firebase-error-btn { + width: 100%; + justify-content: center; + } +} + +/* Accessibility */ +@media (prefers-reduced-motion: reduce) { + .firebase-error-container, + .firebase-error-icon-symbol, + .firebase-error-btn { + animation: none; + } +} + +/* High Contrast Mode */ +@media (prefers-contrast: high) { + .firebase-error-card { + border-width: 2px; + } + + .firebase-error-btn { + border-width: 2px; + } +} diff --git a/src/components/Common/FirebaseError.jsx b/src/components/Common/FirebaseError.jsx new file mode 100644 index 0000000..99f2d21 --- /dev/null +++ b/src/components/Common/FirebaseError.jsx @@ -0,0 +1,155 @@ +import React from "react"; +import PropTypes from "prop-types"; +import "./FirebaseError.css"; + +/** + * FirebaseError Component + * + * Displays user-friendly Firebase error messages with retry functionality. + * Integrates with the validation utility to show contextual error information. + */ +const FirebaseError = ({ + errorInfo, + onRetry, + onDismiss, + severity = "error", + showDeveloperInfo = false, +}) => { + if (!errorInfo) return null; + + const { title, message, userAction, developerAction, originalError, code, context } = errorInfo; + + // Determine icon and styling based on severity + const getSeverityConfig = () => { + switch (severity) { + case "warning": + return { + icon: "⚠️", + className: "firebase-error-warning", + label: "Warning", + }; + case "info": + return { + icon: "ℹ️", + className: "firebase-error-info", + label: "Information", + }; + case "error": + default: + return { + icon: "❌", + className: "firebase-error-error", + label: "Error", + }; + } + }; + + const severityConfig = getSeverityConfig(); + + return ( +
+
+ {/* Header */} +
+
+ {severityConfig.icon} +
+
+

{title || "Firebase Error"}

+ {context && ( + During: {context} + )} +
+ {onDismiss && ( + + )} +
+ + {/* Main Message */} +
+

{message}

+ + {userAction && ( +
+ What you can do: +

{userAction}

+
+ )} +
+ + {/* Developer Information (shown in development mode) */} + {showDeveloperInfo && (developerAction || originalError || code) && ( +
+ + Developer Information + +
+ {developerAction && ( +
+ Fix: +

{developerAction}

+
+ )} + {code && ( +
+ Error Code: {code} +
+ )} + {originalError && ( +
+ Original Error: +
{originalError}
+
+ )} +
+
+ )} + + {/* Action Buttons */} +
+ {onRetry && ( + + )} + {onDismiss && ( + + )} +
+
+
+ ); +}; + +FirebaseError.propTypes = { + errorInfo: PropTypes.shape({ + title: PropTypes.string, + message: PropTypes.string.isRequired, + userAction: PropTypes.string, + developerAction: PropTypes.string, + originalError: PropTypes.string, + code: PropTypes.string, + context: PropTypes.string, + }), + onRetry: PropTypes.func, + onDismiss: PropTypes.func, + severity: PropTypes.oneOf(["error", "warning", "info"]), + showDeveloperInfo: PropTypes.bool, +}; + +export default FirebaseError; diff --git a/src/components/LoadingSpinner.css b/src/components/Common/LoadingSpinner.css similarity index 93% rename from src/components/LoadingSpinner.css rename to src/components/Common/LoadingSpinner.css index bc18a72..a3570ce 100644 --- a/src/components/LoadingSpinner.css +++ b/src/components/Common/LoadingSpinner.css @@ -1,98 +1,100 @@ -/* Centered small spinner */ -.center-spinner { - position: fixed; - top: 0; - left: 0; - right: 0; - bottom: 0; - display: flex; - justify-content: center; - align-items: center; - z-index: 9999; - background: transparent; /* No background overlay */ -} - -.spinner-card { - background: rgba(31, 31, 31, 0.95); - padding: 30px 40px; - border-radius: 16px; - box-shadow: 0 15px 40px rgba(0, 0, 0, 0.3), - 0 0 0 1px rgba(127, 92, 255, 0.3); - border: 1px solid rgba(127, 92, 255, 0.4); - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - min-width: 200px; - animation: slideUp 0.3s ease-out; -} - -@keyframes slideUp { - from { - opacity: 0; - transform: translateY(20px); - } - to { - opacity: 1; - transform: translateY(0); - } -} - -.spinner-icon { - width: 45px; - height: 45px; - border: 3px solid rgba(127, 92, 255, 0.2); - border-top: 3px solid #7f5cff; - border-radius: 50%; - animation: spin 1.2s ease-in-out infinite; - margin-bottom: 15px; -} - -.spinner-message { - color: #fff; - font-size: 0.95rem; - font-weight: 500; - text-align: center; - background: linear-gradient(90deg, #7f5cff, #00d4ff); - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - background-clip: text; -} - -.loading-dots { - display: inline-flex; - gap: 4px; - margin-top: 8px; -} - -.loading-dots span { - width: 6px; - height: 6px; - border-radius: 50%; - background: #7f5cff; - animation: bounce 1.4s ease-in-out infinite; -} - -.loading-dots span:nth-child(2) { - animation-delay: 0.2s; - background: #00d4ff; -} - -.loading-dots span:nth-child(3) { - animation-delay: 0.4s; - background: #7f5cff; -} - -@keyframes spin { - 0% { transform: rotate(0deg); } - 100% { transform: rotate(360deg); } -} - -@keyframes bounce { - 0%, 80%, 100% { - transform: scale(0); - } - 40% { - transform: scale(1); - } +/* Centered small spinner */ +.center-spinner { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + display: flex; + justify-content: center; + align-items: center; + z-index: 9999; + background: transparent; /* No background overlay */ + pointer-events: none; /* Allow clicks through the overlay */ +} + +.spinner-card { + background: rgba(31, 31, 31, 0.95); + padding: 30px 40px; + border-radius: 16px; + box-shadow: 0 15px 40px rgba(0, 0, 0, 0.3), + 0 0 0 1px rgba(127, 92, 255, 0.3); + border: 1px solid rgba(127, 92, 255, 0.4); + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + min-width: 200px; + animation: slideUp 0.3s ease-out; + pointer-events: auto; /* Spinner card itself can be clicked */ +} + +@keyframes slideUp { + from { + opacity: 0; + transform: translateY(20px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.spinner-icon { + width: 45px; + height: 45px; + border: 3px solid rgba(127, 92, 255, 0.2); + border-top: 3px solid #7f5cff; + border-radius: 50%; + animation: spin 1.2s ease-in-out infinite; + margin-bottom: 15px; +} + +.spinner-message { + color: #fff; + font-size: 0.95rem; + font-weight: 500; + text-align: center; + background: linear-gradient(90deg, #7f5cff, #00d4ff); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +.loading-dots { + display: inline-flex; + gap: 4px; + margin-top: 8px; +} + +.loading-dots span { + width: 6px; + height: 6px; + border-radius: 50%; + background: #7f5cff; + animation: bounce 1.4s ease-in-out infinite; +} + +.loading-dots span:nth-child(2) { + animation-delay: 0.2s; + background: #00d4ff; +} + +.loading-dots span:nth-child(3) { + animation-delay: 0.4s; + background: #7f5cff; +} + +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} + +@keyframes bounce { + 0%, 80%, 100% { + transform: scale(0); + } + 40% { + transform: scale(1); + } } \ No newline at end of file diff --git a/src/components/LoadingSpinner.jsx b/src/components/Common/LoadingSpinner.jsx similarity index 96% rename from src/components/LoadingSpinner.jsx rename to src/components/Common/LoadingSpinner.jsx index e8cae2f..6f1f91a 100644 --- a/src/components/LoadingSpinner.jsx +++ b/src/components/Common/LoadingSpinner.jsx @@ -1,19 +1,19 @@ -import "./LoadingSpinner.css"; - -const LoadingSpinner = ({ message = "Loading cryptocurrency data" }) => { - return ( -
-
-
-

{message}

-
- - - -
-
-
- ); -}; - +import "./LoadingSpinner.css"; + +const LoadingSpinner = ({ message = "Loading cryptocurrency data" }) => { + return ( +
+
+
+

{message}

+
+ + + +
+
+
+ ); +}; + export default LoadingSpinner; \ No newline at end of file diff --git a/src/components/Common/PageNotFound.css b/src/components/Common/PageNotFound.css new file mode 100644 index 0000000..21f5143 --- /dev/null +++ b/src/components/Common/PageNotFound.css @@ -0,0 +1,37 @@ +.container{ + text-align: center; + height:70vh !important; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; +} +.container h1{ + font-size: 3.5rem; + margin-bottom: 1rem; +} +.container h1:hover{ + color: #007bff; + transition: color 0.3s ease; +} +.container h1::after{ + content: ''; + display: block; + width: 50px; + height: 4px; + background-color: #007bff; + margin: 0.5rem auto 0; + border-radius: 2px; +} +.container h1::after:hover{ + background-color: #0056b3; + transition: background-color 0.3s ease; +} + +.container p{ + font-size: 1.5rem; + margin-bottom: 2rem; +} +.back-home{ + text-decoration: none !important; +} \ No newline at end of file diff --git a/src/components/Common/PageNotFound.jsx b/src/components/Common/PageNotFound.jsx new file mode 100644 index 0000000..11e01da --- /dev/null +++ b/src/components/Common/PageNotFound.jsx @@ -0,0 +1,15 @@ +import React from 'react' +import { Link } from 'react-router-dom' +import "./PageNotFound.css" + +const PageNotFound = () => { + return ( +
+

404

+

Oops! Page Not Found

+ Go back to Home +
+ ) +} + +export default PageNotFound \ No newline at end of file diff --git a/src/components/Common/RateLimitIndicator.jsx b/src/components/Common/RateLimitIndicator.jsx new file mode 100644 index 0000000..022bc6a --- /dev/null +++ b/src/components/Common/RateLimitIndicator.jsx @@ -0,0 +1,157 @@ +import { useContext, useState, useEffect, useRef } from "react"; +import { CoinContext } from "@/context/CoinContextInstance"; +import { coinGeckoRateLimiter } from "@/utils/rateLimiter"; + +/** + * RateLimitIndicator + * + * A non-intrusive status badge that appears in the bottom-left corner + * whenever CoinGecko API requests are being rate-limited or queued. + * + * - Reads `isRateLimited` from CoinContext (set by apiClient when a 429 occurs) + * - Polls the rateLimiter singleton every 500ms for live queue depth + * - Auto-hides 2 seconds after both flags clear + * - Uses CSS transitions for a smooth fade-in / fade-out + */ +const RateLimitIndicator = () => { + const { isRateLimited } = useContext(CoinContext); + + // Live queue metrics — polled on a short interval + const [queueDepth, setQueueDepth] = useState(0); + const [activeRequests, setActiveRequests] = useState(0); + + // Controls actual DOM visibility (delayed hide allows CSS fade-out) + const [mounted, setMounted] = useState(false); + // Controls CSS opacity/transform transition + const [show, setShow] = useState(false); + + const hideTimerRef = useRef(null); + + // ─── Poll queue depth ──────────────────────────────────────────── + useEffect(() => { + const interval = setInterval(() => { + const metrics = coinGeckoRateLimiter.getMetrics(); + setQueueDepth(metrics.queueDepth); + setActiveRequests(metrics.activeRequests); + }, 500); + + return () => clearInterval(interval); + }, []); + + // ─── Show / hide logic ─────────────────────────────────────────── + const isActive = isRateLimited || queueDepth > 0 || activeRequests > 1; + + useEffect(() => { + clearTimeout(hideTimerRef.current); + + if (isActive) { + // Mount first, then trigger the CSS transition on the next frame + setMounted(true); + requestAnimationFrame(() => setShow(true)); + } else { + // Start fade-out, then unmount after transition completes + setShow(false); + hideTimerRef.current = setTimeout(() => setMounted(false), 600); + } + + return () => clearTimeout(hideTimerRef.current); + }, [isActive]); + + if (!mounted) return null; + + // ─── Status label ──────────────────────────────────────────────── + const getStatusText = () => { + if (isRateLimited) return "Rate limited — retrying…"; + if (queueDepth > 0) return `${queueDepth} request${queueDepth > 1 ? "s" : ""} queued`; + return "Fetching data…"; + }; + + const getStatusColor = () => { + if (isRateLimited) return "#f59e0b"; // amber — warning + return "#6366f1"; // indigo — normal activity + }; + + const statusColor = getStatusColor(); + + return ( +
+ {/* Animated dot */} + + + {/* Status text */} + + {getStatusText()} + + + {/* Queue depth badge (only when multiple requests are waiting) */} + {queueDepth > 1 && ( + + {activeRequests} active + + )} + + {/* Keyframe animations injected once via a +
+ ); +}; + +export default RateLimitIndicator; diff --git a/src/components/Common/RouteLoadingFallback.css b/src/components/Common/RouteLoadingFallback.css new file mode 100644 index 0000000..a17a78a --- /dev/null +++ b/src/components/Common/RouteLoadingFallback.css @@ -0,0 +1,37 @@ +.route-loading-fallback { + min-height: 100vh; + width: 100%; + display: flex; + align-items: center; + justify-content: center; + background: transparent; +} + +/* Smooth fade-in animation for route loading */ +.route-loading-fallback > * { + animation: routeFadeIn 0.3s ease-in-out; +} + +@keyframes routeFadeIn { + from { + opacity: 0; + transform: translateY(-10px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +/* Prevent layout shift during route transitions */ +.route-loading-fallback .center-spinner { + position: relative; + min-height: 60vh; +} + +/* Mobile optimization */ +@media (max-width: 768px) { + .route-loading-fallback .center-spinner { + min-height: 50vh; + } +} diff --git a/src/components/Common/RouteLoadingFallback.jsx b/src/components/Common/RouteLoadingFallback.jsx new file mode 100644 index 0000000..ead16b8 --- /dev/null +++ b/src/components/Common/RouteLoadingFallback.jsx @@ -0,0 +1,18 @@ +import LoadingSpinner from "./LoadingSpinner"; +import "./RouteLoadingFallback.css"; + +/** + * Route Loading Fallback Component + * + * Optimized fallback UI for lazy-loaded route components. + * Provides smooth transitions with minimal layout shift. + */ +const RouteLoadingFallback = ({ message = "Loading page..." }) => { + return ( +
+ +
+ ); +}; + +export default RouteLoadingFallback; diff --git a/src/components/Contributors.jsx b/src/components/Contributors.jsx deleted file mode 100644 index 63a573d..0000000 --- a/src/components/Contributors.jsx +++ /dev/null @@ -1,467 +0,0 @@ -import React, { useEffect, useMemo, useState } from "react"; -import "./contributors.css"; - -const OWNER = "KaranUnique"; -const REPO = "CryptoHub"; - -// Project Admin Config -const PROJECT_ADMIN = { - username: "KaranUnique", - repo: "CryptoHub", - repoUrl: "https://github.com/KaranUnique/CryptoHub", - githubUrl: "https://github.com/KaranUnique", - avatarUrl: `https://avatars.githubusercontent.com/KaranUnique?v=4&s=200`, - description: "Project Creator & Lead Maintainer" -}; - -// Points per level -const LEVEL_POINTS = { - 1: 2, - 2: 5, - 3: 11, -}; - -const getLevelFromPr = (pr) => { - const title = pr.title?.toLowerCase() || ""; - const titleMatch = title.match(/level[\s-]*(1|2|3)/); - if (titleMatch) { - return Number(titleMatch[1]); - } - - if (Array.isArray(pr.labels)) { - for (const label of pr.labels) { - const name = (label?.name || "").toLowerCase(); - const labelMatch = name.match(/level[\s-]*(1|2|3)/); - if (labelMatch) return Number(labelMatch[1]); - } - } - - return null; -}; - -const getRankFromPoints = (points) => { - if (points >= 30) return "Gold 🥇"; - if (points >= 20) return "Silver 🥈"; - if (points >= 10) return "Bronze 🥉"; - return "Contributor"; -}; - -const Contributors = () => { - const [contributors, setContributors] = useState([]); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(""); - const [search, setSearch] = useState(""); - const [sortBy, setSortBy] = useState("most_points"); - const [selectedRankFilter, setSelectedRankFilter] = useState("all"); - const [selectedContributor, setSelectedContributor] = useState(null); - const [showModal, setShowModal] = useState(false); - - // PUBLIC FETCH - NO TOKEN NEEDED - useEffect(() => { - const fetchAllMergedPRs = async () => { - setLoading(true); - setError(""); - - try { - let page = 1; - const perPage = 100; - let mergedPrs = []; - let keepFetching = true; - - // Fetch all pages of closed PRs (public GitHub API) - while (keepFetching) { - const url = `https://api.github.com/repos/${OWNER}/${REPO}/pulls?state=closed&per_page=${perPage}&page=${page}`; - - const response = await fetch(url, { - method: 'GET', - headers: { - 'Accept': 'application/vnd.github+json', - 'User-Agent': 'CryptoHub-Contributors-App', - }, - }); - - if (!response.ok) { - throw new Error(`GitHub API Error: ${response.status}`); - } - - const data = await response.json(); - const merged = data.filter((pr) => pr.merged_at); - mergedPrs = mergedPrs.concat(merged); - - if (data.length < perPage) { - keepFetching = false; - } else { - page += 1; - } - } - - const contributorsMap = {}; - - mergedPrs.forEach((pr) => { - const user = pr.user; - if (!user) return; - - const username = user.login; - const avatar_url = user.avatar_url; - const html_url = user.html_url; - - const level = getLevelFromPr(pr); - const points = level ? LEVEL_POINTS[level] || 0 : 0; - - if (!contributorsMap[username]) { - contributorsMap[username] = { - username, - avatar_url, - html_url, - totalPoints: 0, - totalPRs: 0, - rank: "Contributor", - prs: [], - }; - } - - contributorsMap[username].totalPRs += 1; - contributorsMap[username].totalPoints += points; - contributorsMap[username].prs.push({ - id: pr.id, - number: pr.number, - title: pr.title, - html_url: pr.html_url, - merged_at: pr.merged_at, - level, - points, - }); - }); - - const contributorsArr = Object.values(contributorsMap).map((c) => ({ - ...c, - rank: getRankFromPoints(c.totalPoints), - })); - - setContributors(contributorsArr); - } catch (err) { - console.error("Fetch error:", err); - setError(`Failed to load data: ${err.message}`); - } finally { - setLoading(false); - } - }; - - fetchAllMergedPRs(); - }, []); - - const filteredContributors = useMemo(() => { - let result = [...contributors]; - - if (search.trim()) { - const q = search.trim().toLowerCase(); - result = result.filter((c) => - c.username.toLowerCase().includes(q) - ); - } - - if (selectedRankFilter !== "all") { - const rankMap = { - gold: "Gold", - silver: "Silver", - bronze: "Bronze", - contributor: "Contributor", - }; - const selectedRank = rankMap[selectedRankFilter]; - result = result.filter((c) => c.rank === selectedRank); - } - - if (sortBy === "most_points") { - result.sort((a, b) => b.totalPoints - a.totalPoints); - } else if (sortBy === "most_prs") { - result.sort((a, b) => b.totalPRs - a.totalPRs); - } - - return result; - }, [contributors, search, selectedRankFilter, sortBy]); - - const handleOpenModal = (contributor) => { - setSelectedContributor(contributor); - setShowModal(true); - }; - - const handleCloseModal = () => { - setSelectedContributor(null); - setShowModal(false); - }; - - const handleOpenGitHubProfile = (url) => { - if (!url) return; - window.open(url, "_blank", "noopener,noreferrer"); - }; - - const handleOpenRepo = () => { - window.open(PROJECT_ADMIN.repoUrl, "_blank", "noopener,noreferrer"); - }; - - return ( -
- {/* Header Section */} -
-

Our Amazing Contributors

-

- Meet the talented developers who help make CryptoHub better every day. -

- -
-
- Contributors - {contributors.length} -
- -
- Total PRs - - {contributors.reduce((sum, c) => sum + c.totalPRs, 0)} - -
- -
- Total Points - - {contributors.reduce((sum, c) => sum + c.totalPoints, 0)} - -
-
-
- - {/* Filters Section */} -
- setSearch(e.target.value)} - /> - -
- - - -
-
- - {/* NEW PROJECT ADMIN SECTION */} -
-
-
-
- {PROJECT_ADMIN.username} -
👑
-
-
-

Project Admin

-

{PROJECT_ADMIN.username}

-

{PROJECT_ADMIN.description}

-
-
- -
-

Repository

-
- {PROJECT_ADMIN.repo} - 📂 -
-
- -
- - -
-
-
- - {/* Content Section */} -
- {loading && ( -
-

Loading contributor data from GitHub...

-
- )} - - {error && ( -
-

{error}

-

- Stats will show "No contributors found" - normal if repo has no merged PRs yet. -

-
- )} - - {!loading && !error && filteredContributors.length === 0 && ( -
-

No contributors found.

-

- No merged pull requests in KaranUnique/CryptoHub yet. -
Stats show 0 contributors, 0 PRs, 0 points ✅ -

-
- )} - - {!loading && !error && filteredContributors.length > 0 && ( -
- {filteredContributors.map((c) => ( -
-
-
- {c.username} -
-
-

{c.username}

-

- {c.rank} -

-
-
- -
-
- Points - {c.totalPoints} -
-
- Merged PRs - {c.totalPRs} -
-
- -
- - -
-
- ))} -
- )} -
- - {/* Modal for PR details */} - {showModal && selectedContributor && ( -
-
e.stopPropagation()} - > -
-

- PRs by {selectedContributor.username} -

- -
- -
- {selectedContributor.prs.length === 0 && ( -

- No merged pull requests found for this contributor. -

- )} - - {selectedContributor.prs.length > 0 && ( -
    - {selectedContributor.prs.map((pr) => ( -
  • -
    - - #{pr.number} — {pr.title} - - - Merged at: {pr.merged_at - ? new Date(pr.merged_at).toLocaleString() - : "N/A"} - -
    - -
    - - Level: {pr.level ? `Level ${pr.level}` : "Not specified"} - - - Points: {pr.points} - -
    -
  • - ))} -
- )} -
- -
- -
-
-
- )} -
- ); -}; - -export default Contributors; diff --git a/src/components/ActivityTracker.jsx b/src/components/Dashboard/ActivityTracker.jsx similarity index 94% rename from src/components/ActivityTracker.jsx rename to src/components/Dashboard/ActivityTracker.jsx index d855469..fb72477 100644 --- a/src/components/ActivityTracker.jsx +++ b/src/components/Dashboard/ActivityTracker.jsx @@ -1,7 +1,7 @@ import React, { useEffect, useRef, useCallback } from "react"; import { useLocation } from "react-router-dom"; -import { useActivity } from "../context/ActivityContext"; -import { useLeaderboard } from "../context/LeaderboardContext"; +import { useActivity } from "../../context/ActivityContext"; +import { useLeaderboard } from "../../context/LeaderboardContext"; const ActivityTracker = () => { const location = useLocation(); diff --git a/src/components/Leaderboard.css b/src/components/Dashboard/Leaderboard.css similarity index 100% rename from src/components/Leaderboard.css rename to src/components/Dashboard/Leaderboard.css diff --git a/src/components/Leaderboard.jsx b/src/components/Dashboard/Leaderboard.jsx similarity index 66% rename from src/components/Leaderboard.jsx rename to src/components/Dashboard/Leaderboard.jsx index 03cc802..47c7870 100644 --- a/src/components/Leaderboard.jsx +++ b/src/components/Dashboard/Leaderboard.jsx @@ -1,24 +1,24 @@ import React from "react"; -import { useLeaderboard } from "../context/LeaderboardContext"; -import { useAuth } from "../context/AuthContext"; +import { useLeaderboard } from "../../context/useLeaderboard"; +import { useAuth } from "../../context/useAuth"; import "./Leaderboard.css"; function Leaderboard() { - const { leaderboard, userRank, loading } = useLeaderboard(); - const { currentUser } = useAuth(); + const { leaderboard, userRank, loading } = useLeaderboard(); + const { currentUser } = useAuth(); - if (loading) { - return ( -
-
-
-
-
-

Loading leaderboard...

-
-
- ); - } + if (loading) { + return ( +
+
+
+
+
+

Loading leaderboard...

+
+
+ ); + } if (loading) { return ( @@ -39,23 +39,24 @@ function Leaderboard() { Top crypto enthusiasts ranked by activity

{userRank && ( -
- Your Rank: #{userRank} -
+
Your Rank: #{userRank}
)}
{leaderboard.length === 0 ? (
-

No leaderboard data yet. Start tracking coins to climb the ranks!

+

+ No leaderboard data yet. Start tracking coins to climb the ranks! +

) : ( leaderboard.map((entry, index) => (
{index === 0 && "🥇"} @@ -102,11 +103,21 @@ function Leaderboard() {

How to earn points:

    -
  • View a coin: +1 point
  • -
  • Add to portfolio: +5 points
  • -
  • Update portfolio: +2 points
  • -
  • Set price alert: +3 points
  • -
  • View chart: +2 points
  • +
  • + View a coin: +1 point +
  • +
  • + Add to portfolio: +5 points +
  • +
  • + Update portfolio: +2 points +
  • +
  • + Set price alert: +3 points +
  • +
  • + View chart: +2 points +
diff --git a/src/components/LineChart.jsx b/src/components/Dashboard/LineChart.jsx similarity index 100% rename from src/components/LineChart.jsx rename to src/components/Dashboard/LineChart.jsx diff --git a/src/components/Dashboard/MarketFilters.css b/src/components/Dashboard/MarketFilters.css new file mode 100644 index 0000000..8f2ad7b --- /dev/null +++ b/src/components/Dashboard/MarketFilters.css @@ -0,0 +1,150 @@ +.market-filters-wrapper { + position: relative; +} + +/* Desktop */ +/* Desktop */ +.market-filters { + display: flex; + align-items: center; + gap: 10px; + /* Removed large margins as it's inside a header */ + margin: 0; + flex-wrap: wrap; +} + +.filter-label { + font-size: 14px; + color: #aaa; +} + +.market-filter-btn { + padding: 6px 14px; + border-radius: 20px; + border: 1px solid rgba(255, 255, 255, 0.2); + background: transparent; + color: #fff; + cursor: pointer; + transition: background 0.2s ease, border 0.2s ease; + white-space: nowrap; +} + +.market-filter-btn:hover { + background: rgba(255, 255, 255, 0.08); +} + +.market-filter-btn.active { + background: #00f3ff; + border-color: #00f3ff; + color: #130032; + box-shadow: 0 0 12px rgba(0, 243, 255, 0.6); + font-weight: 700; + transform: translateY(-1px); +} + +.market-filter-btn.active:hover { + background: #33f6ff; + box-shadow: 0 0 16px rgba(0, 243, 255, 0.8); +} + +/* Toggle button hidden on desktop */ +.filter-toggle { + display: none; +} + +/* ================= MOBILE ================= */ +@media (max-width: 768px) { + .market-filters-wrapper { + display: flex; + flex-direction: column; + align-items: flex-end; + position: relative; + z-index: 50; + } + + .filter-toggle { + display: block; + background: #111; + color: #fff; + border: none; + padding: 10px 14px; + border-radius: 6px; + font-size: 14px; + cursor: pointer; + border: 1px solid rgba(255, 255, 255, 0.1); + } + + .market-filters { + position: absolute; + top: 100%; + right: 0; + margin-top: 8px; + flex-direction: column; + align-items: stretch; + gap: 8px; + background: #111; + border: 1px solid rgba(255, 255, 255, 0.1); + padding: 10px; + border-radius: 8px; + min-width: 160px; + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5); + + /* Animation setup */ + opacity: 0; + transform: translateY(-12px); + max-height: 0; + overflow: hidden; + pointer-events: none; + + transition: + opacity 0.25s ease, + transform 0.25s ease, + max-height 0.3s ease; + } + + .market-filters.open { + opacity: 1; + transform: translateY(0); + max-height: 300px; + pointer-events: auto; + } + + .filter-label { + display: none; + } + + .market-filter-btn { + width: 100%; + text-align: left; + padding: 10px; + } +} + +/* ================= SMALL MOBILE (up to 400px) ================= */ +@media (max-width: 400px) { + .filter-toggle { + padding: 8px 12px; + font-size: 13px; + width: 100%; + /* Make toggle full width for easier tapping */ + text-align: center; + } + + .market-filters-wrapper { + width: 100%; + /* Ensure it takes full available width */ + align-items: center; + /* Center align on very small screens */ + } + + .market-filters { + width: 100%; + /* Dropdown takes full width */ + right: auto; + /* Reset right positioning */ + left: 0; + /* Align left */ + min-width: auto; + /* Allow it to shrink if needed */ + } +} \ No newline at end of file diff --git a/src/components/Dashboard/MarketFilters.jsx b/src/components/Dashboard/MarketFilters.jsx new file mode 100644 index 0000000..17ed871 --- /dev/null +++ b/src/components/Dashboard/MarketFilters.jsx @@ -0,0 +1,58 @@ +import React, { useState } from 'react' +import { useContext } from "react"; +import { CoinContext } from "../../context/CoinContextInstance"; +import "./MarketFilters.css"; + +const MarketFilters = () => { + const { selectedFilters, setSelectedFilters } = useContext(CoinContext); + const [open, setOpen] = useState(false); + + const toggleFilter = (filter) => { + setSelectedFilters((prev) => { + // If clicking "all", reset to all + if (filter === "all") return ["all"]; + + // If clicking the currently active filter, toggle off to "all" + if (prev.includes(filter)) return ["all"]; + + // Otherwise, switch to the new filter (single selection) + return [filter]; + }); + }; + + return ( +
+ {/* Mobile toggle button */} + + +
+ Filters: + + + + + + +
+
+ ) +} + +export default MarketFilters \ No newline at end of file diff --git a/src/components/NewsPanel.css b/src/components/Dashboard/NewsPanel.css similarity index 100% rename from src/components/NewsPanel.css rename to src/components/Dashboard/NewsPanel.css diff --git a/src/components/NewsPanel.jsx b/src/components/Dashboard/NewsPanel.jsx similarity index 97% rename from src/components/NewsPanel.jsx rename to src/components/Dashboard/NewsPanel.jsx index 8861b14..2b0d645 100644 --- a/src/components/NewsPanel.jsx +++ b/src/components/Dashboard/NewsPanel.jsx @@ -1,5 +1,5 @@ import React, { useState, useEffect } from 'react'; -import { fetchCoinNews, analyzeSentiment } from '../utils/newsService'; +import { fetchCoinNews, analyzeSentiment } from '../../utils/newsService'; import './NewsPanel.css'; const NewsPanel = ({ coinId, coinName }) => { diff --git a/src/components/Footer.jsx b/src/components/Footer.jsx deleted file mode 100644 index a674145..0000000 --- a/src/components/Footer.jsx +++ /dev/null @@ -1,134 +0,0 @@ -import React, { useState } from 'react'; -import './Footer.css'; -import { Link } from 'react-router-dom'; -import { FaFacebook, FaInstagram, FaGithub, FaTwitter, FaDiscord } from 'react-icons/fa'; -import { FaCcVisa, FaCcMastercard, FaCcPaypal, FaApplePay, FaGooglePay } from 'react-icons/fa'; -import { FiSend } from 'react-icons/fi'; - - -const Footer = () => { - const [email, setEmail] = useState(''); - const [isSubmitting, setIsSubmitting] = useState(false); - const [status, setStatus] = useState(''); - - const currentYear = new Date().getFullYear(); - - const handleSubscribe = async (e) => { - e.preventDefault(); - if (!email.includes('@')) { - setStatus('Please enter a valid email address'); - return; - } - - setIsSubmitting(true); - setStatus('Subscribing...'); - - // Simulate API call - setTimeout(() => { - setStatus('Successfully subscribed to CryptoHub Insights!'); - setEmail(''); - setIsSubmitting(false); - setTimeout(() => setStatus(''), 3000); - }, 1500); - }; - - return ( -
-
- {/* Main Content Grid */} -
- {/* Brand Section */} -
-

- CryptoHub. -

-

- The world's most accurate real-time crypto tracking & analytics platform. - Live prices, advanced charts, portfolio tracking & AI insights. -

-
- - - - - -
-
- - {/* Quick Links */} -
-

Markets

-
    -
  • Trending Coins
  • -
  • Top Gainers
  • -
  • Top Losers
  • -
  • New Listings
  • -
-
- -
-

Product

-
    -
  • Features
  • -
  • Pricing
  • -
  • Portfolio
  • -
  • API Access
  • -
-
- - {/* Newsletter Section */} -
-

Newsletter

-

Weekly crypto insights, alpha signals & market analysis

-
- setEmail(e.target.value)} - required - disabled={isSubmitting} - /> - -
- {status &&

{status}

} -
-
- - {/* Social & Bottom Section */} -
- - -
-

- Privacy Policy • - Terms of Service • - Cookie Policy -

-

Copyright © {currentYear} CryptoHub. All rights reserved.

-
-
-
-
- ); -}; - -export default Footer; \ No newline at end of file diff --git a/src/components/Footer.css b/src/components/Layout/Footer.css similarity index 70% rename from src/components/Footer.css rename to src/components/Layout/Footer.css index d71ed38..9eabcf5 100644 --- a/src/components/Footer.css +++ b/src/components/Layout/Footer.css @@ -55,8 +55,9 @@ color: #b8bdd0; font-size: 15px; line-height: 1.6; - margin-bottom: 24px; - max-width: 320px; + margin: 0 auto 24px; + max-width: 400px; + } /* Payment Methods */ @@ -252,12 +253,43 @@ color: #7928ca; } +.footer-newsletter { + max-width: 100%; +} + +.footer-newsletter form { + display: flex; + flex-wrap: wrap; + gap: 10px; +} + +.footer-newsletter form input { + border-radius: 5px; + flex: 1; + padding: 5px 0px; +} + +.footer-newsletter form input::placeholder { + padding-left: 10px; +} + +button { + border-radius: 5px; + padding: 5px 10px; + background-color: #666; +} + +button:hover { + background-color: #7928ca; +} + /* Animations */ @keyframes slideIn { from { opacity: 0; transform: translateY(10px); } + to { opacity: 1; transform: translateY(0); @@ -266,11 +298,22 @@ /* Responsive Design */ @media (max-width: 1024px) { + .footer-brand { + grid-column: span 3; + text-align: center; + } + + .footer-brand p { + margin: 0 auto 24px; + max-width: 400px; + text-align: center; + } + .footer-main { grid-template-columns: 1fr 1fr 1fr; gap: 32px; } - + .footer-brand { grid-column: span 3; } @@ -280,46 +323,134 @@ .footer-content { padding: 40px 20px 20px; } - + .footer-main { grid-template-columns: 1fr; gap: 32px; } - + .footer-brand { text-align: center; } - + .payment-methods { justify-content: center; } - + .footer-bottom-section { flex-direction: column; text-align: center; } - + .footer-bottom { text-align: center; order: -1; } - + .newsletter-form { max-width: 100%; } } @media (max-width: 480px) { + .footer-content { + padding: 30px 16px; + } + + .footer-main { + grid-template-columns: 1fr; + text-align: center; + gap: 16px; + /* Reduced specific gap for mobile compactness */ + } + + .footer-links ul { + display: flex; + flex-direction: column; + align-items: center; + gap: 8px; + } + + .footer-links h4, + .footer-newsletter h4, + .footer-newsletter p { + margin-bottom: 12px; + /* Reduced specific margins */ + } + + .footer-newsletter { + text-align: left; + /* Align text left as requested */ + width: 100%; + margin: 0; + /* Remove centering margins */ + padding: 0 10px; + /* Add slight padding to prevent edge clipping */ + } + + .newsletter-form { + display: flex; + flex-direction: column; + width: 100%; + gap: 10px; + } + + .newsletter-form input { + width: 100%; + margin: 0; + box-sizing: border-box; + } + + .newsletter-form button { + width: 100%; + height: 48px; + /* Reset height from square to rectangle */ + border-radius: 8px; + /* Match input radius */ + box-sizing: border-box; + } + .social-links { - justify-content: center; + justify-content: flex-start; + /* Align social icons left too */ + padding-left: 10px; } - + .payment-methods { - gap: 6px; + justify-content: center; + gap: 10px; + margin-top: 10px; } - + .payment-icon { width: 36px; height: 24px; } + + .footer-bottom-section { + padding-top: 24px; + gap: 20px; + } +} + +/* Very small screens */ +@media (max-width: 400px) { + .footer-logo { + font-size: 28px; + } + + .footer-links h4 { + font-size: 15px; + } + + .footer-brand p { + font-size: 14px; + padding: 0 10px; + } + + .footer-bottom a { + display: block; + margin: 5px 0; + /* Stack links vertically */ + } } \ No newline at end of file diff --git a/src/components/Layout/Footer.jsx b/src/components/Layout/Footer.jsx new file mode 100644 index 0000000..2bcb327 --- /dev/null +++ b/src/components/Layout/Footer.jsx @@ -0,0 +1,205 @@ +import React, { useState } from "react"; +import "./Footer.css"; +import { Link } from "react-router-dom"; +import { + FaFacebook, + FaInstagram, + FaGithub, + FaTwitter, + FaDiscord, + FaCcVisa, + FaCcMastercard, + FaCcPaypal, + FaApplePay, + FaGooglePay, +} from "react-icons/fa"; +import { FiSend } from "react-icons/fi"; + +const Footer = () => { + const [email, setEmail] = useState(""); + const [isSubmitting, setIsSubmitting] = useState(false); + const [status, setStatus] = useState(""); + + const currentYear = new Date().getFullYear(); + + const isValidEmail = (value) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value); + + const handleSubscribe = async (e) => { + e.preventDefault(); + + if (!isValidEmail(email)) { + setStatus("Please enter a valid email address"); + return; + } + + setIsSubmitting(true); + setStatus("Subscribing..."); + + // Simulated API call + setTimeout(() => { + setStatus("Successfully subscribed to CryptoHub Insights!"); + setEmail(""); + setIsSubmitting(false); + setTimeout(() => setStatus(""), 3000); + }, 1500); + }; + + return ( +
+
+ {/* Main Grid */} +
+ {/* Brand */} +
+

+ CryptoHub. +

+

+ Real-time crypto tracking, advanced analytics, market insights & + portfolio tools. +

+ +
+ + + + + +
+
+ + {/* Markets */} +
+

Markets

+
    +
  • + Trending Coins +
  • +
  • + Top Gainers +
  • +
  • + Top Losers +
  • +
  • + New Listings +
  • +
+
+ + {/* Product */} +
+

Product

+
    +
  • + Features +
  • +
  • + Pricing +
  • +
  • + Portfolio +
  • +
  • + API Access +
  • +
  • + Feedback +
  • +
+
+ + {/* Newsletter */} +
+

Newsletter

+

Weekly crypto insights & market updates

+ +
+ setEmail(e.target.value)} + disabled={isSubmitting} + aria-label="Email address" + required + /> + +
+ + {status && ( +

+ {status} +

+ )} +
+
+ + {/* Bottom */} +
+ + +
+

+ Privacy Policy |{" "} + Terms of Service |{" "} + Cookies|{" "} +

+

© {currentYear} CryptoHub. All rights reserved.

+
+
+
+
+ ); +}; + +export default Footer; diff --git a/src/components/Layout/Navbar.css b/src/components/Layout/Navbar.css new file mode 100644 index 0000000..a810551 --- /dev/null +++ b/src/components/Layout/Navbar.css @@ -0,0 +1,2118 @@ +/* RESPONSIVE NAVBAR CSS WITH MOBILE DROPDOWN MENU */ + +/* Base Reset */ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +:root { + --primary-color: #b026ff; + --secondary-color: #00f3ff; + --bg-dark: #0a0a0f; + --text-light: #ffffff; + --border-color: rgba(255, 255, 255, 0.1); + --transition-smooth: cubic-bezier(0.4, 0, 0.2, 1); + --transition-bounce: cubic-bezier(0.68, -0.55, 0.265, 1.55); + --transition-elastic: cubic-bezier(0.175, 0.885, 0.32, 1.275); +} + +/* MOTION & ANIMATION KEYFRAMES */ + +/* Navbar slide-down entrance animation */ +@keyframes navbarSlideDown { + 0% { + transform: translateY(-100%); + opacity: 0; + } + + 100% { + transform: translateY(0); + opacity: 1; + } +} + +/* Staggered fade-in for nav items */ +@keyframes navItemFadeIn { + 0% { + opacity: 0; + } + + 100% { + opacity: 1; + } +} + +/* Glow pulse effect */ +@keyframes glowPulse { + + 0%, + 100% { + box-shadow: + 0 0 5px rgba(0, 243, 255, 0.3), + 0 0 10px rgba(176, 38, 255, 0.2); + } + + 50% { + box-shadow: + 0 0 15px rgba(0, 243, 255, 0.5), + 0 0 25px rgba(176, 38, 255, 0.4); + } +} + +/* Shimmer effect for buttons */ +@keyframes shimmer { + 0% { + background-position: -200% center; + } + + 100% { + background-position: 200% center; + } +} + +/* Floating animation */ +@keyframes float { + + 0%, + 100% { + transform: translateY(0px); + } + + 50% { + transform: translateY(-3px); + } +} + +/* Border gradient animation */ +@keyframes borderGlow { + + 0%, + 100% { + border-color: rgba(0, 243, 255, 0.3); + } + + 50% { + border-color: rgba(176, 38, 255, 0.5); + } +} + +/* Ripple effect */ +@keyframes ripple { + 0% { + transform: scale(0); + opacity: 0.5; + } + + 100% { + transform: scale(4); + opacity: 0; + } +} + +/* Text gradient animation */ +@keyframes gradientShift { + + 0%, + 100% { + background-position: 0% 50%; + } + + 50% { + background-position: 100% 50%; + } +} + +/* Navbar Container */ +.navbar { + position: fixed; + top: 0; + left: 0; + right: 0; + width: 100%; + background: rgba(10, 10, 15, 0.95); + backdrop-filter: blur(20px) saturate(180%); + -webkit-backdrop-filter: blur(20px) saturate(180%); + border-bottom: 1px solid var(--border-color); + height: 86px; + padding: 0 1.5rem; + display: flex; + margin-right: 5px; + align-items: center; + z-index: 1000; + transition: all 0.4s var(--transition-smooth); + /* Entrance animation */ + animation: navbarSlideDown 0.6s var(--transition-elastic) forwards; + overflow: visible; +} + +/* Responsive Navbar Padding to match App Container */ +@media (max-width: 1440px) { + .navbar { + padding: 0 1.25rem; + } +} + +@media (max-width: 1024px) { + .navbar { + padding: 0 1rem; + } +} + +@media (max-width: 768px) { + .navbar { + padding: 0 0.75rem; + } +} + +@media (max-width: 480px) { + .navbar { + padding: 0 0.5rem; + } +} + +/* Subtle border glow animation on navbar */ +.navbar::after { + content: ""; + position: absolute; + bottom: 0; + left: 0; + right: 0; + height: 1px; + background: linear-gradient(90deg, + transparent, + rgba(0, 243, 255, 0.5), + rgba(176, 38, 255, 0.5), + transparent); + opacity: 0; + transition: opacity 0.4s ease; +} + +.navbar:hover::after { + opacity: 1; +} + +.navbar.scrolled { + height: 60px; + padding: 0 2%; + background: rgba(10, 10, 15, 0.98); + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3); +} + +/* Navbar Content Container */ +.navbar-content { + display: flex; + justify-content: space-between; + align-items: center; + gap: 3rem; + width: 100%; + max-width: 1400px; + margin: 0 auto; +} + +/* Desktop layout - grid for logo, menu, actions */ +@media (min-width: 768px) { + .navbar-content { + display: grid; + grid-template-columns: auto 1fr auto; + /* logo | menu | actions */ + gap: 1rem; + align-items: center; + } +} + +@media (min-width: 1024px) { + .navbar-content { + gap: 2rem; + } +} + +@media (min-width: 1280px) { + .navbar-content { + gap: 3rem; + } +} + +/* Logo/Brand */ +.navbar-logo { + display: flex; + align-items: center; + height: 100%; + gap: 0.875rem; + text-decoration: none; + margin-left: 0; + flex-shrink: 0; + font-weight: 800; + font-size: 1.35rem; + background: linear-gradient(135deg, #ffffff 0%, #00f3ff 50%, #b026ff 100%); + background-size: 200% 200%; + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + transition: + transform 0.4s var(--transition-elastic), + background-position 0.6s ease; + position: relative; + padding: 0; + animation: navItemFadeIn 0.5s var(--transition-smooth) 0.1s forwards; +} + +.navbar-logo:hover { + transform: translateY(-3px) scale(1.02); + background-position: 100% 50%; + text-decoration: none; + filter: drop-shadow(0 0 8px rgba(0, 243, 255, 0.4)); +} + +.navbar-logo::after { + height: 6px; + filter: blur(6px); + opacity: 0.6; +} + +.navbar-logo:hover::after { + width: 100%; +} + +.navbar-logo-icon { + width: 44px; + height: 44px; + border-radius: 12px; + background: linear-gradient(135deg, + rgba(176, 38, 255, 0.2) 0%, + rgba(0, 243, 255, 0.15) 50%, + rgba(176, 38, 255, 0.1) 100%); + border: 1.5px solid rgba(176, 38, 255, 0.3); + display: flex; + align-items: center; + justify-content: center; + transition: all 0.4s var(--transition-smooth); + position: relative; + overflow: hidden; +} + +.navbar-logo-icon::before { + content: ""; + position: absolute; + top: 0; + left: -100%; + width: 100%; + height: 100%; + background: linear-gradient(90deg, + transparent, + rgba(255, 255, 255, 0.1), + transparent); + transition: left 0.6s var(--transition-smooth); +} + +.navbar-logo:hover .navbar-logo-icon { + border-color: rgba(0, 243, 255, 0.6); + box-shadow: + 0 8px 32px rgba(0, 243, 255, 0.25), + 0 0 0 1px rgba(176, 38, 255, 0.2); + transform: rotate(5deg) scale(1.05); +} + +.navbar-logo:hover .navbar-logo-icon::before { + left: 100%; +} + +.logo-img { + width: 44px; + height: 44px; + flex-shrink: 0; + object-fit: contain; + filter: drop-shadow(0 0 6px rgba(0, 243, 255, 0.8)) drop-shadow(0 0 12px rgba(176, 38, 255, 0.4)); + transition: all 0.4s var(--transition-smooth); +} + +.navbar-logo:hover .logo-img { + transform: scale(1.1); + filter: drop-shadow(0 0 8px rgba(0, 243, 255, 1)) drop-shadow(0 0 16px rgba(176, 38, 255, 0.6)); +} + +.logo-text { + display: inline-block; + white-space: nowrap; + flex-shrink: 0; +} + +/* Desktop Navigation Menu */ +.navbar-menu { + display: flex; + flex-direction: column; + list-style: none; + gap: 1.2rem; + padding: 0; + margin: 0; +} + +@media (min-width: 768px) { + .navbar-menu { + display: flex; + flex-direction: row; + flex: 1; + max-width: none; + margin: 0; + padding: 0; + gap: 0.5rem; + justify-content: center; + align-items: center; + } +} + +@media (min-width: 1024px) { + .navbar-menu { + gap: 0.875rem; + } +} + +@media (min-width: 1280px) { + .navbar-menu { + gap: 1.2rem; + } +} + +.navbar-item { + display: flex; + position: relative; +} + +.navbar-item:hover { + z-index: 100; +} + +.navbar-link { + display: flex; + align-items: center; + padding: 0.5rem 1.125rem; + color: rgba(255, 255, 255, 0.75); + text-decoration: none; + font-weight: 500; + font-size: 0.95rem; + letter-spacing: 0.3px; + transition: all 0.3s var(--transition-smooth); + position: relative; + white-space: nowrap; + border-radius: 10px; + overflow: hidden; + z-index: 1; +} + +.navbar-link::before { + content: ""; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: transparent; + border-radius: 10px; + transform: scaleX(0); + transform-origin: right; + transition: transform 0.4s var(--transition-smooth); + z-index: -1; +} + +.navbar-link:hover { + color: #ffffff; + transform: translateY(-3px) scale(1.05); + text-decoration: none; + text-shadow: + 0 0 10px rgba(0, 243, 255, 0.5), + 0 0 20px rgba(176, 38, 255, 0.3); +} + +.navbar-item:hover .navbar-link { + color: #ffffff; + transform: translateY(-3px) scale(1.05); + text-decoration: none; + text-shadow: + 0 0 10px rgba(0, 243, 255, 0.5), + 0 0 20px rgba(176, 38, 255, 0.3); +} + +.navbar-link:focus-visible, +.navbar-link:active { + color: #ffffff; + transform: translateY(-3px) scale(1.05); + text-decoration: none; + text-shadow: + 0 0 10px rgba(0, 243, 255, 0.5), + 0 0 20px rgba(176, 38, 255, 0.3); +} + +.navbar-link:hover::before { + transform: scaleX(1); + transform-origin: left; +} + +.navbar-item:hover .navbar-link::before { + transform: scaleX(1); + transform-origin: left; +} + +.navbar-link:focus-visible::before, +.navbar-link:active::before { + transform: scaleX(1); + transform-origin: left; +} + +.navbar-link::after { + content: ""; + position: absolute; + bottom: 2px; + left: 50%; + transform: translateX(-50%); + width: 0; + height: 2px; + background: linear-gradient(90deg, #00f3ff, #b026ff); + border-radius: 2px; + transition: all 0.4s var(--transition-bounce); + opacity: 0; + box-shadow: 0 0 8px rgba(0, 243, 255, 0.6); +} + +.navbar-link:hover::after { + width: 80%; + opacity: 1; + animation: glowPulse 1.5s ease infinite; +} + +.navbar-item:hover .navbar-link::after { + width: 80%; + opacity: 1; + animation: glowPulse 1.5s ease infinite; +} + +.navbar-link:focus-visible::after, +.navbar-link:active::after { + width: 80%; + opacity: 1; + animation: glowPulse 1.5s ease infinite; +} + +/* Staggered animation for nav items */ +.navbar-item:nth-child(1) { + animation: navItemFadeIn 0.5s var(--transition-smooth) 0.15s both; +} + +.navbar-item:nth-child(2) { + animation: navItemFadeIn 0.5s var(--transition-smooth) 0.2s both; +} + +.navbar-item:nth-child(3) { + animation: navItemFadeIn 0.5s var(--transition-smooth) 0.25s both; +} + +.navbar-item:nth-child(4) { + animation: navItemFadeIn 0.5s var(--transition-smooth) 0.3s both; +} + +.navbar-item:nth-child(5) { + animation: navItemFadeIn 0.5s var(--transition-smooth) 0.35s both; +} + +.navbar-item:nth-child(6) { + animation: navItemFadeIn 0.5s var(--transition-smooth) 0.4s both; +} + +.navbar-item:nth-child(7) { + animation: navItemFadeIn 0.5s var(--transition-smooth) 0.45s both; +} + +.navbar-link.active { + color: #ffffff; + font-weight: 600; + background: linear-gradient(135deg, + rgba(176, 38, 255, 0.15) 0%, + rgba(0, 243, 255, 0.1) 100%); + box-shadow: + 0 4px 20px rgba(176, 38, 255, 0.15), + inset 0 1px 0 rgba(255, 255, 255, 0.1); + animation: borderGlow 3s ease infinite; + border: none; +} + +.navbar-link.active::after { + width: 80%; + opacity: 1; + animation: glowPulse 2s ease infinite; +} + +/* Actions Container */ +.navbar-actions { + display: flex; + align-items: center; + gap: 1.5rem; + flex-shrink: 0; +} + +/* Desktop Auth */ +.desktop-auth { + display: flex; + align-items: center; + gap: 1rem; +} + +/* Mobile Only Elements */ +.mobile-only { + display: none; +} + +/* Action Buttons */ +.navbar-btn { + padding: 0.75rem 1.5rem; + border-radius: 12px; + font-weight: 600; + font-size: 0.95rem; + text-decoration: none; + transition: all 0.4s var(--transition-smooth); + cursor: pointer; + border: none; + white-space: nowrap; + display: inline-block; + text-align: center; + min-width: fit-content; + position: relative; + overflow: hidden; + letter-spacing: 0.5px; + z-index: 1; +} + +.navbar-btn::before { + content: ""; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: inherit; + border-radius: inherit; + z-index: -1; + transition: transform 0.4s var(--transition-smooth); +} + +.navbar-btn:hover::before { + transform: scale(1.05); +} + +.navbar-btn-login { + color: #ffffff; + background: transparent; + border: 2px solid rgba(255, 255, 255, 0.2); + padding: 0.75rem 1.8rem; + position: relative; + overflow: hidden; + animation: navItemFadeIn 0.5s var(--transition-smooth) 0.4s forwards; + transition: + border-color 0.3s var(--transition-elastic), + box-shadow 0.3s var(--transition-elastic); +} + +.navbar-btn-login::after { + content: ""; + position: absolute; + top: 0; + left: -100%; + width: 100%; + height: 100%; + background: linear-gradient(90deg, + transparent, + rgba(255, 255, 255, 0.2), + transparent); + transition: left 0.5s var(--transition-smooth); +} + +/* Ripple container for login button */ +.navbar-btn-login::before { + content: ""; + position: absolute; + top: 50%; + left: 50%; + width: 0; + height: 0; + background: rgba(255, 255, 255, 0.15); + border-radius: 50%; + transform: translate(-50%, -50%); + transition: + width 0.6s ease, + height 0.6s ease; +} + +.navbar-btn-login:hover { + background: rgba(255, 255, 255, 0.08); + border-color: rgba(0, 243, 255, 0.5); + transform: translateY(-3px) scale(1.03); + box-shadow: + 0 8px 32px rgba(0, 243, 255, 0.15), + 0 0 0 1px rgba(0, 243, 255, 0.1); + color: #00f3ff; + text-decoration: none; + text-shadow: 0 0 10px rgba(0, 243, 255, 0.5); +} + +.navbar-btn-login:hover::after { + left: 100%; +} + +.navbar-btn-login:hover::before { + width: 300px; + height: 300px; + opacity: 0; +} + +.navbar-btn-login:active { + transform: translateY(-1px) scale(0.98); +} + +.navbar-btn-signup { + background: linear-gradient(135deg, #00f3ff 0%, #b026ff 50%, #00f3ff 100%); + background-size: 200% auto; + color: #000000; + padding: 0.75rem 2rem; + font-weight: 700; + box-shadow: + 0 4px 20px rgba(0, 243, 255, 0.3), + 0 0 0 1px rgba(176, 38, 255, 0.2); + animation: navItemFadeIn 0.5s var(--transition-smooth) 0.5s forwards; + transition: + border-color 0.3s var(--transition-elastic), + box-shadow 0.3s var(--transition-elastic); + position: relative; + z-index: 1; +} + +.navbar-btn-signup::before { + content: ""; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: linear-gradient(135deg, #00f3ff 0%, #b026ff 50%, #00f3ff 100%); + background-size: 200% auto; + border-radius: inherit; + z-index: -1; + opacity: 0; + transition: opacity 0.3s ease; +} + +.navbar-btn-signup:hover { + transform: translateY(-4px) scale(1.05); + box-shadow: + 0 15px 45px rgba(0, 243, 255, 0.5), + 0 0 0 2px rgba(176, 38, 255, 0.4), + 0 0 40px rgba(176, 38, 255, 0.3), + 0 0 60px rgba(0, 243, 255, 0.2); + animation: shimmer 1.5s linear infinite; + color: #ffffff; + text-decoration: none; + text-shadow: 0 0 10px rgba(255, 255, 255, 0.5); +} + +.navbar-btn-signup:hover::before { + opacity: 1; + animation: shimmer 1.5s linear infinite; +} + +.navbar-btn-signup:active { + transform: translateY(-2px) scale(1.02); + box-shadow: + 0 8px 25px rgba(0, 243, 255, 0.4), + 0 0 0 1px rgba(176, 38, 255, 0.3); +} + +/* User Menu */ +.user-menu { + display: flex; + align-items: center; + gap: 0.875rem; + background: rgba(255, 255, 255, 0.05); + padding: 10px 18px; + border-radius: 14px; + border: 1px solid rgba(255, 255, 255, 0.08); + flex-shrink: 0; + backdrop-filter: blur(10px); + transition: all 0.4s var(--transition-elastic); + animation: navItemFadeIn 0.5s var(--transition-smooth) 0.3s both; + position: relative; + overflow: hidden; +} + +.user-menu::before { + content: ""; + position: absolute; + top: 0; + left: -100%; + width: 50%; + height: 100%; + background: linear-gradient(90deg, + transparent, + rgba(255, 255, 255, 0.05), + transparent); + transition: left 0.8s var(--transition-smooth); +} + +.user-menu:hover { + background: rgba(255, 255, 255, 0.08); + border-color: rgba(0, 243, 255, 0.3); + transform: translateY(-2px); + box-shadow: + 0 12px 32px rgba(0, 0, 0, 0.3), + 0 0 0 1px rgba(0, 243, 255, 0.1); +} + +.user-menu:hover::before { + left: 150%; +} + +.user-email { + font-size: 0.85rem; + color: rgba(255, 255, 255, 0.85); + max-width: 200px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + font-weight: 500; + flex-shrink: 1; +} + +.icon-btn { + background: none; + border: none; + color: rgba(255, 255, 255, 0.7); + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.3s var(--transition-smooth); + font-size: 1.1rem; + padding: 8px; + border-radius: 8px; + flex-shrink: 0; + position: relative; + overflow: hidden; +} + +.icon-btn::before { + content: ""; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(255, 255, 255, 0.1); + border-radius: 8px; + transform: scale(0); + transition: transform 0.3s var(--transition-smooth); +} + +.icon-btn:hover { + color: #00f3ff; + transform: translateY(-2px) rotate(10deg) scale(1.1); + text-shadow: 0 0 10px rgba(0, 243, 255, 0.8); +} + +.icon-btn:hover::before { + transform: scale(1); + background: rgba(0, 243, 255, 0.15); +} + +.icon-btn:active { + transform: translateY(0) rotate(0deg) scale(0.95); +} + +.logout-btn { + padding: 8px 16px; + border-radius: 10px; + font-size: 0.85rem; + font-weight: 600; + background: linear-gradient(135deg, + rgba(239, 68, 68, 0.15) 0%, + rgba(239, 68, 68, 0.1) 100%); + border: 1.5px solid rgba(239, 68, 68, 0.25); + color: #ef4444; + cursor: pointer; + transition: all 0.3s var(--transition-smooth); + flex-shrink: 0; + white-space: nowrap; + position: relative; + overflow: hidden; +} + +.logout-btn::before { + content: ""; + position: absolute; + top: 0; + left: -100%; + width: 100%; + height: 100%; + background: linear-gradient(90deg, + transparent, + rgba(239, 68, 68, 0.1), + transparent); + transition: left 0.6s var(--transition-smooth); +} + +.logout-btn:hover { + background: linear-gradient(135deg, + rgba(239, 68, 68, 0.25) 0%, + rgba(239, 68, 68, 0.2) 100%); + border-color: rgba(239, 68, 68, 0.6); + transform: translateY(-3px) scale(1.03); + box-shadow: + 0 10px 25px rgba(239, 68, 68, 0.25), + 0 0 20px rgba(239, 68, 68, 0.15); + text-shadow: 0 0 8px rgba(239, 68, 68, 0.5); +} + +.logout-btn:hover::before { + left: 100%; +} + +.logout-btn:active { + transform: translateY(-1px) scale(0.98); +} + +/* Profile Menu Container */ +.profile-menu-container { + position: relative; +} + +.profile-icon-btn { + width: 40px; + height: 40px; + font-size: 1.2rem; + border-radius: 50%; + background: linear-gradient(135deg, + rgba(176, 38, 255, 0.2) 0%, + rgba(0, 243, 255, 0.15) 100%); + border: 2px solid rgba(176, 38, 255, 0.3); + color: #fff; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.3s var(--transition-smooth); + position: relative; + overflow: hidden; +} + +.profile-icon-btn::before { + content: ""; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: linear-gradient(135deg, + rgba(0, 243, 255, 0.2) 0%, + rgba(176, 38, 255, 0.2) 100%); + opacity: 0; + transition: opacity 0.3s var(--transition-smooth); +} + +.profile-icon-btn:hover { + transform: translateY(-3px) scale(1.08); + border-color: rgba(0, 243, 255, 0.6); + box-shadow: + 0 8px 24px rgba(0, 243, 255, 0.3), + 0 0 20px rgba(176, 38, 255, 0.25); +} + +.profile-icon-btn:hover::before { + opacity: 1; +} + +.profile-icon-btn:active { + transform: translateY(0) scale(0.98); +} + +/* Profile Dropdown */ +.profile-dropdown { + position: absolute; + top: calc(100% + 12px); + right: 0; + min-width: 280px; + background: rgba(15, 15, 25, 0.98); + border: 1px solid rgba(176, 38, 255, 0.3); + border-radius: 16px; + box-shadow: + 0 20px 60px rgba(0, 0, 0, 0.6), + 0 0 40px rgba(176, 38, 255, 0.2), + inset 0 1px 0 rgba(255, 255, 255, 0.1); + backdrop-filter: blur(20px); + overflow: hidden; + opacity: 0; + visibility: hidden; + transform: translateY(-10px) scale(0.95); + transition: all 0.3s var(--transition-smooth); + z-index: 100; +} + +.profile-dropdown.show { + opacity: 1; + visibility: visible; + transform: translateY(0) scale(1); +} + +.profile-dropdown::before { + content: ""; + position: absolute; + top: -8px; + right: 18px; + width: 0; + height: 0; + border-left: 8px solid transparent; + border-right: 8px solid transparent; + border-bottom: 8px solid rgba(176, 38, 255, 0.3); +} + +.profile-dropdown-header { + padding: 18px 20px; + background: linear-gradient(135deg, + rgba(176, 38, 255, 0.15) 0%, + rgba(0, 243, 255, 0.1) 100%); + display: flex; + align-items: center; + gap: 12px; +} + +.profile-dropdown-header .profile-icon { + font-size: 1.25rem; + color: #00f3ff; + filter: drop-shadow(0 0 8px rgba(0, 243, 255, 0.6)); +} + +.profile-email { + color: rgba(255, 255, 255, 0.95); + font-size: 0.9rem; + font-weight: 500; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + flex: 1; +} + +.profile-dropdown-divider { + height: 1px; + background: linear-gradient(90deg, + transparent, + rgba(176, 38, 255, 0.3), + rgba(0, 243, 255, 0.3), + transparent); +} + +.profile-dropdown-items { + padding: 8px; +} + +.profile-dropdown-item { + display: flex; + align-items: center; + gap: 12px; + padding: 12px 16px; + color: rgba(255, 255, 255, 0.85); + font-size: 0.95rem; + font-weight: 500; + text-decoration: none; + border: none; + background: transparent; + cursor: pointer; + border-radius: 10px; + transition: all 0.25s var(--transition-smooth); + width: 100%; + text-align: left; + position: relative; + overflow: hidden; +} + +.profile-dropdown-item::before { + content: ""; + position: absolute; + top: 0; + left: -100%; + width: 100%; + height: 100%; + background: linear-gradient(90deg, + transparent, + rgba(176, 38, 255, 0.15), + transparent); + transition: left 0.4s var(--transition-smooth); +} + +.profile-dropdown-item:hover { + background: rgba(176, 38, 255, 0.2); + color: #fff; + transform: translateX(5px); + box-shadow: 0 4px 12px rgba(176, 38, 255, 0.2); +} + +.profile-dropdown-item:hover::before { + left: 100%; +} + +.profile-dropdown-item svg { + font-size: 1.1rem; + transition: all 0.25s var(--transition-smooth); +} + +.profile-dropdown-item:hover svg { + transform: scale(1.15); + filter: drop-shadow(0 0 6px currentColor); +} + +.profile-dropdown-item.logout-item { + color: #ef4444; + margin-top: 4px; +} + +.profile-dropdown-item.logout-item:hover { + background: rgba(239, 68, 68, 0.15); + color: #ff6b6b; +} + +.profile-dropdown-item:active { + transform: translateX(3px) scale(0.98); +} + +/* Mobile Menu Toggle Button */ +.navbar-toggle { + display: none; + background: none; + border: none; + cursor: pointer; + padding: 10px; + border-radius: 10px; + transition: all 0.3s var(--transition-smooth); + position: relative; + overflow: hidden; + color: #ffffff; +} + +.navbar-toggle::before { + content: ""; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(255, 255, 255, 0.1); + border-radius: 10px; + transform: scale(0); + transition: transform 0.3s var(--transition-smooth); +} + +.navbar-toggle:hover::before { + transform: scale(1); +} + +.navbar-toggle:hover { + color: var(--secondary-color); +} + +.navbar-toggle span { + display: block; + width: 24px; + height: 2.5px; + background: currentColor; + border-radius: 2px; + transition: all 0.3s var(--transition-smooth); + position: relative; +} + +.navbar-toggle span:not(:last-child) { + margin-bottom: 5px; +} + +.navbar-toggle.active span:nth-child(1) { + transform: rotate(45deg) translateY(7.5px); + background: #00f3ff; +} + +.navbar-toggle.active span:nth-child(2) { + opacity: 0; + transform: translateX(20px); +} + +.navbar-toggle.active span:nth-child(3) { + transform: rotate(-45deg) translateY(-7.5px); + background: #b026ff; +} + +/* Mobile Menu Dropdown */ +.mobile-menu { + position: fixed; + top: 86px; + left: 0; + right: 0; + width: 100%; + background: rgba(10, 10, 15, 0.98); + backdrop-filter: blur(10px); + -webkit-backdrop-filter: blur(10px); + border-bottom: 1px solid rgba(255, 255, 255, 0.1); + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5); + padding: 0.5rem 0; + animation: slideDown 0.3s ease-out; + z-index: 999; +} + +@keyframes slideDown { + from { + opacity: 0; + transform: translateY(-10px); + } + + to { + opacity: 1; + transform: translateY(0); + } +} + +.mobile-menu-list { + list-style: none; + padding: 0; + margin: 0; +} + +.mobile-menu-item { + border-bottom: 1px solid rgba(255, 255, 255, 0.05); +} + +.mobile-menu-link { + display: block; + padding: 1rem 1.5rem; + color: rgba(255, 255, 255, 0.8); + text-decoration: none; + font-weight: 500; + font-size: 0.95rem; + transition: all 0.3s ease; + cursor: pointer; + position: relative; + border-radius: 10px; + overflow: hidden; +} + +.mobile-menu-link::before { + content: ""; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: linear-gradient(135deg, + rgba(176, 38, 255, 0.1) 0%, + rgba(0, 243, 255, 0.05) 100%); + border-radius: 10px; + transform: scaleX(0); + transform-origin: right; + transition: transform 0.4s ease; + z-index: -1; +} + +.mobile-menu-link::after { + content: ""; + position: absolute; + bottom: 2px; + left: 50%; + transform: translateX(-50%); + width: 0; + height: 2px; + background: linear-gradient(90deg, #00f3ff, #b026ff); + border-radius: 2px; + transition: all 0.4s ease; + opacity: 0; + box-shadow: 0 0 8px rgba(0, 243, 255, 0.6); +} + +.mobile-menu-link:hover, +.mobile-menu-link.active { + color: #ffffff; + transform: translateY(-2px) scale(1.02); + text-decoration: none; + text-shadow: + 0 0 10px rgba(0, 243, 255, 0.5), + 0 0 20px rgba(176, 38, 255, 0.3); +} + +.mobile-menu-link:hover::before, +.mobile-menu-link.active::before { + transform: scaleX(1); + transform-origin: left; +} + +.mobile-menu-link:hover::after, +.mobile-menu-link.active::after { + width: 80%; + opacity: 1; + animation: glowPulse 1.5s ease infinite; +} + +.mobile-menu-item:hover .mobile-menu-link { + color: #ffffff; + transform: translateY(-2px) scale(1.02); + text-decoration: none; + text-shadow: + 0 0 10px rgba(0, 243, 255, 0.5), + 0 0 20px rgba(176, 38, 255, 0.3); +} + +.mobile-menu-item:hover .mobile-menu-link::before { + transform: scaleX(1); + transform-origin: left; +} + +.mobile-menu-item:hover .mobile-menu-link::after { + width: 80%; + opacity: 1; + animation: glowPulse 1.5s ease infinite; +} + +.mobile-menu-link:focus-visible, +.mobile-menu-link:active { + color: #ffffff; + transform: translateY(-2px) scale(1.02); + text-shadow: + 0 0 10px rgba(0, 243, 255, 0.5), + 0 0 20px rgba(176, 38, 255, 0.3); +} + +.mobile-menu-link:focus-visible::before, +.mobile-menu-link:active::before { + transform: scaleX(1); + transform-origin: left; +} + +.mobile-menu-link:focus-visible::after, +.mobile-menu-link:active::after { + width: 80%; + opacity: 1; + animation: glowPulse 1.5s ease infinite; +} + +.mobile-dropdown-menu { + list-style: none; + padding: 0; + margin: 0; + background: rgba(0, 0, 0, 0.3); +} + +.mobile-dropdown-link { + display: block; + padding: 0.75rem 1.5rem 0.75rem 2.5rem; + color: rgba(255, 255, 255, 0.7); + text-decoration: none; + font-size: 0.9rem; + transition: all 0.3s ease; + position: relative; + border-radius: 8px; + overflow: hidden; +} + +.mobile-dropdown-link::before { + content: ""; + position: absolute; + left: 0; + top: 0; + width: 3px; + height: 100%; + background: linear-gradient(180deg, #b026ff, #00f3ff); + transform: scaleY(0); + transition: transform 0.3s ease; + border-radius: 0 3px 3px 0; +} + +.mobile-dropdown-link:hover { + background: linear-gradient(135deg, + rgba(176, 38, 255, 0.15), + rgba(0, 243, 255, 0.1)); + color: #ffffff; + transform: translateX(5px); + box-shadow: 0 4px 12px rgba(0, 243, 255, 0.1); +} + +.mobile-dropdown-link:hover::before { + transform: scaleY(1); +} + +.mobile-dropdown-menu li:hover .mobile-dropdown-link { + background: linear-gradient(135deg, + rgba(176, 38, 255, 0.15), + rgba(0, 243, 255, 0.1)); + color: #ffffff; + transform: translateX(5px); + box-shadow: 0 4px 12px rgba(0, 243, 255, 0.1); +} + +.mobile-dropdown-menu li:hover .mobile-dropdown-link::before { + transform: scaleY(1); +} + +.mobile-dropdown-link:focus-visible, +.mobile-dropdown-link:active { + background: linear-gradient(135deg, + rgba(176, 38, 255, 0.15), + rgba(0, 243, 255, 0.1)); + color: #ffffff; + transform: translateX(5px); + box-shadow: 0 4px 12px rgba(0, 243, 255, 0.1); +} + +.mobile-dropdown-link:focus-visible::before, +.mobile-dropdown-link:active::before { + transform: scaleY(1); +} + +.mobile-controls { + padding: 1rem 2rem; + border-top: 1px solid rgba(255, 255, 255, 0.05); +} + +.mobile-control-item { + display: flex; + align-items: center; + justify-content: space-between; + padding: 0.75rem 0; + color: rgba(255, 255, 255, 0.7); + cursor: pointer; +} + +.toggle-track { + width: 50px; + height: 26px; + background: rgba(255, 255, 255, 0.1); + border-radius: 13px; + position: relative; + transition: all 0.3s ease; + border: 1px solid rgba(255, 255, 255, 0.2); +} + +.toggle-track.dark { + background: linear-gradient(135deg, #00f3ff 0%, #b026ff 100%); +} + +.toggle-thumb { + position: absolute; + top: 2px; + left: 2px; + width: 20px; + height: 20px; + background: white; + border-radius: 50%; + transition: all 0.3s ease; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); +} + +.toggle-track.dark .toggle-thumb { + transform: translateX(24px); +} + +.mobile-auth { + padding: 1rem 1.5rem; + border-top: 1px solid rgba(255, 255, 255, 0.05); + display: flex; + gap: 0.75rem; + flex-direction: column; +} + +.mobile-auth .navbar-btn { + width: 100%; + text-align: center; + padding: 0.6rem 1.2rem; + font-size: 0.95rem; + font-weight: 600; + display: block; +} + +.mobile-auth .navbar-btn-login { + border: 2px solid rgba(0, 243, 255, 0.5); + background: transparent; + color: #00f3ff; +} + +.mobile-auth .navbar-btn-login:hover { + background: rgba(0, 243, 255, 0.1); + border-color: #00f3ff; +} + +.mobile-auth .navbar-btn-signup { + background: linear-gradient(135deg, #00f3ff 0%, #b026ff 100%); + color: white; + border: none; +} + +.mobile-auth .navbar-btn-signup:hover { + transform: translateY(-2px); + box-shadow: 0 5px 20px rgba(0, 243, 255, 0.4); +} + +.mobile-change-password { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.75rem 1rem; + margin-bottom: 0.75rem; + color: rgba(255, 255, 255, 0.7); + text-decoration: none; + border-radius: 8px; + background: rgba(255, 255, 255, 0.05); + border: 1px solid rgba(255, 255, 255, 0.1); + transition: all 0.3s ease; +} + +.mobile-change-password:hover { + background: rgba(255, 255, 255, 0.1); + color: #ffffff; +} + +.mobile-auth-buttons { + display: flex; + flex-direction: row; + gap: 0.75rem; +} + +.mobile-auth-buttons .login-btn, +.mobile-auth-buttons .signup-btn { + padding: 0.75rem 1.5rem; + border-radius: 8px; + font-weight: 600; + font-size: 0.95rem; + border: none; + cursor: pointer; + transition: all 0.3s ease; + text-align: center; + text-decoration: none; + display: block; + flex: 1; +} + +.mobile-auth-buttons .login-btn { + background: transparent; + border: 2px solid rgba(255, 255, 255, 0.25); + color: #ffffff; +} + +.mobile-auth-buttons .login-btn:hover { + background: rgba(255, 255, 255, 0.1); + border-color: rgba(255, 255, 255, 0.6); +} + +.mobile-auth-buttons .signup-btn { + background: linear-gradient(135deg, #00f3ff 0%, #b026ff 100%); + color: #000000; +} + +.mobile-auth-buttons .signup-btn:hover { + transform: translateY(-2px); + box-shadow: 0 8px 25px rgba(0, 243, 255, 0.5); +} + +.full-width { + width: 100%; +} + +/* Visibility Classes */ +.desktop-only { + display: none; +} + +.mobile-only { + display: flex; +} + +/* =================================== + RESPONSIVE BREAKPOINTS + =================================== */ + +/* Desktop Only */ +.desktop-only { + display: none; +} + +@media (min-width: 1024px) { + + /* Show desktop elements, hide mobile elements on desktop only */ + .desktop-only { + display: flex; + } + + .mobile-only { + display: none; + } + + .mobile-menu { + display: none !important; + } + + .navbar-menu { + display: flex; + } +} + +/* Mobile Only */ +@media (max-width: 767px) { + .navbar { + padding: 0 0.75rem; + } + + .navbar.scrolled { + padding: 0 0.75rem; + } + + .navbar-content { + display: flex; + justify-content: space-between; + padding-right: 1rem; + align-items: center; + gap: 1rem; + } + + + .mobile-only { + display: flex; + } + + .desktop-only { + display: none !important; + } + + .navbar-toggle { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + } + + .navbar-menu { + display: none; + } + + .desktop-auth { + display: flex !important; + } + + .navbar-btn-login, + .navbar-btn-signup { + display: none; + } + + .navbar-logo { + font-size: 1.6rem; + } + + .navbar-logo-icon { + width: 54px; + height: 54px; + border-radius: 14px; + } + + .navbar-actions { + gap: 0.875rem; + justify-content: flex-end; + } + + .profile-icon-btn { + width: 48px; + height: 48px; + font-size: 1.3rem; + } + + .profile-dropdown { + min-width: 260px; + right: -10px; + } + + .profile-dropdown-header { + padding: 14px 16px; + } + + .profile-email { + font-size: 0.85rem; + } + + .profile-dropdown-item { + padding: 10px 14px; + font-size: 0.9rem; + } +} + +/* Tablet */ +@media (min-width: 768px) and (max-width: 1023px) { + .navbar { + padding: 0.75rem 2%; + } + + .navbar.scrolled { + padding: 0.6rem 2%; + } + + .navbar-content { + display: flex; + justify-content: space-between; + align-items: center; + gap: 1rem; + } + + + .navbar-toggle { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + } + + .navbar-menu { + display: none; + } + + .desktop-auth { + display: flex !important; + } + + .navbar-btn-login, + .navbar-btn-signup { + display: none; + } + + .navbar-logo { + font-size: 1.2rem; + } + + .navbar-logo-icon { + width: 40px; + height: 40px; + } + + .navbar-actions { + gap: 1rem; + justify-content: flex-end; + } + + .profile-icon-btn { + width: 50px; + height: 50px; + font-size: 1.35rem; + } + + .profile-dropdown { + min-width: 270px; + } + + .profile-dropdown-header { + padding: 16px 18px; + } + + .profile-email { + font-size: 0.875rem; + } + + .profile-dropdown-item { + padding: 11px 15px; + font-size: 0.925rem; + } +} + +/* Desktop */ +@media (min-width: 1024px) and (max-width: 1279px) { + .navbar { + padding: 0 1.25rem; + } + + .navbar.scrolled { + padding: 0 1.25rem; + } + + .navbar-toggle { + display: none; + } + + .navbar-menu { + display: flex !important; + gap: 0.5rem; + } + + .navbar-content { + padding: 0; + grid-template-columns: auto 1fr auto; + gap: 1.5rem; + } + + .navbar-link { + padding: 0.5rem 0.75rem; + font-size: 0.9rem; + } + + .navbar-logo { + font-size: 1.35rem; + } + + .navbar-logo-icon { + width: 42px; + height: 42px; + } + + .navbar-actions { + gap: 1rem; + } + + .navbar-btn { + padding: 0.6rem 1.1rem; + font-size: 0.85rem; + } + + .navbar-btn-login { + padding: 0.6rem 1.3rem; + } + + .navbar-btn-signup { + padding: 0.6rem 1.5rem; + } + + .profile-icon-btn { + width: 50px; + height: 50px; + font-size: 1.35rem; + } + + .profile-dropdown { + min-width: 275px; + } +} + +/* Large Desktop */ +@media (min-width: 1280px) and (max-width: 1440px) { + .navbar { + padding: 0 1.25rem; + } + + .navbar.scrolled { + padding: 0 1.25rem; + } + + .navbar-content { + padding: 0; + max-width: 1400px; + gap: 2rem; + } +} + +@media (min-width: 1441px) { + .navbar { + padding: 0 1.5rem; + } + + .navbar.scrolled { + padding: 0 1.5rem; + } +} + +/* Original large desktop styles continued... */ +@media (min-width: 1280px) { + + .navbar-link { + padding: 0.5rem 1rem; + font-size: 1rem; + } + + .navbar-logo { + font-size: 1.5rem; + } + + .navbar-logo-icon { + width: 44px; + height: 44px; + } + + .navbar-actions { + gap: 1.5rem; + } + + .navbar-btn { + padding: 0.75rem 1.5rem; + font-size: 0.95rem; + } + + .navbar-btn-login { + padding: 0.75rem 1.75rem; + } + + .navbar-btn-signup { + padding: 0.75rem 2rem; + } + + .profile-icon-btn { + width: 52px; + height: 52px; + font-size: 1.4rem; + } +} + +/* Accessibility */ +@media (prefers-reduced-motion: reduce) { + * { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + } +} + +/* Touch devices */ +@media (hover: none) and (pointer: coarse) { + + .navbar-link, + .navbar-btn, + .icon-btn, + .mobile-nav-link, + .login-btn, + .signup-btn { + min-height: 44px; + min-width: 44px; + } +} + +/* Dropdown */ +.dropdown-trigger { + cursor: pointer; + position: relative; + display: flex; + align-items: center; + gap: 0.5rem; +} + +.dropdown-trigger::after { + content: "▼"; + font-size: 0.65rem; + transition: transform 0.3s var(--transition-smooth); + color: rgba(255, 255, 255, 0.6); +} + +.navbar-item:hover .dropdown-trigger::after { + color: var(--secondary-color); +} + +.navbar-item:hover .dropdown-trigger { + color: #ffffff; + background: linear-gradient(135deg, + rgba(176, 38, 255, 0.1) 0%, + rgba(0, 243, 255, 0.05) 100%); +} + +.dropdown-menu { + position: absolute; + top: calc(100% + 0.5rem); + left: 50%; + transform: translateX(-50%) translateY(-10px); + background: rgba(10, 10, 15, 0.98); + backdrop-filter: blur(20px); + -webkit-backdrop-filter: blur(20px); + border: 1px solid rgba(255, 255, 255, 0.15); + border-radius: 12px; + min-width: 200px; + padding: 0.5rem; + z-index: 1000; + opacity: 0; + visibility: hidden; + pointer-events: none; + transition: + opacity 0.3s var(--transition-smooth), + transform 0.3s var(--transition-smooth), + visibility 0.3s; + box-shadow: + 0 10px 40px rgba(0, 0, 0, 0.5), + 0 0 20px rgba(176, 38, 255, 0.15), + inset 0 1px 0 rgba(255, 255, 255, 0.1); +} + +.navbar-item:hover .dropdown-menu { + opacity: 1; + visibility: visible; + pointer-events: auto; + transform: translateX(-50%) translateY(0); +} + +.dropdown-menu.show { + opacity: 1; + visibility: visible; + pointer-events: auto; + transform: translateX(-50%) translateY(0); +} + +.dropdown-menu::before { + content: ""; + position: absolute; + top: -6px; + left: 50%; + transform: translateX(-50%); + width: 0; + height: 0; + border-left: 6px solid transparent; + border-right: 6px solid transparent; + border-bottom: 6px solid rgba(255, 255, 255, 0.15); +} + +.dropdown-menu li { + list-style: none; + margin: 0.25rem 0; +} + +.dropdown-menu li:first-child { + margin-top: 0; +} + +.dropdown-menu li:last-child { + margin-bottom: 0; +} + +.dropdown-link { + display: block; + padding: 0.75rem 1rem; + border-radius: 8px; + color: rgba(255, 255, 255, 0.75); + text-decoration: none; + font-weight: 500; + font-size: 0.9rem; + transition: all 0.3s var(--transition-smooth); + position: relative; + overflow: hidden; +} + +.dropdown-link::before { + content: ""; + position: absolute; + left: 0; + top: 0; + width: 3px; + height: 100%; + background: linear-gradient(180deg, + var(--secondary-color), + var(--primary-color)); + transform: scaleY(0); + transition: transform 0.3s var(--transition-smooth); + border-radius: 0 3px 3px 0; +} + +.dropdown-link:hover { + background: linear-gradient(135deg, + rgba(176, 38, 255, 0.15), + rgba(0, 243, 255, 0.1)); + color: #ffffff; + padding-left: 1.25rem; + transform: translateX(5px); + box-shadow: 0 4px 12px rgba(0, 243, 255, 0.1); +} + +.dropdown-menu li:hover .dropdown-link { + background: linear-gradient(135deg, + rgba(176, 38, 255, 0.15), + rgba(0, 243, 255, 0.1)); + color: #ffffff; + padding-left: 1.25rem; + transform: translateX(5px); + box-shadow: 0 4px 12px rgba(0, 243, 255, 0.1); +} + +.dropdown-link:focus-visible, +.dropdown-link:active { + background: linear-gradient(135deg, + rgba(176, 38, 255, 0.15), + rgba(0, 243, 255, 0.1)); + color: #ffffff; + padding-left: 1.25rem; + transform: translateX(5px); + box-shadow: 0 4px 12px rgba(0, 243, 255, 0.1); +} + +.dropdown-link:hover::before { + transform: scaleY(1); +} + +.dropdown-menu li:hover .dropdown-link::before { + transform: scaleY(1); +} + +.dropdown-link:focus-visible::before, +.dropdown-link:active::before { + transform: scaleY(1); +} + +.dropdown-link:focus { + outline: 2px solid var(--secondary-color); + outline-offset: 2px; + background: linear-gradient(135deg, + rgba(176, 38, 255, 0.15), + rgba(0, 243, 255, 0.1)); + color: #ffffff; +} + +/* Mobile Dropdown Styles */ +@media (max-width: 767px) { + .navbar-content { + transform: translateX(-30px); + } + + .navbar { + height: auto; + } + + .dropdown-trigger { + width: 100%; + justify-content: space-between; + padding: 1rem 2rem; + color: rgba(255, 255, 255, 0.7); + } + + .dropdown-trigger::after { + content: "▼"; + font-size: 0.7rem; + margin-left: auto; + transition: transform 0.3s var(--transition-smooth); + } + + .navbar-item:has(.dropdown-menu.show) .dropdown-trigger, + .dropdown-trigger:active { + color: #ffffff; + background: rgba(255, 255, 255, 0.05); + } + + .navbar-item:has(.dropdown-menu.show) .dropdown-trigger::after { + transform: rotate(180deg); + color: var(--secondary-color); + } + + .dropdown-menu { + position: static; + transform: none; + opacity: 0; + max-height: 0; + overflow: hidden; + margin-top: 0.5rem; + margin-left: 1rem; + padding: 0; + border: none; + border-left: 2px solid rgba(0, 243, 255, 0.3); + background: transparent; + box-shadow: none; + transition: all 0.3s var(--transition-smooth); + } + + .dropdown-menu.show { + opacity: 1; + max-height: 300px; + padding: 0.5rem 0; + } + + .dropdown-menu::before { + display: none; + } + + .dropdown-link { + padding: 0.6rem 1rem; + font-size: 0.85rem; + } + + .dropdown-link:hover { + padding-left: 1.5rem; + } +} diff --git a/src/components/Layout/Navbar.jsx b/src/components/Layout/Navbar.jsx new file mode 100644 index 0000000..1d8f03a --- /dev/null +++ b/src/components/Layout/Navbar.jsx @@ -0,0 +1,412 @@ +import React, { useState, useEffect, useCallback, useRef } from "react"; +import { useAuth } from "../../context/useAuth"; +import { Link, useNavigate, useLocation } from "react-router-dom"; +import { FiLock, FiUser, FiLogOut, FiMail, FiBookmark } from "react-icons/fi"; +import "./Navbar.css"; + +function Navbar() { + const { currentUser, logout, isEmailProvider } = useAuth(); + const navigate = useNavigate(); + const location = useLocation(); + const navRef = useRef(null); + const mobileMenuRef = useRef(null); + const profileMenuRef = useRef(null); + + const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false); + const [scrolled, setScrolled] = useState(false); + const [openDropdown, setOpenDropdown] = useState(null); + const [isProfileOpen, setIsProfileOpen] = useState(false); + const [mobileOpenDropdown, setMobileOpenDropdown] = useState(null); + + const isDashboardPage = location.pathname === "/dashboard"; + + /* -------------------- Handlers -------------------- */ + + const handleDropdownEnter = (label) => { + setOpenDropdown(label); + }; + + const handleDropdownLeave = () => { + setOpenDropdown(null); + }; + + const handleDropdownClick = (label) => { + setOpenDropdown(openDropdown === label ? null : label); + }; + + const toggleMobileMenu = () => { + setIsMobileMenuOpen((prev) => !prev); + }; + + const closeMobileMenu = () => { + setIsMobileMenuOpen(false); + setOpenDropdown(null); + }; + + const handleLogout = useCallback(async () => { + try { + await logout(); + navigate("/"); + closeMobileMenu(); + } catch (error) { + console.error("Logout failed:", error); + } + }, [logout, navigate]); + + /* -------------------- Effects -------------------- */ + + useEffect(() => { + if (isMobileMenuOpen) { + document.body.style.overflow = "hidden"; + } else { + document.body.style.overflow = "unset"; + } + return () => { + document.body.style.overflow = "unset"; + }; + }, [isMobileMenuOpen]); + + useEffect(() => { + const onScroll = () => setScrolled(window.scrollY > 20); + window.addEventListener("scroll", onScroll); + return () => window.removeEventListener("scroll", onScroll); + }, []); + + useEffect(() => { + const handleClickOutside = (e) => { + // If click is inside the navbar, ignore — allow internal controls to handle state + if (navRef.current && navRef.current.contains(e.target)) return; + + // Clicked outside navbar — close everything + if (openDropdown) setOpenDropdown(null); + if (isProfileOpen) setIsProfileOpen(false); + if (mobileOpenDropdown) setMobileOpenDropdown(null); + if (isMobileMenuOpen) setIsMobileMenuOpen(false); + }; + + const handleEscape = (event) => { + if (event.key === "Escape") { + if (openDropdown) setOpenDropdown(null); + if (isProfileOpen) setIsProfileOpen(false); + if (mobileOpenDropdown) setMobileOpenDropdown(null); + } + }; + + document.addEventListener("click", handleClickOutside); + document.addEventListener("keydown", handleEscape); + + return () => { + document.removeEventListener("click", handleClickOutside); + document.removeEventListener("keydown", handleEscape); + }; + }, [openDropdown, isProfileOpen]); + + // Close menus when the route changes + useEffect(() => { + setIsMobileMenuOpen(false); + setOpenDropdown(null); + setIsProfileOpen(false); + setMobileOpenDropdown(null); + }, [location.pathname]); + + const handleMobileDropdownClick = (label) => { + setMobileOpenDropdown((prev) => (prev === label ? null : label)); + }; + + /* -------------------- Nav Links -------------------- */ + + const navLinks = [ + { to: "/", label: "Home" }, + { to: "/pricing", label: "Pricing" }, + { + label: "Markets", + dropdown: [ + { to: "/new-listings", label: "New Listings" }, + { to: "/trending", label: "Trending" }, + { to: "/gainers", label: "Gainers" }, + { to: "/top-losers", label: "Top Losers" }, + ], + }, + { to: "/blog", label: "Insights" }, + { to: "/features", label: "Features" }, + { + label: "more", + dropdown: [ + { to: "/about", label: "About" }, + { to: "/contributors", label: "Contributors" }, + { to: "/contactus", label: "Contact Us" }, + { to: "/faq", label: "FAQ" }, + ], + }, + + ]; + + const authenticatedNavLinks = [ + ...navLinks, + { to: "/dashboard", label: "Dashboard" }, + { to: "/leaderboard", label: "Leaderboard" }, + ]; + + const isLinkActive = (to) => { + if (!to) return false; + if (to === "/") return location.pathname === "/"; + return location.pathname === to || location.pathname.startsWith(to + "/") || location.pathname.startsWith(to); + }; + + /* -------------------- JSX -------------------- */ + + return ( + + ); +} + +export default Navbar; diff --git a/src/components/ScrollToTop.tsx b/src/components/Layout/ScrollToTop.jsx similarity index 77% rename from src/components/ScrollToTop.tsx rename to src/components/Layout/ScrollToTop.jsx index 16d44af..86dcc80 100644 --- a/src/components/ScrollToTop.tsx +++ b/src/components/Layout/ScrollToTop.jsx @@ -1,7 +1,7 @@ import { useEffect, useState } from "react"; import { ArrowUp } from "lucide-react"; -const ScrollToTop = () => { +const ScrollToTop = ({ lenis }) => { const [visible, setVisible] = useState(false); useEffect(() => { @@ -14,10 +14,11 @@ const ScrollToTop = () => { }, []); const scrollToTop = () => { - window.scrollTo({ - top: 0, - behavior: "smooth", - }); + if (lenis) { + lenis.scrollTo(0); + } else { + window.scrollTo({ top: 0, behavior: "smooth" }); + } }; if (!visible) return null; @@ -27,7 +28,7 @@ const ScrollToTop = () => { onClick={scrollToTop} aria-label="Scroll to top" className=" - fixed bottom-6 right-6 z-50 + fixed bottom-40 sm:bottom-32 right-2 sm:right-6 z-50 rounded-full bg-cyan-300 text-black p-3 shadow-lg hover:bg-gray-800 diff --git a/src/components/Legal/CookiePolicy.css b/src/components/Legal/CookiePolicy.css new file mode 100644 index 0000000..1e0b3db --- /dev/null +++ b/src/components/Legal/CookiePolicy.css @@ -0,0 +1,105 @@ + + + +/* Background Wrapper */ +.cookie-wrapper { + min-height: 100vh; + display: flex; + justify-content: center; + align-items: center; + padding: 60px 20px; + background: linear-gradient(135deg, #141e30, #243b55); + font-family: "Poppins", sans-serif; +} + +/* Main Big Card */ +.main-card { + width: 100%; + max-width: 1100px; + background: rgba(255, 255, 255, 0.08); + backdrop-filter: blur(15px); + border-radius: 25px; + padding: 40px; + border: 1px solid rgba(255, 255, 255, 0.15); + box-shadow: 0 20px 50px rgba(0, 0, 0, 0.4); + color: #ffffff; +} + +/* Header Card */ +.header-card { + text-align: center; + margin-bottom: 40px; + padding: 25px; + border-radius: 18px; + background: rgba(0, 0, 0, 0.25); + border: 1px solid rgba(255, 255, 255, 0.1); +} + +.header-card h1 { + font-size: 2.5rem; + background: linear-gradient(90deg, #00f5a0, #00d9f5); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; +} + +.header-card p { + margin-top: 10px; + color: #d1d5db; + font-size: 0.9rem; +} + +/* Section Cards */ +.sections { + display: grid; + gap: 25px; +} + +.section-card { + padding: 25px; + border-radius: 18px; + background: rgba(255, 255, 255, 0.05); + border: 1px solid rgba(255, 255, 255, 0.1); + transition: all 0.3s ease; +} + +.section-card:hover { + border: 1px solid #00f5a0; + box-shadow: 0 10px 30px rgba(0, 245, 160, 0.3); +} + +.section-card h2 { + margin-bottom: 15px; + color: #00f5a0; + font-size: 1.3rem; +} + +.section-card p { + color: #e5e7eb; + line-height: 1.6; +} + +.section-card ul { + padding-left: 20px; +} + +.section-card li { + margin-bottom: 8px; + color: #e5e7eb; + transition: 0.3s ease; +} + +.section-card li:hover { + color: #00d9f5; + transform: translateX(5px); +} + +/* Responsive */ +@media (max-width: 768px) { + .main-card { + padding: 25px; + } + + .header-card h1 { + font-size: 2rem; + } +} diff --git a/src/components/Legal/CookiePolicy.jsx b/src/components/Legal/CookiePolicy.jsx new file mode 100644 index 0000000..519b4a5 --- /dev/null +++ b/src/components/Legal/CookiePolicy.jsx @@ -0,0 +1,161 @@ +"use client"; + +import React, { useEffect, useState } from "react"; +import { motion } from "framer-motion"; + +const sections = [ + { + id: "cookies", + title: "What Are Cookies?", + content: + "Cookies are small text files stored on your device when you visit a website. They help improve your browsing experience by remembering preferences and providing better functionality.", + }, + { + id: "usage", + title: "How We Use Cookies", + list: [ + "Remember user preferences", + "Improve website performance", + "Analyze traffic and usage patterns", + "Enhance website security", + ], + }, + { + id: "types", + title: "Types of Cookies We Use", + list: [ + "Essential Cookies – Required for core functionality.", + "Analytics Cookies – Help us understand user behavior.", + "Performance Cookies – Improve speed and responsiveness.", + ], + }, + { + id: "manage", + title: "Managing Cookies", + content: + "You can control or disable cookies through your browser settings. However, disabling cookies may affect certain features of the website.", + }, + { + id: "contact", + title: "Contact Us", + content: + "If you have questions about our Cookie Policy, please contact us at support@cryptohub.com.", + }, +]; + +const CookiePolicy = () => { + const [scrollProgress, setScrollProgress] = useState(0); + + useEffect(() => { + const handleScroll = () => { + const totalHeight = + document.documentElement.scrollHeight - + document.documentElement.clientHeight; + const progress = (window.scrollY / totalHeight) * 100; + setScrollProgress(progress); + }; + + window.addEventListener("scroll", handleScroll); + return () => window.removeEventListener("scroll", handleScroll); + }, []); + + return ( +
+ + {/* Scroll Progress Bar */} +
+ + {/* Hero Section */} +
+ + 🍪 Cookie Policy + + +

+ Transparency matters. This policy explains how we use cookies to + enhance your experience and protect your privacy. +

+ +

+ Last Updated: January 2026 +

+
+ +
+ + {/* Sidebar Navigation */} +
+
+ {sections.map((section) => ( + + + + + {section.title} + + + ))} +
+
+ + {/* Content Sections */} +
+ {sections.map((section, index) => ( + +

+ {index + 1}. {section.title} +

+ + {section.content && ( +

+ {section.content} +

+ )} + + {section.list && ( +
    + {section.list.map((item, i) => ( +
  • + {item} +
  • + ))} +
+ )} +
+ ))} +
+
+ + {/* Footer */} +
+ © 2026 CryptoHub. All rights reserved. +
+
+ ); +}; + +export default CookiePolicy; \ No newline at end of file diff --git a/src/components/Legal/PrivacyPolicy.css b/src/components/Legal/PrivacyPolicy.css new file mode 100644 index 0000000..49b962c --- /dev/null +++ b/src/components/Legal/PrivacyPolicy.css @@ -0,0 +1,201 @@ + +.privacy-policy-container { + max-width: 1100px; + margin: 0 auto; + padding: 3rem 1.5rem; + font-family: 'Inter', sans-serif; + line-height: 1.8; + color: #f1f5f9; + min-height: 100vh; + + background: linear-gradient( + 160deg, + #0f0f23 0%, + #1a0d2e 25%, + #2a1b4a 55%, + #140c2b 75%, + #0f0f23 100% + ); + + position: relative; + overflow: hidden; +} + +.privacy-header { + text-align: center; + margin-bottom: 4rem; + padding: 3rem 2rem; + + background: rgba(255, 255, 255, 0.06); + backdrop-filter: blur(20px); + border-radius: 24px; + border: 1px solid rgba(255, 255, 255, 0.08); + + box-shadow: 0 20px 40px rgba(0, 0, 0, 0.4); + transition: all 0.4s ease; +} + +.privacy-header:hover { + transform: translateY(-4px); + box-shadow: 0 25px 50px rgba(0, 212, 255, 0.15); +} + +.privacy-title { + font-size: 2.8rem; + font-weight: 700; + background: linear-gradient(90deg, #00d4ff, #7c3aed); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + margin-bottom: 1rem; +} + +.privacy-date { + font-size: 0.95rem; + color: rgba(255, 255, 255, 0.6); + margin-bottom: 1.2rem; +} + +.privacy-intro { + font-size: 1.05rem; + color: rgba(255, 255, 255, 0.85); + max-width: 750px; + margin: 0 auto; +} + +.privacy-section { + margin-bottom: 3rem; + padding: 2.5rem; + + background: rgba(255, 255, 255, 0.05); + backdrop-filter: blur(18px); + + border-radius: 20px; + border: 1px solid rgba(255, 255, 255, 0.08); + + box-shadow: 0 15px 35px rgba(0, 0, 0, 0.35); + + transition: transform 0.4s ease, + box-shadow 0.4s ease, + background 0.4s ease; +} + +.privacy-section:hover { + transform: translateY(-6px); + box-shadow: 0 25px 50px rgba(124, 58, 237, 0.2); + background: rgba(255, 255, 255, 0.08); +} + + +.section-title { + font-size: 1.6rem; + font-weight: 600; + margin-bottom: 1.2rem; + color: #00d4ff; + border-left: 4px solid #7c3aed; + padding-left: 12px; +} + +.section-content h3 { + margin-top: 1rem; + margin-bottom: 0.5rem; + color: #c084fc; + font-size: 1.1rem; +} + +.section-content p { + color: rgba(255, 255, 255, 0.85); + margin-bottom: 0.8rem; +} + +.section-content ul { + list-style: none; + padding-left: 0; +} + +.section-content li { + position: relative; + padding-left: 22px; + margin-bottom: 0.6rem; + color: rgba(255, 255, 255, 0.8); + transition: color 0.3s ease; +} + +.section-content li::before { + content: "•"; + position: absolute; + left: 0; + color: #00d4ff; + font-weight: bold; +} + +.section-content li:hover { + color: #ffffff; +} + + +.privacy-footer { + margin-top: 4rem; + text-align: center; + padding: 2rem; + + background: rgba(255, 255, 255, 0.05); + backdrop-filter: blur(18px); + + border-radius: 20px; + border: 1px solid rgba(255, 255, 255, 0.08); + + box-shadow: 0 15px 35px rgba(0, 0, 0, 0.35); +} + +.privacy-footer p { + color: rgba(255, 255, 255, 0.75); +} + +.privacy-footer a { + color: #00d4ff; + text-decoration: none; + font-weight: 500; + transition: all 0.3s ease; +} + +.privacy-footer a:hover { + color: #7c3aed; + text-decoration: underline; +} + + +@keyframes fadeUp { + from { + opacity: 0; + transform: translateY(20px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.privacy-header, +.privacy-section, +.privacy-footer { + animation: fadeUp 0.8s ease forwards; +} + +@media (max-width: 768px) { + + .privacy-title { + font-size: 2rem; + } + + .privacy-section { + padding: 1.8rem; + } + + .section-title { + font-size: 1.3rem; + } + + .privacy-intro { + font-size: 0.95rem; + } +} diff --git a/src/components/Legal/PrivacyPolicy.jsx b/src/components/Legal/PrivacyPolicy.jsx new file mode 100644 index 0000000..9e104d5 --- /dev/null +++ b/src/components/Legal/PrivacyPolicy.jsx @@ -0,0 +1,115 @@ +import React from 'react'; +import { Link } from 'react-router-dom'; // Assuming React Router is used +import './PrivacyPolicy.css'; // Custom styles + +const PrivacyPolicy = () => { + return ( +
+
+

Privacy Policy

+

Effective: January 22, 2026

+

+ CryptoHub ("we", "us", "our") is committed to protecting your privacy. This Privacy Policy explains how we collect, use, disclose, and safeguard your information when you visit our website or use our services related to cryptocurrency tracking, wallets, and analytics [web:3]. +

+
+ +
+

1. Information We Collect

+
+

Personal Information

+
    +
  • Account Data: Name, email, username, and password when you create an account [web:1].
  • +
  • KYC Data: Government ID, proof of address for compliance with regulations in crypto services [web:3].
  • +
  • Wallet Addresses: Public blockchain addresses you connect for tracking portfolios.
  • +
  • Payment Information: Details for premium subscriptions or transactions.
  • +
+

Automatically Collected

+
    +
  • IP address, device type, browser info, and usage data via cookies [web:1].
  • +
  • Cryptocurrency transaction history from connected wallets or APIs.
  • +
+

Third-Party Data

+

From blockchain explorers, exchanges, or analytics providers when you link accounts [web:5].

+
+
+ +
+

2. How We Use Your Information

+
+
    +
  • Provide services: Portfolio tracking, alerts, and analytics.
  • +
  • Security: Fraud detection, account protection using encryption (AES-256) [web:3].
  • +
  • Compliance: KYC/AML for regulatory requirements.
  • +
  • Marketing: Personalized emails (with opt-out).
  • +
  • Improvement: Analyze usage to enhance features [web:1].
  • +
+
+
+ +
+

3. Sharing Your Information

+
+
    +
  • Service Providers: Hosting, analytics (e.g., Google Analytics), bound by contracts [web:1].
  • +
  • Legal: To authorities for compliance or subpoenas.
  • +
  • Affiliates: Within CryptoHub for operations.
  • +
  • No selling: We do not sell your data to third parties [web:3].
  • +
+
+
+ +
+

4. Data Security

+
+

We use encryption for data in transit and at rest, access controls, and regular audits. Private keys and seeds are never stored by us—only public data [web:3].

+
+
+ +
+

5. Your Rights

+
+
    +
  • Access, correct, or delete your data via account settings or email support@cryptohub.com.
  • +
  • Opt-out of cookies or marketing.
  • +
  • EEA/UK: GDPR rights including portability [web:1].
  • +
  • California: CCPA rights to know, delete, opt-out of sale (we don't sell) [web:1].
  • +
+
+
+ +
+

6. Cookies and Tracking

+
+

We use essential cookies for functionality and analytics cookies (opt-out available). No non-essential tracking without consent [web:1].

+
+
+ +
+

7. Children's Privacy

+
+

Services not for under 13. We do not knowingly collect data from children [web:1].

+
+
+ +
+

8. International Transfers

+
+

Data processed in the US and other locations with safeguards like Standard Contractual Clauses [web:1].

+
+
+ +
+

9. Changes to Policy

+
+

Updates posted here with notice for material changes.

+
+
+ +
+

Contact: privacy@cryptohub.com | Terms of Service

+
+
+ ); +}; + +export default PrivacyPolicy; diff --git a/src/components/Legal/TermsOfService.css b/src/components/Legal/TermsOfService.css new file mode 100644 index 0000000..d7efc76 --- /dev/null +++ b/src/components/Legal/TermsOfService.css @@ -0,0 +1,94 @@ +/* Wrapper */ +.terms-wrapper { + min-height: 100vh; + background: linear-gradient(135deg, #0f172a, #111827); + display: flex; + justify-content: center; + align-items: center; + padding: 60px 20px; + font-family: 'Inter', sans-serif; + color: #e2e8f0; +} + +/* Glass Card */ +.terms-card { + width: 100%; + max-width: 900px; + padding: 50px; + border-radius: 18px; + background: rgba(255, 255, 255, 0.04); + backdrop-filter: blur(18px); + border: 1px solid rgba(255, 255, 255, 0.08); + box-shadow: 0 10px 40px rgba(0, 0, 0, 0.4); + transition: 0.4s ease; +} + +/* Title */ +.main-title { + text-align: center; + font-size: 2.8rem; + font-weight: 700; + color: #22d3ee; + margin-bottom: 8px; + letter-spacing: -1px; +} + +.last-updated { + text-align: center; + color: #94a3b8; + margin-bottom: 45px; +} + +/* Sections */ +.terms-section { + margin-bottom: 22px; + padding: 22px; + border-radius: 14px; + background: rgba(255, 255, 255, 0.02); + border: 1px solid transparent; + transition: all 0.3s ease; +} + +/* Smooth Professional Hover */ +.terms-section:hover { + background: rgba(34, 211, 238, 0.05); + border: 1px solid rgba(34, 211, 238, 0.3); + transform: translateY(-4px); +} + +/* Section Title */ +.terms-section h2 { + color: #10b981; + margin-bottom: 10px; + font-size: 1.2rem; + font-weight: 600; +} + +/* Content */ +.section-content { + color: #cbd5e1; + line-height: 1.7; + font-size: 0.95rem; +} + +/* List */ +ul { + padding-left: 20px; +} + +li { + margin-bottom: 6px; +} + +/* Email */ +.email { + margin-top: 12px; + color: #22d3ee; + font-weight: 600; + cursor: pointer; + transition: 0.3s; +} + +.email:hover { + color: #10b981; +} diff --git a/src/components/Legal/TermsOfService.jsx b/src/components/Legal/TermsOfService.jsx new file mode 100644 index 0000000..5c77d4c --- /dev/null +++ b/src/components/Legal/TermsOfService.jsx @@ -0,0 +1,78 @@ +import React from "react"; +import "./TermsOfService.css"; + +const TermsOfService = () => { + return ( +
+
+

Terms of Service

+

Last updated: January 2026

+ + {[ + { + title: "1. Introduction", + content: + "Welcome to CryptoHub. By accessing or using our website and services, you agree to follow these Terms of Service. Please read them carefully.", + }, + { + title: "2. Use of Our Services", + content: ( +
    +
  • You must be at least 18 years old to use our platform.
  • +
  • You agree to provide accurate information.
  • +
  • You are responsible for keeping login credentials secure.
  • +
  • You agree not to misuse or harm the platform.
  • +
+ ), + }, + { + title: "3. Account Responsibilities", + content: + "You are fully responsible for all activities under your account. If you notice unauthorized access, contact us immediately.", + }, + { + title: "4. Intellectual Property", + content: + "All content including logos, text, graphics, and design belongs to CryptoHub and is protected by copyright laws.", + }, + { + title: "5. Payments and Subscriptions", + content: + "If you purchase premium services, you agree to pay the listed fees. Payments are non-refundable unless stated otherwise.", + }, + { + title: "6. Limitation of Liability", + content: + "CryptoHub provides cryptocurrency information for educational purposes only. We are not responsible for financial losses.", + }, + { + title: "7. Termination", + content: + "We reserve the right to suspend or terminate accounts that violate these terms.", + }, + { + title: "8. Changes to Terms", + content: + "We may update these Terms anytime. Continued use means you accept updated terms.", + }, + { + title: "9. Contact Us", + content: ( + <> + If you have questions, contact us at: +

support@cryptohub.com

+ + ), + }, + ].map((section, index) => ( +
+

{section.title}

+
{section.content}
+
+ ))} +
+
+ ); +}; + +export default TermsOfService; diff --git a/src/components/MarketFilters.css b/src/components/MarketFilters.css deleted file mode 100644 index f9a3185..0000000 --- a/src/components/MarketFilters.css +++ /dev/null @@ -1,31 +0,0 @@ -.market-filters { - display: flex; - align-items: center; - gap: 10px; - margin: 20px 0; - flex-wrap: wrap; -} - -.filter-label { - font-size: 14px; - color: #aaa; -} - -.filter-btn { - padding: 6px 14px; - border-radius: 20px; - border: 1px solid rgba(255, 255, 255, 0.2); - background: transparent; - color: #fff; - cursor: pointer; - transition: 0.2s ease; -} - -.filter-btn:hover { - background: rgba(255, 255, 255, 0.08); -} - -.filter-btn.active { - background: #7927ff; - border-color: #7927ff; -} diff --git a/src/components/MarketFilters.jsx b/src/components/MarketFilters.jsx deleted file mode 100644 index c1e5ea0..0000000 --- a/src/components/MarketFilters.jsx +++ /dev/null @@ -1,69 +0,0 @@ -import React from 'react' -import { useContext } from "react"; -import { CoinContext } from "../context/CoinContext"; -import "./MarketFilters.css"; - -const MarketFilters = () => { - const {selectedFilters, setSelectedFilters} = useContext(CoinContext); - - const toggleFilter = (filter) => { - // Clicking "all" - if (filter === "all") { - setSelectedFilters(["all"]); - return; - } - - setSelectedFilters((prev) => { - let updated = [...prev]; - - // Remove "all" if another filter is selected - updated = updated.filter((f) => f !== "all"); - - if (updated.includes(filter)) { - // Remove filter - updated = updated.filter((f) => f !== filter); - } else { - // Add filter - updated.push(filter); - } - - // If nothing selected, reset to "all" - if (updated.length === 0) { - return ["all"]; - } - - return updated; - }); -}; - - return ( -
- Filters: - - - - -
- ) -} - -export default MarketFilters \ No newline at end of file diff --git a/src/components/Navbar.css b/src/components/Navbar.css deleted file mode 100644 index 0b3ccdf..0000000 --- a/src/components/Navbar.css +++ /dev/null @@ -1,1082 +0,0 @@ -/* =================================== - RESPONSIVE NAVBAR CSS WITH MOBILE DROPDOWN MENU - =================================== */ - -/* Base Reset */ -* { - margin: 0; - padding: 0; - box-sizing: border-box; -} - -:root { - --primary-color: #b026ff; - --secondary-color: #00f3ff; - --bg-dark: #0a0a0f; - --text-light: #ffffff; - --border-color: rgba(255, 255, 255, 0.1); - --transition-smooth: cubic-bezier(0.4, 0, 0.2, 1); -} - -/* Navbar Container */ -.navbar { - position: sticky; - top: 0; - left: 0; - right: 0; - width: 100%; - background: rgba(10, 10, 15, 0.95); - backdrop-filter: blur(20px) saturate(180%); - -webkit-backdrop-filter: blur(20px) saturate(180%); - border-bottom: 1px solid var(--border-color); - padding: 1rem 2%; - z-index: 1000; - transition: all 0.4s var(--transition-smooth); -} - -.navbar.scrolled { - padding: 0.75rem 2%; - background: rgba(10, 10, 15, 0.98); - box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3); -} - -/* Navbar Content Container */ -.navbar-content { - display: flex; - justify-content: space-between; - align-items: center; - gap: 1rem; - width: 100%; - max-width: 1400px; - margin: 0 auto; - padding: 0 2%; -} - -/* Desktop layout - grid for logo, menu, actions */ -@media (min-width: 768px) { - .navbar-content { - display: grid; - grid-template-columns: auto 1fr auto; /* logo | menu | actions */ - gap: 3px !important; - } -} - - -/* Logo/Brand */ -.navbar-logo { - display: flex; - align-items: center; - gap: 0.875rem; - text-decoration: none; - flex-shrink: 0; - font-weight: 800; - font-size: 1.35rem; - background: linear-gradient(135deg, #ffffff 0%, #00f3ff 50%, #b026ff 100%); - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - background-clip: text; - transition: all 0.4s var(--transition-smooth); - position: relative; - padding: 0.5rem 0; -} - -.navbar-logo:hover { - transform: translateY(-2px); - opacity: 0.9; -} - -.navbar-logo::after { - content: ''; - position: absolute; - bottom: 0; - left: 0; - width: 0; - height: 2px; - background: linear-gradient(90deg, #00f3ff, #b026ff); - transition: width 0.4s var(--transition-smooth); - border-radius: 2px; -} - -.navbar-logo:hover::after { - width: 100%; -} - -.navbar-logo-icon { - width: 44px; - height: 44px; - border-radius: 12px; - background: linear-gradient(135deg, - rgba(176, 38, 255, 0.2) 0%, - rgba(0, 243, 255, 0.15) 50%, - rgba(176, 38, 255, 0.1) 100%); - border: 1.5px solid rgba(176, 38, 255, 0.3); - display: flex; - align-items: center; - justify-content: center; - transition: all 0.4s var(--transition-smooth); - position: relative; - overflow: hidden; -} - -.navbar-logo-icon::before { - content: ''; - position: absolute; - top: 0; - left: -100%; - width: 100%; - height: 100%; - background: linear-gradient(90deg, - transparent, - rgba(255, 255, 255, 0.1), - transparent); - transition: left 0.6s var(--transition-smooth); -} - -.navbar-logo:hover .navbar-logo-icon { - border-color: rgba(0, 243, 255, 0.6); - box-shadow: 0 8px 32px rgba(0, 243, 255, 0.25), - 0 0 0 1px rgba(176, 38, 255, 0.2); - transform: rotate(5deg) scale(1.05); -} - -.navbar-logo:hover .navbar-logo-icon::before { - left: 100%; -} - -.logo-img { - width: 70%; - height: 70%; - object-fit: contain; - filter: drop-shadow(0 0 6px rgba(0, 243, 255, 0.8)) - drop-shadow(0 0 12px rgba(176, 38, 255, 0.4)); - transition: all 0.4s var(--transition-smooth); -} - -.navbar-logo:hover .logo-img { - transform: scale(1.1); - filter: drop-shadow(0 0 8px rgba(0, 243, 255, 1)) - drop-shadow(0 0 16px rgba(176, 38, 255, 0.6)); -} - -/* Desktop Navigation Menu */ -.navbar-menu { - display: none; - flex-direction: column; - list-style: none; - gap: 0; - padding: 0; - margin: 0; -} - -@media (min-width: 768px) { - .navbar-menu { - display: flex; - flex-direction: row; - flex: 1; - max-width: 500px; - margin: 0 auto; - gap: 0.5rem; - justify-content: center; - align-items: center; - } -} - -.navbar-item { - display: flex; - position: relative; -} - -.navbar-link { - display: flex; - align-items: center; - padding: 0.625rem 1.125rem; - color: rgba(255, 255, 255, 0.75); - text-decoration: none; - font-weight: 500; - font-size: 0.95rem; - letter-spacing: 0.3px; - transition: all 0.3s var(--transition-smooth); - position: relative; - white-space: nowrap; - border-radius: 10px; - overflow: hidden; - z-index: 1; -} - -.navbar-link::before { - content: ''; - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - background: linear-gradient(135deg, - rgba(176, 38, 255, 0.1) 0%, - rgba(0, 243, 255, 0.05) 100%); - border-radius: 10px; - transform: scaleX(0); - transform-origin: right; - transition: transform 0.4s var(--transition-smooth); - z-index: -1; -} - -.navbar-link:hover { - color: #ffffff; - transform: translateY(-2px); -} - -.navbar-link:hover::before { - transform: scaleX(1); - transform-origin: left; -} - -.navbar-link::after { - content: ''; - position: absolute; - bottom: 0; - left: 50%; - transform: translateX(-50%); - width: 0; - height: 2px; - background: linear-gradient(90deg, #00f3ff, #b026ff); - border-radius: 2px; - transition: all 0.4s var(--transition-smooth); - opacity: 0; -} - -.navbar-link:hover::after { - width: 70%; - opacity: 1; -} - -.navbar-link.active { - color: #ffffff; - font-weight: 600; - background: linear-gradient(135deg, - rgba(176, 38, 255, 0.15) 0%, - rgba(0, 243, 255, 0.1) 100%); - box-shadow: 0 4px 20px rgba(176, 38, 255, 0.15), - inset 0 1px 0 rgba(255, 255, 255, 0.1); -} - -.navbar-link.active::after { - width: 70%; - opacity: 1; -} - -/* Actions Container */ -.navbar-actions { - display: flex; - align-items: center; - gap: 1.5rem; - flex-shrink: 0; -} - -/* Desktop Auth */ -.desktop-auth { - display: flex; - align-items: center; - gap: 1rem; -} - -/* Mobile Only Elements */ -.mobile-only { - display: none; -} - -/* Action Buttons */ -.navbar-btn { - padding: 0.75rem 1.5rem; - border-radius: 12px; - font-weight: 600; - font-size: 0.95rem; - text-decoration: none; - transition: all 0.4s var(--transition-smooth); - cursor: pointer; - border: none; - white-space: nowrap; - display: inline-block; - text-align: center; - min-width: fit-content; - position: relative; - overflow: hidden; - letter-spacing: 0.5px; - z-index: 1; -} - -.navbar-btn::before { - content: ''; - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - background: inherit; - border-radius: inherit; - z-index: -1; - transition: transform 0.4s var(--transition-smooth); -} - -.navbar-btn:hover::before { - transform: scale(1.05); -} - -.navbar-btn-login { - color: #ffffff; - background: transparent; - border: 2px solid rgba(255, 255, 255, 0.2); - padding: 0.75rem 1.8rem; - position: relative; - overflow: hidden; -} - -.navbar-btn-login::after { - content: ''; - position: absolute; - top: 0; - left: -100%; - width: 100%; - height: 100%; - background: linear-gradient(90deg, - transparent, - rgba(255, 255, 255, 0.1), - transparent); - transition: left 0.6s var(--transition-smooth); -} - -.navbar-btn-login:hover { - background: rgba(255, 255, 255, 0.08); - border-color: rgba(255, 255, 255, 0.4); - transform: translateY(-2px); - box-shadow: 0 8px 32px rgba(255, 255, 255, 0.1); -} - -.navbar-btn-login:hover::after { - left: 100%; -} - -.navbar-btn-signup { - background: linear-gradient(135deg, #00f3ff 0%, #b026ff 100%); - color: #000000; - padding: 0.75rem 2rem; - font-weight: 700; - box-shadow: 0 4px 20px rgba(0, 243, 255, 0.3), - 0 0 0 1px rgba(176, 38, 255, 0.2); -} - -.navbar-btn-signup:hover { - transform: translateY(-3px) scale(1.02); - box-shadow: 0 12px 40px rgba(0, 243, 255, 0.4), - 0 0 0 1px rgba(176, 38, 255, 0.3), - 0 0 30px rgba(176, 38, 255, 0.2); - background: linear-gradient(135deg, #00f3ff 10%, #b026ff 90%); -} - -/* User Menu */ -.user-menu { - display: flex; - align-items: center; - gap: 1rem; - background: rgba(255, 255, 255, 0.05); - padding: 10px 16px; - border-radius: 14px; - border: 1px solid rgba(255, 255, 255, 0.08); - flex-shrink: 0; - backdrop-filter: blur(10px); - transition: all 0.3s var(--transition-smooth); -} - -.user-menu:hover { - background: rgba(255, 255, 255, 0.08); - border-color: rgba(255, 255, 255, 0.15); - transform: translateY(-1px); - box-shadow: 0 8px 24px rgba(0, 0, 0, 0.2); -} - -.user-email { - font-size: 0.85rem; - color: rgba(255, 255, 255, 0.85); - max-width: 150px; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - font-weight: 500; - flex-shrink: 1; -} - -.icon-btn { - background: none; - border: none; - color: rgba(255, 255, 255, 0.7); - cursor: pointer; - display: flex; - align-items: center; - justify-content: center; - transition: all 0.3s var(--transition-smooth); - font-size: 1.1rem; - padding: 8px; - border-radius: 8px; - flex-shrink: 0; - position: relative; - overflow: hidden; -} - -.icon-btn::before { - content: ''; - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - background: rgba(255, 255, 255, 0.1); - border-radius: 8px; - transform: scale(0); - transition: transform 0.3s var(--transition-smooth); -} - -.icon-btn:hover { - color: #ffffff; - transform: translateY(-1px) rotate(5deg); -} - -.icon-btn:hover::before { - transform: scale(1); -} - -.logout-btn { - padding: 8px 16px; - border-radius: 10px; - font-size: 0.85rem; - font-weight: 600; - background: linear-gradient(135deg, - rgba(239, 68, 68, 0.15) 0%, - rgba(239, 68, 68, 0.1) 100%); - border: 1.5px solid rgba(239, 68, 68, 0.25); - color: #ef4444; - cursor: pointer; - transition: all 0.3s var(--transition-smooth); - flex-shrink: 0; - white-space: nowrap; - position: relative; - overflow: hidden; -} - -.logout-btn::before { - content: ''; - position: absolute; - top: 0; - left: -100%; - width: 100%; - height: 100%; - background: linear-gradient(90deg, - transparent, - rgba(239, 68, 68, 0.1), - transparent); - transition: left 0.6s var(--transition-smooth); -} - -.logout-btn:hover { - background: linear-gradient(135deg, - rgba(239, 68, 68, 0.2) 0%, - rgba(239, 68, 68, 0.15) 100%); - border-color: rgba(239, 68, 68, 0.4); - transform: translateY(-2px); - box-shadow: 0 8px 20px rgba(239, 68, 68, 0.15); -} - -.logout-btn:hover::before { - left: 100%; -} - -/* Mobile Menu Toggle Button */ -.navbar-toggle { - display: none; - background: none; - border: none; - cursor: pointer; - padding: 10px; - border-radius: 10px; - transition: all 0.3s var(--transition-smooth); - position: relative; - overflow: hidden; - color: #ffffff; -} - -.navbar-toggle::before { - content: ''; - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - background: rgba(255, 255, 255, 0.1); - border-radius: 10px; - transform: scale(0); - transition: transform 0.3s var(--transition-smooth); -} - -.navbar-toggle:hover::before { - transform: scale(1); -} - -.navbar-toggle:hover { - color: var(--secondary-color); -} - -/* Mobile Menu Dropdown */ -.mobile-menu { - display: none; - position: absolute; - top: 100%; - left: 0; - right: 0; - background: rgba(10, 10, 15, 0.98); - backdrop-filter: blur(10px); - -webkit-backdrop-filter: blur(10px); - border-bottom: 1px solid var(--border-color); - padding: 1rem 0; - max-height: 0; - overflow: hidden; - opacity: 0; - transition: all 0.3s ease; -} - -.mobile-menu.open { - display: block; - max-height: 500px; - opacity: 1; -} - -.mobile-nav-links { - list-style: none; - padding: 0; - margin: 0; -} - -.mobile-nav-item { - border-bottom: 1px solid rgba(255, 255, 255, 0.05); -} - -.mobile-nav-link { - display: block; - padding: 1rem 2rem; - color: rgba(255, 255, 255, 0.7); - text-decoration: none; - font-weight: 500; - font-size: 0.95rem; - transition: all 0.3s ease; -} - -.mobile-nav-link:hover, -.mobile-nav-link.active { - color: #ffffff; - background: rgba(255, 255, 255, 0.05); - padding-left: 2.5rem; -} - -.mobile-controls { - padding: 1rem 2rem; - border-top: 1px solid rgba(255, 255, 255, 0.05); -} - -.mobile-control-item { - display: flex; - align-items: center; - justify-content: space-between; - padding: 0.75rem 0; - color: rgba(255, 255, 255, 0.7); - cursor: pointer; -} - -.toggle-track { - width: 50px; - height: 26px; - background: rgba(255, 255, 255, 0.1); - border-radius: 13px; - position: relative; - transition: all 0.3s ease; - border: 1px solid rgba(255, 255, 255, 0.2); -} - -.toggle-track.dark { - background: linear-gradient(135deg, #00f3ff 0%, #b026ff 100%); -} - -.toggle-thumb { - position: absolute; - top: 2px; - left: 2px; - width: 20px; - height: 20px; - background: white; - border-radius: 50%; - transition: all 0.3s ease; - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); -} - -.toggle-track.dark .toggle-thumb { - transform: translateX(24px); -} - -.mobile-auth { - padding: 1rem 2rem; - border-top: 1px solid rgba(255, 255, 255, 0.05); -} - -.mobile-change-password { - display: flex; - align-items: center; - gap: 0.5rem; - padding: 0.75rem 1rem; - margin-bottom: 0.75rem; - color: rgba(255, 255, 255, 0.7); - text-decoration: none; - border-radius: 8px; - background: rgba(255, 255, 255, 0.05); - border: 1px solid rgba(255, 255, 255, 0.1); - transition: all 0.3s ease; -} - -.mobile-change-password:hover { - background: rgba(255, 255, 255, 0.1); - color: #ffffff; -} - -.mobile-auth-buttons { - display: flex; - flex-direction: row; - gap: 0.75rem; -} - -.mobile-auth-buttons .login-btn, -.mobile-auth-buttons .signup-btn { - padding: 0.75rem 1.5rem; - border-radius: 8px; - font-weight: 600; - font-size: 0.95rem; - border: none; - cursor: pointer; - transition: all 0.3s ease; - text-align: center; - text-decoration: none; - display: block; - flex: 1; -} - -.mobile-auth-buttons .login-btn { - background: transparent; - border: 2px solid rgba(255, 255, 255, 0.25); - color: #ffffff; -} - -.mobile-auth-buttons .login-btn:hover { - background: rgba(255, 255, 255, 0.1); - border-color: rgba(255, 255, 255, 0.6); -} - -.mobile-auth-buttons .signup-btn { - background: linear-gradient(135deg, #00f3ff 0%, #b026ff 100%); - color: #000000; -} - -.mobile-auth-buttons .signup-btn:hover { - transform: translateY(-2px); - box-shadow: 0 8px 25px rgba(0, 243, 255, 0.5); -} - -.full-width { - width: 100%; -} - -/* Visibility Classes */ -.desktop-only { - display: none; -} - -.mobile-only { - display: flex; -} - -/* =================================== - MOBILE DROPDOWN MENU - =================================== */ - -.mobile-menu { - position: fixed; - top: 80px; - left: 0; - right: 0; - background: linear-gradient(135deg, - rgba(10, 10, 15, 0.98) 0%, - rgba(20, 20, 30, 0.95) 100%); - backdrop-filter: blur(30px) saturate(180%); - -webkit-backdrop-filter: blur(30px) saturate(180%); - border-top: 1px solid rgba(176, 38, 255, 0.2); - border-bottom: 1px solid rgba(176, 38, 255, 0.2); - z-index: 999; - max-height: 0; - overflow: hidden; - transition: max-height 0.5s var(--transition-smooth); - box-shadow: 0 10px 50px rgba(0, 0, 0, 0.5); -} - -.mobile-menu.open { - max-height: calc(100vh - 80px); - overflow-y: auto; -} - -.mobile-nav-links { - list-style: none; - padding: 1.5rem; - margin: 0; - display: flex; - flex-direction: column; - gap: 0.5rem; -} - -.mobile-nav-item { - display: flex; -} - -.mobile-nav-link { - display: flex; - align-items: center; - padding: 1rem 1.25rem; - color: rgba(255, 255, 255, 0.85); - text-decoration: none; - font-weight: 500; - font-size: 1rem; - border-radius: 12px; - transition: all 0.3s var(--transition-smooth); - width: 100%; - position: relative; - overflow: hidden; -} - -.mobile-nav-link::before { - content: ''; - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - background: linear-gradient(90deg, - rgba(176, 38, 255, 0.1) 0%, - transparent 100%); - transform: translateX(-100%); - transition: transform 0.4s var(--transition-smooth); - z-index: -1; -} - -.mobile-nav-link:hover, -.mobile-nav-link.active { - color: #ffffff; - background: linear-gradient(90deg, - rgba(176, 38, 255, 0.15) 0%, - rgba(0, 243, 255, 0.1) 100%); -} - -.mobile-nav-link:hover::before { - transform: translateX(0); -} - -.mobile-nav-link.active { - border-left: 4px solid var(--primary-color); - font-weight: 600; -} - -.mobile-controls { - padding: 1rem 1.5rem; - border-top: 1px solid rgba(255, 255, 255, 0.1); - border-bottom: 1px solid rgba(255, 255, 255, 0.1); -} - -.mobile-control-item { - display: flex; - align-items: center; - justify-content: space-between; - padding: 1rem; - color: rgba(255, 255, 255, 0.9); - font-weight: 500; - cursor: pointer; - border-radius: 10px; - transition: all 0.3s var(--transition-smooth); -} - -.mobile-control-item:hover { - background: rgba(255, 255, 255, 0.05); -} - -.toggle-track { - width: 50px; - height: 28px; - border-radius: 14px; - position: relative; - cursor: pointer; - transition: all 0.3s var(--transition-smooth); -} - -.toggle-track.light { - background: linear-gradient(135deg, #e5e7eb 0%, #d1d5db 100%); -} - -.toggle-track.dark { - background: linear-gradient(135deg, #374151 0%, #1f2937 100%); -} - -.toggle-thumb { - position: absolute; - top: 2px; - left: 2px; - width: 24px; - height: 24px; - border-radius: 50%; - background: #ffffff; - transition: transform 0.3s var(--transition-smooth); - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); -} - -.toggle-track.dark .toggle-thumb { - transform: translateX(22px); -} - -.mobile-auth { - padding: 1.5rem; - display: flex; - flex-direction: column; - gap: 1rem; -} - -.mobile-change-password { - display: flex; - align-items: center; - gap: 0.75rem; - padding: 1rem; - color: rgba(255, 255, 255, 0.9); - text-decoration: none; - border-radius: 10px; - background: rgba(255, 255, 255, 0.05); - transition: all 0.3s var(--transition-smooth); -} - -.mobile-change-password:hover { - background: rgba(255, 255, 255, 0.1); - transform: translateY(-2px); -} - -.mobile-auth-buttons { - display: flex; - flex-direction: column; - gap: 1rem; -} - -.login-btn, .signup-btn { - width: 100%; - padding: 1rem; - border-radius: 12px; - font-weight: 600; - font-size: 1rem; - border: none; - cursor: pointer; - transition: all 0.4s var(--transition-smooth); - position: relative; - overflow: hidden; - z-index: 1; -} - -.login-btn { - background: linear-gradient(135deg, - rgba(255, 255, 255, 0.05) 0%, - rgba(255, 255, 255, 0.02) 100%); - border: 2px solid rgba(255, 255, 255, 0.15); - color: #ffffff; -} - -.signup-btn { - background: linear-gradient(135deg, #00f3ff 0%, #b026ff 100%); - color: #000000; - font-weight: 700; -} - -.login-btn:hover, .signup-btn:hover { - transform: translateY(-2px); - box-shadow: 0 8px 32px rgba(0, 243, 255, 0.2); -} - -.full-width { - width: 100%; -} - -/* =================================== - RESPONSIVE BREAKPOINTS - =================================== */ - -/* Desktop Only */ -.desktop-only { - display: none; -} - -@media (min-width: 768px) { - /* Show desktop elements, hide mobile elements */ - .desktop-only { - display: flex; - } - - .mobile-only { - display: none; - } - - .mobile-menu { - display: none !important; - } - - .navbar-menu { - display: flex; - } - - .mobile-only { - display: none !important; - } - - .mobile-menu { - display: none; - } -} - -/* Mobile Only */ -@media (max-width: 767px) { - .mobile-only { - display: flex; - } - - .desktop-only { - display: none !important; - } - - .navbar-toggle { - display: flex; - } - - .navbar-menu { - display: none; - } - - .desktop-auth { - display: none; - } -} - -/* Tablet */ -@media (min-width: 768px) and (max-width: 1023px) { - .navbar-content { - gap: 1.5rem; - } - - .navbar-menu { - gap: 0.5rem; - margin: 0 auto; - max-width: 500px; - } - - .navbar-link { - font-size: 0.9rem; - padding: 0.5rem 0.75rem; - } - - .navbar-actions { - gap: 0.875rem; - } - - .navbar-btn { - padding: 0.6rem 1rem; - font-size: 0.85rem; - } - - .navbar-btn-login { - padding: 0.6rem 1.25rem; - } - - .navbar-btn-signup { - padding: 0.6rem 1.5rem; - } - - .user-email { - max-width: 120px; - font-size: 0.8rem; - } - - .user-menu { - padding: 8px 12px; - gap: 0.75rem; - } -} - -/* Desktop */ -@media (min-width: 1024px) { - .navbar-content { - gap: 2.5rem; - max-width: 1400px; - margin: 0 auto; - padding: 0 3%; - } - - .navbar-menu { - max-width: 600px; - margin: 0 auto; - gap: 1rem; - justify-content: center; - } - - .navbar-link { - padding: 0.75rem 1.25rem; - font-size: 1rem; - } - - .navbar-logo { - font-size: 1.5rem; - } - - .navbar-actions { - gap: 1.5rem; - } - - .navbar-btn { - padding: 0.75rem 1.5rem; - font-size: 0.95rem; - } - - .navbar-btn-login { - padding: 0.75rem 1.75rem; - } - - .navbar-btn-signup { - padding: 0.75rem 2rem; - } - - .user-email { - max-width: 180px; - font-size: 0.9rem; - } -} - -/* Accessibility */ -@media (prefers-reduced-motion: reduce) { - * { - animation-duration: 0.01ms !important; - animation-iteration-count: 1 !important; - transition-duration: 0.01ms !important; - } -} - -/* Touch devices */ -@media (hover: none) and (pointer: coarse) { - .navbar-link, - .navbar-btn, - .icon-btn, - .mobile-nav-link, - .login-btn, - .signup-btn { - min-height: 44px; - min-width: 44px; - } -} \ No newline at end of file diff --git a/src/components/Navbar.jsx b/src/components/Navbar.jsx deleted file mode 100644 index 8c2469c..0000000 --- a/src/components/Navbar.jsx +++ /dev/null @@ -1,143 +0,0 @@ -import React, { useState, useEffect, useCallback } from "react"; -import { useAuth } from "../context/AuthContext"; -import { useTheme } from "../context/ThemeContext"; -import { Link, useNavigate, useLocation } from "react-router-dom"; -import { FiLock } from "react-icons/fi"; -import "./Navbar.css"; - -function Navbar() { - const { currentUser, logout, isEmailProvider } = useAuth(); - const { theme } = useTheme(); - const navigate = useNavigate(); - const location = useLocation(); - const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false); - const [scrolled, setScrolled] = useState(false); - - const isDashboardPage = location.pathname === "/dashboard"; - - // Handle scroll effect for navbar - useEffect(() => { - const handleScroll = () => { - setScrolled(window.scrollY > 20); - }; - window.addEventListener("scroll", handleScroll); - return () => window.removeEventListener("scroll", handleScroll); - }, []); - - const handleLogout = useCallback(async () => { - try { - await logout(); - navigate("/"); - setIsMobileMenuOpen(false); - } catch (error) { - console.error("Failed to logout:", error); - } - }, [logout, navigate]); - - const toggleMobileMenu = () => { - setIsMobileMenuOpen(!isMobileMenuOpen); - }; - - const closeMobileMenu = () => { - setIsMobileMenuOpen(false); - }; - - const navLinks = [ - { to: "/", label: "Home" }, - { to: "/pricing", label: "Pricing" }, - { to: "/blog", label: "Insights" }, - { to: "/features", label: "Features" }, - { to: "/contributors", label: "Contributors" }, - ]; - - const authenticatedNavLinks = [ - ...navLinks, - { to: "/dashboard", label: "Dashboard" }, - { to: "/leaderboard", label: "Leaderboard" }, - ]; - - return ( - - ); -} - -export default Navbar; \ No newline at end of file diff --git a/src/components/PrivateRoute.jsx b/src/components/PrivateRoute.jsx deleted file mode 100644 index 81814b3..0000000 --- a/src/components/PrivateRoute.jsx +++ /dev/null @@ -1,16 +0,0 @@ -import React from "react"; -import { Navigate } from "react-router-dom"; -import { useAuth } from "../context/AuthContext"; - -const PrivateRoute = ({ children }) => { - const { currentUser, loading } = useAuth(); - - - if (loading) { - return children; - } - - return currentUser ? children : ; -}; - -export default PrivateRoute; diff --git a/src/components/Sections/About.css b/src/components/Sections/About.css new file mode 100644 index 0000000..5085f11 --- /dev/null +++ b/src/components/Sections/About.css @@ -0,0 +1,192 @@ +/* Animated gradient underline for headings */ +.about-title-underline { + display: inline-block; + position: relative; +} +.about-title-underline::after { + content: ''; + display: block; + position: absolute; + left: 0; + bottom: -6px; + width: 100%; + height: 4px; + border-radius: 2px; + background: linear-gradient(90deg, #38bdf8 0%, #f472b6 100%); + opacity: 0.85; + animation: underline-move 2.5s linear infinite alternate; +} + +@keyframes underline-move { + 0% { + background-position: 0% 50%; + } + 100% { + background-position: 100% 50%; + } +} + +/* Fade-in animation for About section */ +.fade-in { + opacity: 0; + animation: fadeInAbout 1.1s ease-in forwards; +} +@keyframes fadeInAbout { + from { opacity: 0; transform: translateY(32px); } + to { opacity: 1; transform: none; } +} + +.about-section { + max-width: 900px; + margin: 48px auto; + padding: 44px 28px 36px 28px; + background: rgba(24, 28, 46, 0.85); + border-radius: 22px; + box-shadow: 0 8px 40px 0 rgba(36, 40, 80, 0.22), 0 2px 12px 0 rgba(80, 200, 255, 0.10); + font-family: 'Segoe UI', Arial, sans-serif; + color: #eaf0fa; + position: relative; + overflow: hidden; + border: 1.5px solid rgba(61, 90, 180, 0.18); + backdrop-filter: blur(12px); + transition: box-shadow 0.3s, border 0.3s; +} +.about-section:hover { + box-shadow: 0 12px 48px 0 rgba(36, 40, 80, 0.28), 0 4px 18px 0 rgba(80, 200, 255, 0.16); + border: 1.5px solid #38bdf8; +} +.about-intro { + font-size: 1.18rem; + color: #e0e7ef; + margin-bottom: 22px; + z-index: 1; + position: relative; + line-height: 1.75; + text-align: justify; +} + +/* Features grid and cards */ +.about-features-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); + gap: 22px 18px; + margin: 32px 0 18px 0; + z-index: 1; + position: relative; +} +.about-feature-card { + background: rgba(36, 40, 80, 0.38); + border-radius: 14px; + padding: 18px 18px 18px 54px; + color: #e0e7ef; + font-size: 1.08rem; + font-weight: 500; + box-shadow: 0 2px 12px 0 rgba(56, 189, 248, 0.08); + position: relative; + transition: background 0.3s, transform 0.18s, box-shadow 0.3s; + cursor: pointer; + border: 1.5px solid rgba(56, 189, 248, 0.10); + overflow: hidden; +} +.about-feature-card:hover { + background: linear-gradient(90deg, #38bdf8 0%, #818cf8 100%); + color: #fff; + transform: translateY(-4px) scale(1.03); + box-shadow: 0 6px 24px 0 rgba(56, 189, 248, 0.18); + border: 1.5px solid #818cf8; +} +.about-feature-icon { + position: absolute; + left: 18px; + top: 50%; + transform: translateY(-50%) scale(1.18); + font-size: 1.5rem; + filter: drop-shadow(0 2px 8px #38bdf8aa); + transition: transform 0.3s; +} +.about-feature-card:hover .about-feature-icon { + transform: translateY(-50%) scale(1.28) rotate(-8deg); +} +.about-section::before { + content: ''; + position: absolute; + top: -60px; + right: -60px; + width: 180px; + height: 180px; + background: radial-gradient(circle, #3b82f6 0%, #232946 80%); + opacity: 0.18; + z-index: 0; +} +.about-section h1 { + font-size: 2.7rem; + margin-bottom: 18px; + color: #60a5fa; + letter-spacing: 1px; + font-weight: 700; + z-index: 1; + position: relative; + text-shadow: 0 2px 12px rgba(80,200,255,0.12); +} +.about-section h2 { + font-size: 1.45rem; + margin-top: 32px; + color: #38bdf8; + font-weight: 600; + z-index: 1; + position: relative; + letter-spacing: 0.5px; +} +.about-section p { + font-size: 1.13rem; + color: #e0e7ef; + margin-bottom: 18px; + z-index: 1; + position: relative; + line-height: 1.7; +} +.about-section ul { + margin-left: 28px; + color: #e0e7ef; + z-index: 1; + position: relative; + padding-left: 0; +} +.about-section li { + margin-bottom: 12px; + font-size: 1.08rem; + background: linear-gradient(90deg, #38bdf8 0%, #818cf8 100%); + background-clip: text; + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + text-fill-color: transparent; + font-weight: 500; + transition: background 0.3s, transform 0.2s; + cursor: pointer; + border-radius: 6px; + padding-left: 0.2em; +} +.about-section li:hover { + background: linear-gradient(90deg, #f472b6 0%, #38bdf8 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + text-fill-color: transparent; + transform: translateX(6px) scale(1.04); +} + +@media (max-width: 900px) { + .about-section { + padding: 18px 4vw 18px 4vw; + max-width: 99vw; + } + .about-features-grid { + grid-template-columns: 1fr; + gap: 16px; + } + .about-section h1 { + font-size: 2rem; + } + .about-section h2 { + font-size: 1.1rem; + } +} diff --git a/src/components/Sections/About.jsx b/src/components/Sections/About.jsx new file mode 100644 index 0000000..aadc2d3 --- /dev/null +++ b/src/components/Sections/About.jsx @@ -0,0 +1,79 @@ +import React from "react"; +import "./About.css"; + +const About = () => { + return ( +
+ +

About CryptoHub

+ +

+ CryptoHub is a comprehensive platform designed to provide users with + real-time cryptocurrency market data, insightful analytics, and + educational resources. Our goal is to empower both beginners and + experienced traders with reliable tools to make informed decisions + in the fast-moving digital asset ecosystem. +

+ +

Our Mission

+

+ Our mission is to simplify crypto market analysis by combining + real-time data, intuitive design, and powerful visualization tools. + We strive to make cryptocurrency accessible, transparent, and + user-friendly for everyone. +

+ +

Key Features

+
+
+ 💹 + Live cryptocurrency prices and market overviews +
+ +
+ 📈 + Interactive charts and advanced analytics +
+ +
+ 📰 + Latest crypto news and updates +
+ +
+ 📚 + Educational blogs and learning resources +
+ +
+ 🏆 + Leaderboard and community engagement tools +
+ +
+ 🔒 + Secure authentication and user management +
+
+ +

Open Source & Community

+

+ CryptoHub is proudly open-source and welcomes contributors worldwide. + We believe in collaboration, transparency, and continuous improvement. + Community contributions help us evolve the platform and build a + stronger crypto ecosystem together. +

+ +

Our Vision

+

+ We envision CryptoHub as a trusted, all-in-one crypto intelligence + platform that bridges the gap between data complexity and user clarity. + Whether you're just starting your crypto journey or actively trading, + CryptoHub is built to support your growth. +

+ +
+ ); +}; + +export default About; diff --git a/src/components/Blog.css b/src/components/Sections/Blog.css similarity index 92% rename from src/components/Blog.css rename to src/components/Sections/Blog.css index 313db6f..60cca53 100644 --- a/src/components/Blog.css +++ b/src/components/Sections/Blog.css @@ -60,14 +60,7 @@ background-clip: text; } -.hero-subtitle { - font-size: 1.2rem; - color: #a0a0a0; - line-height: 1.6; - max-width: 700px; - margin: 0 auto; - font-weight: 400; -} + /* ========== FEATURED SECTION ========== */ .featured-section { @@ -106,7 +99,7 @@ border-color: #4559DC; transform: translateY(-10px); background: rgba(255, 255, 255, 0.03); - box-shadow: + box-shadow: 0 25px 50px rgba(0, 0, 0, 0.4), 0 0 0 1px rgba(69, 89, 220, 0.1); } @@ -444,9 +437,9 @@ /* ========== NEWSLETTER ========== */ .newsletter-section { - background: linear-gradient(135deg, - rgba(69, 89, 220, 0.05) 0%, - rgba(0, 243, 255, 0.03) 100%); + background: linear-gradient(135deg, + rgba(69, 89, 220, 0.05) 0%, + rgba(0, 243, 255, 0.03) 100%); border: 1px solid rgba(69, 89, 220, 0.2); border-radius: 24px; padding: 3rem; @@ -560,7 +553,7 @@ grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 1.5rem; } - + .featured-grid { grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 2rem; @@ -571,30 +564,30 @@ .blog-container { padding: 1.5rem 1rem; } - + .glassnode-hero { padding: 3rem 0 4rem; } - + .hero-title { font-size: 2.5rem; } - + .featured-grid, .posts-grid { grid-template-columns: 1fr; gap: 1.5rem; } - + .card-image-container, .post-image-container { height: 220px; } - + .newsletter-section { padding: 2rem; } - + .newsletter-container h3 { font-size: 1.8rem; } @@ -604,44 +597,94 @@ .hero-title { font-size: 2rem; } - + .hero-subtitle { font-size: 1rem; } - + .section-title { font-size: 1.5rem; } - + .card-title, .post-title { font-size: 1.1rem; } } -/* Pagination Styles for 30 Cards */ -.pagination { +/* Pagination Styles */ +.pagination-container { display: flex; justify-content: center; align-items: center; - gap: 1rem; + gap: 0.5rem; margin-top: 4rem; padding-top: 3rem; border-top: 1px solid rgba(255, 255, 255, 0.1); } .pagination-btn { - padding: 0.8rem 1.5rem; - background: rgba(255, 255, 255, 0.03); - border: 1px solid rgba(255, 255, 255, 0.1); + display: flex; + align-items: center; + justify-content: center; + padding: 0 14px; + gap: 6px; + height: 36px; border-radius: 8px; + background: rgba(255, 255, 255, 0.05); + border: 1px solid rgba(255, 255, 255, 0.1); color: #a0a0a0; + cursor: pointer; + transition: all 0.2s ease; font-size: 0.9rem; font-weight: 500; +} + +.pagination-btn:hover:not(:disabled) { + background: rgba(255, 255, 255, 0.1); + border-color: rgba(69, 89, 220, 0.4); + color: #ffffff; +} + +.pagination-btn:disabled { + opacity: 0.4; + cursor: not-allowed; +} + +.pagination-numbers { + display: flex; + align-items: center; + gap: 4px; +} + +.page-number { + width: 36px; + height: 36px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 8px; + background: transparent; + border: 1px solid transparent; + color: #a0a0a0; cursor: pointer; + font-size: 0.9rem; + font-weight: 500; transition: all 0.2s ease; } +.page-number:hover { + background: rgba(255, 255, 255, 0.05); + color: #ffffff; +} + +.page-number.active { + background: rgba(69, 89, 220, 0.15); + border-color: rgba(69, 89, 220, 0.4); + color: #4559DC; + font-weight: 600; +} + /* ===== GO BACK BUTTON - LIGHT THEME FIX ===== */ /* Go Back button - base (dark theme) */ .blog-back-container { @@ -661,15 +704,19 @@ font-weight: 700; font-size: 0.95rem; border-radius: 10px; - background: rgba(51, 49, 179, 0.8); /* stronger background */ - color: #ffffff; /* white text for contrast */ - border: 1px solid rgba(88, 70, 206, 0.4); /* slightly stronger border */ + background: rgba(51, 49, 179, 0.8); + /* stronger background */ + color: #ffffff; + /* white text for contrast */ + border: 1px solid rgba(88, 70, 206, 0.4); + /* slightly stronger border */ cursor: pointer; transition: transform 0.12s ease, box-shadow 0.14s ease, background 0.14s ease, color 0.14s ease; } .blog-back-btn:hover { - background: rgba(51, 49, 179, 1); /* full color on hover */ + background: rgba(51, 49, 179, 1); + /* full color on hover */ transform: scale(1.05); box-shadow: 0 4px 12px rgba(51, 49, 179, 0.3); } @@ -733,22 +780,25 @@ box-shadow: 0 8px 18px rgba(2, 6, 23, 0.45); } -.blog-download-btn:active { transform: translateY(0); } +.blog-download-btn:active { + transform: translateY(0); +} .blog-download-btn.blog-download-txt { background: transparent; - border: 1px dashed rgba(255,255,255,0.06); + border: 1px dashed rgba(255, 255, 255, 0.06); } [data-theme="light"] .blog-download-btn { - background: rgba(255,255,255,0.96); + background: rgba(255, 255, 255, 0.96); color: #1e293b; - border: 1px solid rgba(148,163,184,0.4); + border: 1px solid rgba(148, 163, 184, 0.4); } [data-theme="light"] .blog-download-btn.blog-download-txt { background: transparent; } + [data-theme="light"] .blog-back-btn { background: radial-gradient(circle at top left, #1d4ed8, #3b82f6); color: white; @@ -1163,7 +1213,8 @@ font-size: 1.1rem; } -.action-btn:hover { +.action-btn:hover, +.action-btn.active { background: rgba(69, 89, 220, 0.2); border-color: #4559DC; color: #4559DC; @@ -1174,11 +1225,11 @@ .blog-detail-content-wrapper { grid-template-columns: 1fr; } - + .blog-detail-sidebar { display: none; } - + .floating-actions { right: 1rem; } @@ -1188,23 +1239,23 @@ .blog-detail-page { padding: 1rem; } - + .blog-detail-header { padding: 1.5rem; } - + .blog-metadata { grid-template-columns: 1fr; } - + .blog-stats { grid-template-columns: repeat(2, 1fr); } - + .blog-detail-main { padding: 1.5rem; } - + .floating-actions { flex-direction: row; position: fixed; @@ -1213,5 +1264,4 @@ top: auto; transform: none; } -} - +} \ No newline at end of file diff --git a/src/components/Sections/Blog.jsx b/src/components/Sections/Blog.jsx new file mode 100644 index 0000000..1b4f8ad --- /dev/null +++ b/src/components/Sections/Blog.jsx @@ -0,0 +1,260 @@ +import React, { useState } from "react"; +import "./Blog.css"; +import { useNavigate } from "react-router-dom"; +import { motion } from "framer-motion"; +import { generateBlogPosts } from "../../data/blogData"; +import { FiChevronLeft, FiChevronRight } from "react-icons/fi"; + +export default function Blog() { + const navigate = useNavigate(); + const blogPosts = generateBlogPosts(); + const featuredPosts = blogPosts.filter(post => post.isFeatured); + const regularPosts = blogPosts.filter(post => !post.isFeatured); + + // Pagination State + const [currentPage, setCurrentPage] = useState(1); + const postsPerPage = 9; + + // Calculate current posts + const indexOfLastPost = currentPage * postsPerPage; + const indexOfFirstPost = indexOfLastPost - postsPerPage; + const currentPosts = regularPosts.slice(indexOfFirstPost, indexOfLastPost); + const totalPages = Math.ceil(regularPosts.length / postsPerPage); + + // Change page + const paginate = (pageNumber) => { + setCurrentPage(pageNumber); + const section = document.querySelector('.all-posts-section'); + if (section) { + window.scrollTo({ top: section.offsetTop - 100, behavior: 'smooth' }); + } + }; + + return ( +
+
+ + {/* Hero Section - Glassnode Inspired */} +
+ +
+ + + + + On-Chain Market Intelligence +
+

+ Professional-Grade Insights +

+

+ Your portal to contextualised market analysis, and cutting edge research + for Bitcoin, Ethereum, DeFi and more. Access premium on-chain data and institutional-grade analysis. +

+
+
+ + {/* Featured Reports */} +
+

+ + + + Featured Reports +

+ +
+ {featuredPosts.map((post, index) => ( + navigate(`/blog/${post.id}`, { state: { post } })} + > +
+
+ {post.title} + +
+ {post.tag} +
+
+ +
+
+ {post.category} +
+ {post.tag === "Premium" ? ( +
+ + + + + Premium +
+ ) : ( +
+ + + + + Free +
+ )} +
+
+ +

{post.title}

+

{post.excerpt}

+ +
+
+ {post.date} + + {post.readTime} +
+ + +
+
+
+ ))} +
+
+ + {/* All Posts Grid */} +
+
+

Latest Research & Analysis

+

Comprehensive reports and insights

+
+ +
+ {currentPosts.map((post, index) => ( + navigate(`/blog/${post.id}`, { state: { post } })} + > +
+ {post.title} +
+ {post.tag} +
+
+ +
+
+ {post.category} + {post.date} +
+ +

{post.title}

+

{post.excerpt}

+ +
+ {post.readTime} +
+ Read Article + + + +
+
+
+
+ ))} +
+ + {/* Pagination Controls */} + {totalPages > 1 && ( +
+ + +
+ {totalPages <= 5 ? ( + Array.from({ length: totalPages }, (_, i) => i + 1).map(pageNum => ( + + )) + ) : ( + (() => { + const pages = []; + if (currentPage <= 3) { + for (let i = 1; i <= 5; i++) pages.push(i); + } else if (currentPage >= totalPages - 2) { + for (let i = totalPages - 4; i <= totalPages; i++) pages.push(i); + } else { + for (let i = currentPage - 2; i <= currentPage + 2; i++) pages.push(i); + } + return pages.map(pageNum => ( + + )); + })() + )} +
+ + +
+ )} +
+
+
+ ); +} diff --git a/src/components/BlogDetail.jsx b/src/components/Sections/BlogDetail.jsx similarity index 56% rename from src/components/BlogDetail.jsx rename to src/components/Sections/BlogDetail.jsx index e1c4efc..8816f76 100644 --- a/src/components/BlogDetail.jsx +++ b/src/components/Sections/BlogDetail.jsx @@ -1,5 +1,7 @@ +import { useState, useEffect } from "react"; import { useParams, useNavigate } from "react-router-dom"; import { motion } from "framer-motion"; +import { toast } from "react-hot-toast"; import { FiArrowLeft, FiUser, @@ -11,36 +13,56 @@ import { FiExternalLink, } from "react-icons/fi"; import "./Blog.css"; -import { generateBlogPosts } from "./Blog"; +import { generateBlogPosts } from "../../data/blogData"; +import { useAuth } from "../../context/useAuth"; +import { toggleBookmark, getBookmarks } from "../../services/bookmarkService"; const BlogDetail = () => { const { id, slug } = useParams(); const navigate = useNavigate(); const blogPosts = generateBlogPosts(); + const { currentUser } = useAuth(); + + // State for interactions + const [isBookmarked, setIsBookmarked] = useState(false); + const [_loadingBookmark, setLoadingBookmark] = useState(true); // 🔒 SAFELY find blog - handle both slug and id params - // The route /blog/:slug will put the value in 'slug' even if it's a number const blogId = id || slug; - const blog = blogPosts.find((post) => - post.id === Number(blogId) || post.slug === blogId + const blog = blogPosts.find( + (post) => post.id === Number(blogId) || post.slug === blogId, ); - // ✅ Early return → removes ALL "possibly undefined" warnings + // Check if bookmarked on load + useEffect(() => { + const checkBookmarkStatus = async () => { + if (currentUser && blog) { + try { + const bookmarks = await getBookmarks(currentUser.uid); + setIsBookmarked(bookmarks.includes(blog.id)); + } catch (error) { + console.error("Error fetching bookmarks:", error); + } + } + setLoadingBookmark(false); + }; + + checkBookmarkStatus(); + }, [currentUser, blog]); + + // ✅ Early return if (!blog) { return (

Blog not found

-
); } - // ✅ Normalized content (no undefined access anywhere) + // ✅ Normalized content const content = { toc: blog.content?.toc ?? [ "Introduction", @@ -58,8 +80,7 @@ const BlogDetail = () => { }, { heading: "Overview", - text: - "This article is currently being prepared with detailed analysis and insights.", + text: "This article is currently being prepared with detailed analysis and insights.", }, ], }; @@ -67,6 +88,53 @@ const BlogDetail = () => { // Demo views const views = Math.floor(Math.random() * 2000) + 500; + // Handlers + // Handlers + const handleBookmark = async () => { + if (!currentUser) { + toast.error("Please login to bookmark articles"); + navigate("/login"); + return; + } + + try { + const isNowBookmarked = await toggleBookmark(currentUser.uid, blog.id); + setIsBookmarked(isNowBookmarked); + + if (isNowBookmarked) { + toast.success("Article saved to bookmarks"); + } else { + toast.success("Article removed from bookmarks"); + } + } catch (error) { + console.error("Error toggling bookmark:", error); + toast.error("Failed to update bookmark"); + } + }; + + const handleShare = async () => { + const shareData = { + title: blog.title, + text: blog.excerpt, + url: window.location.href, + }; + + if (navigator.share) { + try { + await navigator.share(shareData); + } catch (err) { + console.error("Error sharing:", err); + } + } else { + navigator.clipboard.writeText(window.location.href); + toast.success("Link copied to clipboard!"); + } + }; + + const handleOpenNewTab = () => { + window.open(window.location.href, "_blank"); + }; + return (
{/* Breadcrumb */} @@ -75,16 +143,11 @@ const BlogDetail = () => { initial={{ opacity: 0, y: -10 }} animate={{ opacity: 1, y: 0 }} > - / - - {blog.category || "Article"} - + {blog.category || "Article"} {/* Header */} @@ -110,8 +173,16 @@ const BlogDetail = () => {
} label="AUTHOR" value="Thomas Wright" /> - } label="PUBLISHED" value={blog.date || "N/A"} /> - } label="READ TIME" value={blog.readTime || "N/A"} /> + } + label="PUBLISHED" + value={blog.date || "N/A"} + /> + } + label="READ TIME" + value={blog.readTime || "N/A"} + /> } label="VIEWS" value={views.toLocaleString()} />
@@ -163,9 +234,18 @@ const BlogDetail = () => { {/* Floating Actions */}
- } title="Bookmark" /> - } title="Share" /> - } title="Open in new tab" /> + } + title={isBookmarked ? "Remove Bookmark" : "Bookmark"} + onClick={handleBookmark} + active={isBookmarked} + /> + } title="Share" onClick={handleShare} /> + } + title="Open in new tab" + onClick={handleOpenNewTab} + />
@@ -184,12 +264,14 @@ const Meta = ({ icon, label, value }) => (
); -const Action = ({ icon, title }) => ( +const Action = ({ icon, title, onClick, active }) => ( {icon} diff --git a/src/components/Sections/ContactUs.css b/src/components/Sections/ContactUs.css new file mode 100644 index 0000000..4e6ea58 --- /dev/null +++ b/src/components/Sections/ContactUs.css @@ -0,0 +1,89 @@ +.contact-container{ + padding:60px 20px; + max-width:900px; + margin:auto; + color:#fff; + background: linear-gradient(135deg, #0b021f, #1a0438); + margin-bottom: 10px; +} +.contact-header{ + text-align: center; + margin-bottom: 60px; +} +.contact-header h1{ + font-size: 40px; + margin-top: 10px; + color:#c77dff; + font-weight: 700; +} +.contact-header p{ + color: #cfcfcf; + max-width: 600px; + margin: 0 auto; +} +.contact-content{ + display: grid; + grid-template-columns: 1.2fr 0.8fr; + gap:50px; +} +.contact-form{ + background: rgba(255, 255, 255, 0.05); + padding:30px; + border-radius:10px; +} +.form-group{ + margin-bottom:20px; + display: flex; + flex-direction: column; +} +.form-group label{ + margin-bottom: 10px; + font-weight: 600; + color:#ddd; +} +.form-group input, +.form-group textarea{ + padding:10px; + border-radius:10px; + border:1px solid rgba(255, 255, 255, 0.1); + background: rgba(255, 255, 255, 0.07); + color:#fff +} +.form-group input::placeholder, +.form-group textarea::placeholder{ + color:#bbb; +} +.form-group textarea{ + resize: vertical; + min-height:100px; +} +button{ + background: #c77dff; + color:#fff; + border:none; + padding:12px 17px; + border-radius: 10px; + cursor: pointer; + font-weight: 600; + transition: background 0.3s ease; +} +button:hover{ + background: #a64dff; +} +.contact-info { + padding: 30px; +} +.contact-info h3{ + margin-bottom: 15px; + color:#c77dff; +} +.contact-info p{ + margin-bottom: 10px; + color:#ccc; +} + +@media (max-width:768px){ + .contact-content{ + grid-template-columns: 1fr; + } +} \ No newline at end of file diff --git a/src/components/Sections/ContactUs.jsx b/src/components/Sections/ContactUs.jsx new file mode 100644 index 0000000..e1f73de --- /dev/null +++ b/src/components/Sections/ContactUs.jsx @@ -0,0 +1,43 @@ +import React from "react"; +import "./ContactUs.css"; + +const ContactUs = () => { + return ( +
+
+

Contact Us

+

+ Have a question, feedback, or partnership idea? Drop us a message and + we will get back to you. +

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

Reach Us

+

Email : support@cryptohub.com

+

Location: India, Remote Team

+

Support Hours: 24/7

+
+
+
+ ); +}; + +export default ContactUs; diff --git a/src/components/Sections/Contributors.jsx b/src/components/Sections/Contributors.jsx new file mode 100644 index 0000000..9dabbb4 --- /dev/null +++ b/src/components/Sections/Contributors.jsx @@ -0,0 +1,630 @@ +import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import "./contributors.css"; + +const OWNER = "KaranUnique"; +const REPO = "CryptoHub"; +const GITHUB_API_BASE = `https://api.github.com/repos/${OWNER}/${REPO}`; +const PER_PAGE = 100; +const ITEMS_PER_PAGE_DISPLAY = 6; + +// Precompiled regex for level extraction (avoid re-creating per call) +const LEVEL_REGEX = /level[\s-]*(1|2|3)/; + +// Project Admin Config +const PROJECT_ADMIN = { + username: "KaranUnique", + repo: "CryptoHub", + repoUrl: `https://github.com/${OWNER}/${REPO}`, + githubUrl: `https://github.com/${OWNER}`, + avatarUrl: `https://avatars.githubusercontent.com/${OWNER}?v=4&s=200`, + description: "Project Creator & Lead Maintainer" +}; + +// Points per level +const LEVEL_POINTS = { 1: 2, 2: 5, 3: 11 }; + +// Rank thresholds (sorted descending for early exit) +const RANK_THRESHOLDS = [ + { min: 30, label: "Gold 🥇" }, + { min: 20, label: "Silver 🥈" }, + { min: 10, label: "Bronze 🥉" }, +]; + +const RANK_MAP = { + gold: "Gold", + silver: "Silver", + bronze: "Bronze", + contributor: "Contributor", +}; + +const getLevelFromPr = (pr) => { + const title = pr.title?.toLowerCase() || ""; + const titleMatch = title.match(LEVEL_REGEX); + if (titleMatch) return Number(titleMatch[1]); + + if (Array.isArray(pr.labels)) { + for (const label of pr.labels) { + const name = (label?.name || "").toLowerCase(); + const labelMatch = name.match(LEVEL_REGEX); + if (labelMatch) return Number(labelMatch[1]); + } + } + + return null; +}; + +const getRankFromPoints = (points) => { + for (const { min, label } of RANK_THRESHOLDS) { + if (points >= min) return label; + } + return "Contributor"; +}; + +// Precompute rank CSS class from rank string (memoized outside render) +const rankClassCache = new Map(); +const getRankClass = (rank) => { + if (rankClassCache.has(rank)) return rankClassCache.get(rank); + const cls = rank.toLowerCase().replace(/ /g, "-").replace(/[🥇🥈🥉]/gu, "").replace(/-$/, ""); + rankClassCache.set(rank, cls); + return cls; +}; + +// Debounce hook +const useDebounce = (value, delay) => { + const [debouncedValue, setDebouncedValue] = useState(value); + useEffect(() => { + const timer = setTimeout(() => setDebouncedValue(value), delay); + return () => clearTimeout(timer); + }, [value, delay]); + return debouncedValue; +}; + +// Extracted ContributorCard to avoid re-renders of sibling cards +const ContributorCard = React.memo(({ contributor, onOpenModal, onOpenGitHub }) => { + const rankClass = getRankClass(contributor.rank); + return ( +
+
+
+ {contributor.username} +
+
+

{contributor.username}

+

+ {contributor.rank} +

+
+
+ +
+
+ Points + {contributor.totalPoints} +
+
+ Merged PRs + {contributor.totalPRs} +
+
+ +
+ + +
+
+ ); +}); + +ContributorCard.displayName = "ContributorCard"; + +// Process raw PR data into contributor map (pure function, no side effects) +const buildContributors = (mergedPrs) => { + const map = {}; + + for (const pr of mergedPrs) { + const user = pr.user; + if (!user) continue; + + const { login: username, avatar_url, html_url } = user; + const level = getLevelFromPr(pr); + const points = level ? LEVEL_POINTS[level] || 0 : 0; + + if (!map[username]) { + map[username] = { + username, + avatar_url, + html_url, + totalPoints: 0, + totalPRs: 0, + prs: [], + }; + } + + map[username].totalPRs += 1; + map[username].totalPoints += points; + map[username].prs.push({ + id: pr.id, + number: pr.number, + title: pr.title, + html_url: pr.html_url, + merged_at: pr.merged_at, + level, + points, + }); + } + + return Object.values(map).map((c) => ({ + ...c, + rank: getRankFromPoints(c.totalPoints), + })); +}; + +const Contributors = () => { + const [contributors, setContributors] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(""); + const [search, setSearch] = useState(""); + const [sortBy, setSortBy] = useState("most_points"); + const [selectedRankFilter, setSelectedRankFilter] = useState("all"); + const [selectedContributor, setSelectedContributor] = useState(null); + const [showModal, setShowModal] = useState(false); + const [currentPage, setCurrentPage] = useState(1); + + // AbortController ref for cleanup on unmount + const abortRef = useRef(null); + + // Debounce search to avoid filtering on every keystroke + const debouncedSearch = useDebounce(search, 250); + + // Fetch with AbortController support + useEffect(() => { + const controller = new AbortController(); + abortRef.current = controller; + + const fetchAllMergedPRs = async () => { + setLoading(true); + setError(""); + + try { + let page = 1; + let mergedPrs = []; + + while (true) { + const url = `${GITHUB_API_BASE}/pulls?state=closed&per_page=${PER_PAGE}&page=${page}`; + const response = await fetch(url, { + signal: controller.signal, + headers: { + 'Accept': 'application/vnd.github+json', + 'User-Agent': 'CryptoHub-Contributors-App', + }, + }); + + if (!response.ok) { + throw new Error(`GitHub API Error: ${response.status}`); + } + + const data = await response.json(); + const merged = data.filter((pr) => pr.merged_at); + mergedPrs = mergedPrs.concat(merged); + + if (data.length < PER_PAGE) break; + page += 1; + } + + setContributors(buildContributors(mergedPrs)); + } catch (err) { + if (err.name === "AbortError") return; // Unmounted, ignore + console.error("Fetch error:", err); + setError(`Failed to load data: ${err.message}`); + } finally { + setLoading(false); + } + }; + + fetchAllMergedPRs(); + + return () => controller.abort(); // Cleanup on unmount + }, []); + + // Memoize aggregate stats to avoid recalculation in render + const stats = useMemo(() => { + let totalPRs = 0; + let totalPoints = 0; + for (const c of contributors) { + totalPRs += c.totalPRs; + totalPoints += c.totalPoints; + } + return { count: contributors.length, totalPRs, totalPoints }; + }, [contributors]); + + const filteredContributors = useMemo(() => { + let result = contributors; + + if (debouncedSearch.trim()) { + const q = debouncedSearch.trim().toLowerCase(); + result = result.filter((c) => c.username.toLowerCase().includes(q)); + } + + if (selectedRankFilter !== "all") { + const selectedRank = RANK_MAP[selectedRankFilter]; + if (selectedRank) { + result = result.filter((c) => c.rank.startsWith(selectedRank)); + } + } + + // Only copy when we need to sort (sort mutates) + result = [...result]; + + if (sortBy === "most_points") { + result.sort((a, b) => b.totalPoints - a.totalPoints); + } else if (sortBy === "most_prs") { + result.sort((a, b) => b.totalPRs - a.totalPRs); + } + + return result; + }, [contributors, debouncedSearch, selectedRankFilter, sortBy]); + + // Reset page when filters change + useEffect(() => { + setCurrentPage(1); + }, [debouncedSearch, sortBy, selectedRankFilter]); + + // Calculate pagination + const indexOfLastItem = currentPage * ITEMS_PER_PAGE_DISPLAY; + const indexOfFirstItem = indexOfLastItem - ITEMS_PER_PAGE_DISPLAY; + const currentContributors = filteredContributors.slice(indexOfFirstItem, indexOfLastItem); + const totalPages = Math.ceil(filteredContributors.length / ITEMS_PER_PAGE_DISPLAY); + + const paginate = (pageNumber) => setCurrentPage(pageNumber); + + const handleOpenModal = useCallback((contributor) => { + setSelectedContributor(contributor); + setShowModal(true); + }, []); + + const handleCloseModal = useCallback(() => { + setSelectedContributor(null); + setShowModal(false); + }, []); + + const handleOpenGitHubProfile = useCallback((url) => { + if (!url) return; + window.open(url, "_blank", "noopener,noreferrer"); + }, []); + + const handleOpenRepo = useCallback(() => { + window.open(PROJECT_ADMIN.repoUrl, "_blank", "noopener,noreferrer"); + }, []); + + return ( +
+ {/* Header Section */} +
+

Our Amazing Contributors

+

+ Meet the talented developers who help make CryptoHub better every day. +

+ +
+
+ Contributors + {stats.count} +
+ +
+ Total PRs + {stats.totalPRs} +
+ +
+ Total Points + {stats.totalPoints} +
+
+
+ + {/* Filters Section */} +
+ setSearch(e.target.value)} + /> + +
+ + + +
+
+ + {/* NEW PROJECT ADMIN SECTION */} +
+
+
+
+ {PROJECT_ADMIN.username} +
👑
+
+
+

Project Admin

+

{PROJECT_ADMIN.username}

+

{PROJECT_ADMIN.description}

+
+
+ +
+

Repository

+
+ {PROJECT_ADMIN.repo} + 📂 +
+
+ +
+ + +
+
+
+ + {/* Content Section */} +
+ {loading && ( +
+

Loading contributor data from GitHub...

+
+ )} + + {error && ( +
+

{error}

+

+ Stats will show "No contributors found" - normal if repo has no merged PRs yet. +

+
+ )} + + {!loading && !error && filteredContributors.length === 0 && ( +
+

No contributors found.

+

+ No merged pull requests in KaranUnique/CryptoHub yet. +
Stats show 0 contributors, 0 PRs, 0 points ✅ +

+
+ )} + + {!loading && !error && filteredContributors.length > 0 && ( + <> +
+ {currentContributors.map((c) => ( + + ))} +
+ + {/* Pagination Controls */} + {totalPages > 1 && ( +
+ + + {/* Windowed/ellipsis pagination to avoid rendering a button for every page */} + {(() => { + const visiblePages = []; + const maxVisible = 7; // show all pages if totalPages is small + + if (totalPages <= maxVisible) { + // Small number of pages: render all + for (let page = 1; page <= totalPages; page++) { + visiblePages.push(page); + } + } else { + const firstPage = 1; + const lastPage = totalPages; + const windowSize = 2; // number of pages to show on each side of current + + let start = Math.max(currentPage - windowSize, firstPage + 1); + let end = Math.min(currentPage + windowSize, lastPage - 1); + + // Ensure window stays within bounds between first+1 and last-1 + start = Math.max(start, firstPage + 1); + end = Math.min(end, lastPage - 1); + + visiblePages.push(firstPage); + + // Left ellipsis + if (start > firstPage + 1) { + visiblePages.push("ellipsis-left"); + } + + // Pages around current + for (let page = start; page <= end; page++) { + visiblePages.push(page); + } + + // Right ellipsis + if (end < lastPage - 1) { + visiblePages.push("ellipsis-right"); + } + + visiblePages.push(lastPage); + } + + return visiblePages.map((item, index) => { + if (typeof item === "string") { + // Ellipsis placeholder + return ( + + … + + ); + } + + const pageNumber = item; + return ( + + ); + }); + })()} + +
+ )} + + )} +
+ + {/* Modal for PR details */} + {showModal && selectedContributor && ( +
+
e.stopPropagation()} + > +
+

+ PRs by {selectedContributor.username} +

+ +
+ +
+ {selectedContributor.prs.length === 0 && ( +

+ No merged pull requests found for this contributor. +

+ )} + + {selectedContributor.prs.length > 0 && ( +
    + {selectedContributor.prs.map((pr) => ( +
  • +
    + + #{pr.number} — {pr.title} + + + Merged at: {pr.merged_at + ? new Date(pr.merged_at).toLocaleString() + : "N/A"} + +
    + +
    + + Level: {pr.level ? `Level ${pr.level}` : "Not specified"} + + + Points: {pr.points} + +
    +
  • + ))} +
+ )} +
+ +
+ +
+
+
+ )} +
+ ); +}; + +export default Contributors; diff --git a/src/components/Sections/FAQ.css b/src/components/Sections/FAQ.css new file mode 100644 index 0000000..6dc22ba --- /dev/null +++ b/src/components/Sections/FAQ.css @@ -0,0 +1,73 @@ +.faq-page{ + padding:60px 20px; + max-width:900px; + margin:auto; + color:#fff; +} +.faq-title{ + text-align:center; + font-size:2rem; + font-weight:700; +} +.faq-subtitle{ + font-size: 1.2rem; + margin-bottom: 30px; + color: #b3b3b3; + text-align:center; +} +.faq-container{ + display:flex; + flex-direction:column; + gap:16px; +} +.faq-item{ + background: rgba(255, 255, 255, 0.05); + border-radius: 10px; + overflow: hidden; + border: 1px solid rgba(255, 255, 255, 0.1); +} +.faq-question { + width: 100%; + background: none; + border: none; + color: #fff; + padding: 18px 20px; + font-size: 18px; + display: flex; + justify-content: space-between; + align-items: center; + cursor: pointer; +} +.faq-question span { + font-size: 24px; +} + +.faq-answer { + max-height: 0; + overflow: hidden; + padding: 0 20px; + color: #cfcfcf; + line-height: 1.6; + transition: max-height 0.5s ease, padding 0.3s ease; +} + +.faq-answer.show { + max-height: 600px; /* increased for longer content */ + padding: 0 20px 18px; +} + +.faq-answer p { + margin-bottom: 10px; +} + +.faq-answer ul { + list-style: disc !important; + list-style-position: inside; + padding-left: 20px !important; + margin: 10px 0; +} + +.faq-answer li { + display: list-item !important; + margin-bottom: 6px; +} diff --git a/src/components/Sections/FAQ.jsx b/src/components/Sections/FAQ.jsx new file mode 100644 index 0000000..bc542f5 --- /dev/null +++ b/src/components/Sections/FAQ.jsx @@ -0,0 +1,140 @@ +import React,{useState} from 'react' +import './FAQ.css' + +const FAQ = () => { +const [activeIndex, setActiveIndex] = useState(null); + + +const faqData = [ + { + question: "What is CryptoHub?", + answer: ( + <> +

+ CryptoHub is a comprehensive cryptocurrency tracking platform designed to help users monitor the crypto market in real time. +

+ +
+

• Track live cryptocurrency prices and trends

+

• Analyze market insights and performance data

+

• Manage and monitor your personal crypto portfolio

+
+ +

+ It is built to provide simple and user-friendly tools for both beginners and experienced traders. +

+ + ) + }, + { + question: "Is CryptoHub free to use?", + answer: ( + <> +

+ Yes, CryptoHub offers a free plan that provides access to essential features. +

+ +
+

• Real-time crypto price tracking

+

• Market overview and insights

+

• Basic portfolio management

+
+ +

+ Premium features may be available for users who need advanced analytics and tools. +

+ + ) + }, + { + question: "Where does CryptoHub get its data?", + answer: ( + <> +

+ CryptoHub collects data from trusted cryptocurrency market APIs and exchange platforms. +

+ +
+

• Live price feeds

+

• Market capitalization data

+

• Trading volume statistics

+
+ +

+ The platform continuously updates its data to ensure accuracy and reliability. +

+ + ) + }, + { + question: "Can I track my portfolio?", + answer: ( + <> +

+ Yes, users can create and manage their crypto portfolio directly on CryptoHub. +

+ +
+

• Add cryptocurrencies you own

+

• Track profit and loss

+

• Monitor overall portfolio performance

+
+ +

+ This helps users make informed investment decisions. +

+ + ) + }, + { + question: "Is my data secure?", + answer: ( + <> +

+ Yes, we follow industry-standard security practices to protect user information. +

+ +
+

• Secure authentication systems

+

• Encrypted data handling

+

• Protected API integrations

+
+ +

+ User privacy and security are a top priority for CryptoHub. +

+ + ) + } +]; + + + +const toogleFAQ=(idx)=>{ + setActiveIndex(activeIndex === idx ? null : idx); +} + return ( +
+
Frequently Asked Questions
+

Find quick answers about CryptoHub and how it works.

+ +
+ {faqData.map((item,idx)=>( +
+ + +
+ {item.answer} +
+ + +
+ ))} +
+
+ ) +} + +export default FAQ diff --git a/src/components/Features.css b/src/components/Sections/Features.css similarity index 78% rename from src/components/Features.css rename to src/components/Sections/Features.css index 8ce6607..a16bbaf 100644 --- a/src/components/Features.css +++ b/src/components/Sections/Features.css @@ -5,11 +5,13 @@ */ .features-container { min-height: 100vh; - background: linear-gradient(145deg, - #0a0a0f 0%, - #0f0a1a 30%, - #1a0f2e 60%, - #0a1428 100%); + background: linear-gradient( + 145deg, + #0a0a0f 0%, + #0f0a1a 30%, + #1a0f2e 60%, + #0a1428 100% + ); padding: 2rem 1rem; position: relative; overflow-x: hidden; @@ -18,36 +20,55 @@ /* Animated Cosmic Background */ .features-container::before { - content: ''; + content: ""; position: fixed; top: 0; left: 0; width: 100%; height: 100%; - background: - radial-gradient(circle at 10% 20%, rgba(139, 92, 246, 0.1) 0%, transparent 50%), - radial-gradient(circle at 90% 80%, rgba(236, 72, 153, 0.1) 0%, transparent 50%), - radial-gradient(circle at 30% 90%, rgba(245, 158, 11, 0.08) 0%, transparent 50%); + background: + radial-gradient( + circle at 10% 20%, + rgba(139, 92, 246, 0.1) 0%, + transparent 50% + ), + radial-gradient( + circle at 90% 80%, + rgba(236, 72, 153, 0.1) 0%, + transparent 50% + ), + radial-gradient( + circle at 30% 90%, + rgba(245, 158, 11, 0.08) 0%, + transparent 50% + ); animation: cosmicShift 15s ease-in-out infinite; z-index: 0; pointer-events: none; } @keyframes cosmicShift { - 0%, 100% { transform: scale(1) rotate(0deg); } - 33% { transform: scale(1.1) rotate(120deg); } - 66% { transform: scale(0.95) rotate(240deg); } + 0%, + 100% { + transform: scale(1) rotate(0deg); + } + 33% { + transform: scale(1.1) rotate(120deg); + } + 66% { + transform: scale(0.95) rotate(240deg); + } } /* Floating Particles */ .features-container::after { - content: ''; + content: ""; position: absolute; top: 0; left: 0; width: 100%; height: 100%; - background-image: + background-image: radial-gradient(1.5px 1.5px at 20px 30px, #ff6b6b66, transparent), radial-gradient(1.5px 1.5px at 80px 70px, #4ecdc466, transparent), radial-gradient(1px 1px at 120px 40px, #45b7d166, transparent), @@ -63,8 +84,12 @@ } @keyframes particleFloat { - from { background-position: 0 0; } - to { background-position: 100px 200px; } + from { + background-position: 0 0; + } + to { + background-position: 100px 200px; + } } /* Header */ @@ -72,17 +97,20 @@ text-align: center; margin-bottom: 50px; position: relative; + margin: 20px; z-index: 10; } .features-title { font-size: clamp(2.8rem, 6vw, 5rem); - background: linear-gradient(135deg, - #8b5cf6 0%, - #ec4899 25%, - #f59e0b 50%, - #10b981 75%, - #8b5cf6 100%); + background: linear-gradient( + 135deg, + #8b5cf6 0%, + #ec4899 25%, + #f59e0b 50%, + #10b981 75%, + #8b5cf6 100% + ); background-size: 300% 300%; -webkit-background-clip: text; -webkit-text-fill-color: transparent; @@ -91,21 +119,28 @@ letter-spacing: -0.03em; margin-bottom: 3rem; position: relative; - animation: gradientShift 4s ease-in-out infinite, titleGlow 3s ease-in-out infinite alternate; + animation: + gradientShift 4s ease-in-out infinite, + titleGlow 3s ease-in-out infinite alternate; text-shadow: 0 0 60px rgba(139, 92, 246, 0.6); } @keyframes gradientShift { - 0%, 100% { background-position: 0% 50%; } - 50% { background-position: 100% 50%; } + 0%, + 100% { + background-position: 0% 50%; + } + 50% { + background-position: 100% 50%; + } } @keyframes titleGlow { - from { - filter: drop-shadow(0 0 30px rgba(139, 92, 246, 0.8)) brightness(1); + from { + filter: drop-shadow(0 0 30px rgba(139, 92, 246, 0.8)) brightness(1); } - to { - filter: drop-shadow(0 0 60px rgba(236, 72, 153, 0.9)) brightness(1.1); + to { + filter: drop-shadow(0 0 60px rgba(236, 72, 153, 0.9)) brightness(1.1); } } @@ -130,10 +165,12 @@ select { appearance: none; -webkit-appearance: none; -moz-appearance: none; - - background: linear-gradient(145deg, - rgba(25, 25, 50, 0.95), - rgba(15, 15, 35, 0.95)); + + background: linear-gradient( + 145deg, + rgba(25, 25, 50, 0.95), + rgba(15, 15, 35, 0.95) + ); backdrop-filter: blur(30px); border: 2px solid rgba(139, 92, 246, 0.4); border-radius: 24px; @@ -144,7 +181,7 @@ select { cursor: pointer; min-width: 200px; transition: all 0.4s cubic-bezier(0.25, 0.46, 0.45, 0.94); - box-shadow: + box-shadow: 0 20px 40px rgba(0, 0, 0, 0.4), inset 0 1px 0 rgba(255, 255, 255, 0.1); } @@ -167,7 +204,7 @@ select option:hover { } .select-wrapper::after { - content: '▼'; + content: "▼"; position: absolute; right: 1.5rem; top: 50%; @@ -188,7 +225,7 @@ select:focus + .select-wrapper::after { select:hover { border-color: #8b5cf6; - box-shadow: + box-shadow: 0 30px 60px rgba(139, 92, 246, 0.4), inset 0 1px 0 rgba(255, 255, 255, 0.2); transform: translateY(-4px); @@ -197,7 +234,7 @@ select:hover { select:focus { outline: none; border-color: #8b5cf6; - box-shadow: + box-shadow: 0 0 0 4px rgba(139, 92, 246, 0.3), 0 30px 60px rgba(139, 92, 246, 0.4); } @@ -213,7 +250,7 @@ select:focus { padding: 4rem 3rem; margin: 0 auto 3rem; max-width: 1300px; - box-shadow: + box-shadow: 0 50px 100px -25px rgba(0, 0, 0, 0.7), 0 0 0 1px rgba(255, 255, 255, 0.02), inset 0 1px 0 rgba(255, 255, 255, 0.06); @@ -221,21 +258,28 @@ select:focus { } @keyframes cardFloat { - 0%, 100% { transform: translateY(0px); } - 50% { transform: translateY(-10px); } + 0%, + 100% { + transform: translateY(0px); + } + 50% { + transform: translateY(-10px); + } } .features-card::before { - content: ''; + content: ""; position: absolute; top: 0; left: 0; right: 0; height: 1px; - background: linear-gradient(90deg, - transparent 0%, - rgba(139, 92, 246, 0.6) 50%, - transparent 100%); + background: linear-gradient( + 90deg, + transparent 0%, + rgba(139, 92, 246, 0.6) 50%, + transparent 100% + ); z-index: 5; } @@ -253,7 +297,7 @@ select:focus { .chart-wrapper canvas { border-radius: 28px !important; - box-shadow: + box-shadow: 0 30px 60px rgba(0, 0, 0, 0.5), inset 0 1px 0 rgba(255, 255, 255, 0.1) !important; } @@ -273,22 +317,28 @@ select:focus { height: 10px; background: var(--neon-cyan); border-radius: 50%; - animation: spin 1.2s cubic-bezier(0.4, 0, 0.2, 1) infinite, - loaderPulse 2.5s ease-in-out infinite; + animation: + spin 1.2s cubic-bezier(0.4, 0, 0.2, 1) infinite, + loaderPulse 2.5s ease-in-out infinite; } @keyframes spin { - to { transform: rotate(360deg); } + to { + transform: rotate(360deg); + } } @keyframes loaderPulse { - 0%, 100% { - box-shadow: 0 0 0 0 rgba(139, 92, 246, 0.8), - 0 0 0 0 rgba(139, 92, 246, 0.4); + 0%, + 100% { + box-shadow: + 0 0 0 0 rgba(139, 92, 246, 0.8), + 0 0 0 0 rgba(139, 92, 246, 0.4); } - 50% { - box-shadow: 0 0 0 20px rgba(139, 92, 246, 0), - 0 0 0 40px rgba(139, 92, 246, 0.1); + 50% { + box-shadow: + 0 0 0 20px rgba(139, 92, 246, 0), + 0 0 0 40px rgba(139, 92, 246, 0.1); } } @@ -301,7 +351,9 @@ select:focus { } @keyframes textPulse { - 50% { opacity: 0.7; } + 50% { + opacity: 0.7; + } } /* No Data State */ @@ -336,7 +388,7 @@ select:focus { backdrop-filter: blur(30px); border-radius: 28px; border: 1px solid rgba(255, 255, 255, 0.08); - box-shadow: + box-shadow: 0 30px 60px rgba(0, 0, 0, 0.4), inset 0 1px 0 rgba(255, 255, 255, 0.05); } @@ -378,15 +430,17 @@ select:focus { } .coin-item::before { - content: ''; + content: ""; position: absolute; top: 0; left: 0; right: 0; height: 100%; - background: linear-gradient(135deg, - rgba(139, 92, 246, 0.1), - rgba(236, 72, 153, 0.1)); + background: linear-gradient( + 135deg, + rgba(139, 92, 246, 0.1), + rgba(236, 72, 153, 0.1) + ); opacity: 0; transition: opacity 0.4s ease; } @@ -394,7 +448,7 @@ select:focus { .coin-item:hover { transform: translateY(-12px) scale(1.05); border-color: rgba(139, 92, 246, 0.6); - box-shadow: + box-shadow: 0 35px 70px rgba(139, 92, 246, 0.4), 0 20px 40px rgba(0, 0, 0, 0.5); } @@ -404,11 +458,13 @@ select:focus { } .coin-item.active { - background: linear-gradient(135deg, - rgba(139, 92, 246, 0.2), - rgba(236, 72, 153, 0.2)); + background: linear-gradient( + 135deg, + rgba(139, 92, 246, 0.2), + rgba(236, 72, 153, 0.2) + ); border-color: #8b5cf6; - box-shadow: + box-shadow: 0 0 0 1px rgba(139, 92, 246, 0.5), 0 35px 70px rgba(139, 92, 246, 0.4); transform: scale(1.08); @@ -440,62 +496,66 @@ select:focus { /* Responsive */ @media (max-width: 768px) { - .features-container { padding: 1.5rem 1rem; } - - .features-card { - padding: 3rem 2rem; + .features-container { + padding: 1.5rem 1rem; + } + + .features-card { + padding: 3rem 2rem; margin-bottom: 2rem; border-radius: 24px; } - - .features-controls { + + .features-controls { gap: 1.5rem; flex-direction: column; align-items: center; } - - select { - width: 100%; + + select { + width: 100%; max-width: 350px; padding: 1.3rem 3.5rem 1.3rem 1.8rem; } - - .chart-wrapper { height: 450px; } - + + .chart-wrapper { + height: 450px; + } + .top-coins-display { padding: 2.5rem 1.5rem; margin-bottom: 2rem; } - + .coins-grid { grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 1rem; } - + .coin-item { padding: 1.5rem 1.2rem; } - + .coin-symbol { font-size: 2rem; } } @media (max-width: 480px) { - .features-title { - font-size: 2.5rem; + .features-title { + font-size: 2.5rem; margin-bottom: 2rem; } - - .features-card { - padding: 2.5rem 1.5rem; + + .features-card { + padding: 2.5rem 1.5rem; } - + .top-coins-title { font-size: 1.8rem; margin-bottom: 2rem; } - + .coins-grid { grid-template-columns: 1fr; gap: 1.2rem; @@ -548,4 +608,4 @@ select, .top-coins-display { border-width: 0.5px; } -} \ No newline at end of file +} diff --git a/src/components/Features.jsx b/src/components/Sections/Features.jsx similarity index 52% rename from src/components/Features.jsx rename to src/components/Sections/Features.jsx index 53a247f..1485f24 100644 --- a/src/components/Features.jsx +++ b/src/components/Sections/Features.jsx @@ -1,7 +1,6 @@ import React, { useEffect, useState } from "react"; import { Line } from "react-chartjs-2"; import "./Features.css"; -// eslint-disable-next-line no-unused-vars import { motion } from "framer-motion"; import { @@ -16,19 +15,14 @@ import { } from "chart.js"; ChartJS.register(LineElement, CategoryScale, LinearScale, PointElement, Tooltip, Legend, Filler); +import { topCoins } from "../../config/coins"; const Features = () => { /* Define coin data with brand-specific neon colors for the graph BTC: Orange, ETH: Blue-Purple, SOL: Green, BNB: Yellow, ADA: Cyan Blue */ - const topCoins = [ - { id: "bitcoin", name: "Bitcoin", symbol: "BTC", color: "#F7931A" }, - { id: "ethereum", name: "Ethereum", symbol: "ETH", color: "#627EEA" }, - { id: "solana", name: "Solana", symbol: "SOL", color: "#14F195" }, - { id: "binancecoin", name: "BNB", symbol: "BNB", color: "#F3BA2F" }, - { id: "cardano", name: "Cardano", symbol: "ADA", color: "#2979FF" }, - ]; + const [selectedCoin, setSelectedCoin] = useState(topCoins[0].id); const [days, setDays] = useState(1); @@ -79,12 +73,12 @@ const Features = () => { }; // Helper to convert hex to rgba for gradient - const hexToRgba = (hex, alpha) => { - const r = parseInt(hex.slice(1, 3), 16); - const g = parseInt(hex.slice(3, 5), 16); - const b = parseInt(hex.slice(5, 7), 16); - return `rgba(${r}, ${g}, ${b}, ${alpha})`; - }; + // const hexToRgba = (hex, alpha) => { + // const r = parseInt(hex.slice(1, 3), 16); + // const g = parseInt(hex.slice(3, 5), 16); + // const b = parseInt(hex.slice(5, 7), 16); + // return `rgba(${r}, ${g}, ${b}, ${alpha})`; + // }; const timeRanges = [ { value: 1, label: "24 Hours" }, @@ -127,6 +121,7 @@ const Features = () => { value={selectedCoin} onChange={(e) => setSelectedCoin(e.target.value)} className="cosmic-select" + aria-label="Select Coin" > {topCoins.map((coin) => (
- {/* CARD */} -
- {loading ? ( -
-
-

Loading {topCoins.find(c => c.id === selectedCoin)?.name} chart...

-
- ) : prices.length === 0 ? ( -
-

No data available

-

Please try a different coin or time range

-
- ) : ( -
- + {loading ? ( +
+
+

Loading {topCoins.find(c => c.id === selectedCoin)?.name} chart...

+
+ ) : prices.length === 0 ? ( +
+

No data available

+

Please try a different coin or time range

+
+ ) : ( +
+ -
- )} -
- - {/* TOP 5 COINS DISPLAY */} -
-

Top 5 Coins

-
- {topCoins.map((coin) => ( -
setSelectedCoin(coin.id)} - > - {coin.symbol} - {coin.name} + }} + />
- ))} + )} +
+ + {/* TOP 5 COINS DISPLAY */} +
+

Top 5 Coins

+
+ {topCoins.map((coin) => ( +
setSelectedCoin(coin.id)} + > + {coin.symbol} + {coin.name} +
+ ))} +
-
diff --git a/src/components/Pricing.css b/src/components/Sections/Pricing.css similarity index 74% rename from src/components/Pricing.css rename to src/components/Sections/Pricing.css index 751a893..6a4ff19 100644 --- a/src/components/Pricing.css +++ b/src/components/Sections/Pricing.css @@ -9,6 +9,12 @@ body { font-family: 'Inter', sans-serif; line-height: 1.6; } +section.py-20.px-8 { + max-width: 1200px; + +} +section.py-20.px-6 { + max-width: 1200px;} /* Glass Panels */ .glass-panel { @@ -80,7 +86,7 @@ body { .max-w-7xl table { width: 100% !important; - min-width: 800px; + min-width: 0; background: rgba(17, 24, 39, 0.95) !important; backdrop-filter: blur(40px) !important; border: 1px solid rgba(71, 85, 105, 0.3) !important; @@ -292,3 +298,116 @@ table tbody td:last-child { .dark table tbody td:first-child { color: #e2e8f0 !important; } +/* ========================================================= + PRICING PAGE — MASTER TEXT & GRAY UI CONTRAST FIX + Covers: FAQ, gray buttons, small text, glass panels, tables + ========================================================= */ + +/* 1. ALL low-contrast gray text → stronger slate */ +.text-gray-300, +.text-gray-400, +.text-gray-500, +.text-gray-600, +.text-gray-700 { + color: #334155 !important; /* slate-700 */ +} + +/* Dark mode: force bright readable text */ +.dark .text-gray-300, +.dark .text-gray-400, +.dark .text-gray-500, +.dark .text-gray-600, +.dark .text-gray-700 { + color: #e5e7eb !important; /* slate-200 */ +} + +/* 2. Glass panel text (cards, FAQ, modals, etc) */ +.glass-panel, +.glass-panel p, +.glass-panel span, +.glass-panel div, +.glass-panel li, +.glass-panel h1, +.glass-panel h2, +.glass-panel h3, +.glass-panel h4, +.glass-panel h5 { + color: #1f2937 !important; /* slate-800 */ +} + +.dark .glass-panel, +.dark .glass-panel p, +.dark .glass-panel span, +.dark .glass-panel div, +.dark .glass-panel li, +.dark .glass-panel h1, +.dark .glass-panel h2, +.dark .glass-panel h3, +.dark .glass-panel h4, +.dark .glass-panel h5 { + color: #f1f5f9 !important; /* slate-100 */ +} + +/* 3. FAQ section specific (questions + answers) */ +.glass-panel button, +.glass-panel button h4, +.glass-panel .px-10.pb-8 { + color: #0f172a !important; /* slate-900 */ +} + +.dark .glass-panel button, +.dark .glass-panel button h4, +.dark .glass-panel .px-10.pb-8 { + color: #f8fafc !important; /* near white */ +} + +/* 4. Gray buttons (Explorer, Close, neutral CTAs) */ +.bg-gray-700, +.bg-gray-600, +.bg-gray-500, +.from-gray-500, +.to-gray-600 { + color: #ffffff !important; + background-color: #334155 !important; /* slate-700 */ + border-color: #475569 !important; +} + +.dark .bg-gray-700, +.dark .bg-gray-600, +.dark .bg-gray-500, +.dark .from-gray-500, +.dark .to-gray-600 { + background-color: #1e293b !important; /* slate-900 */ + color: #ffffff !important; + border-color: #334155 !important; +} + +/* 5. Comparison table small labels & cells */ +table td, +table th, +table td span { + color: #1f2937 !important; +} + +.dark table td, +.dark table th, +.dark table td span { + color: #e5e7eb !important; +} + +/* 6. Small muted UI labels */ +.text-sm, +.text-xs { + color: #334155; +} + +.dark .text-sm, +.dark .text-xs { + color: #cbd5f5; +} + +/* 7. Improve overall text rendering */ +body { + -webkit-font-smoothing: antialiased; + text-rendering: optimizeLegibility; +} diff --git a/src/components/Pricing.jsx b/src/components/Sections/Pricing.jsx similarity index 77% rename from src/components/Pricing.jsx rename to src/components/Sections/Pricing.jsx index 33fc65e..5b6f68f 100644 --- a/src/components/Pricing.jsx +++ b/src/components/Sections/Pricing.jsx @@ -1,57 +1,69 @@ // Pricing.jsx - FIXED & PRODUCTION READY VERSION -import { useTheme } from "../context/ThemeContext"; +import { useTheme } from "../../context/useTheme"; import { useNavigate } from "react-router-dom"; import { useState, useEffect, useCallback } from "react"; import { motion, AnimatePresence } from "framer-motion"; -import './Pricing.css'; +import "./Pricing.css"; import { - FiCheck, FiX, FiInfo, FiArrowRight, FiStar, FiShield, FiClock, - FiUsers, FiTrendingUp, FiZap, FiPlus + FiCheck, + FiX, + FiInfo, + FiArrowRight, + FiStar, + FiShield, + FiClock, + FiUsers, + FiTrendingUp, + FiZap, + FiPlus, } from "react-icons/fi"; -import { plans, faqs, comparisonFeatures } from "../data/pricingPlansData"; +import { plans, faqs, comparisonFeatures } from "../../data/pricingPlansData"; export default function Pricing() { const { isDark } = useTheme(); const navigate = useNavigate(); const [billingCycle, setBillingCycle] = useState("monthly"); const [openFaqIndex, setOpenFaqIndex] = useState(null); - const [hoveredPlan, setHoveredPlan] = useState(null); const [scrollProgress, setScrollProgress] = useState(0); // FIXED: Proper scroll handler with cleanup useEffect(() => { const handleScroll = () => { const scrollTop = window.scrollY; - const docHeight = document.documentElement.scrollHeight - window.innerHeight; + const docHeight = + document.documentElement.scrollHeight - window.innerHeight; setScrollProgress(scrollTop / docHeight); }; - window.addEventListener('scroll', handleScroll, { passive: true }); - return () => window.removeEventListener('scroll', handleScroll); + window.addEventListener("scroll", handleScroll, { passive: true }); + return () => window.removeEventListener("scroll", handleScroll); }, []); // FIXED: Memoized handlers to prevent re-renders - const handlePlanClick = useCallback((planName) => { - if (planName === "Explorer") { - navigate("/signup"); - } else { - // Simulate payment modal - const modal = document.getElementById('payment-modal'); - if (modal) { - modal.showModal(); + const handlePlanClick = useCallback( + (planName) => { + if (planName === "Explorer") { + navigate("/signup"); } else { - alert("Payment coming soon! 🚀"); + // Simulate payment modal + const modal = document.getElementById("payment-modal"); + if (modal) { + modal.showModal(); + } else { + alert("Payment coming soon! 🚀"); + } } - } - }, [navigate]); + }, + [navigate], + ); const toggleBillingCycle = useCallback(() => { - setBillingCycle(prev => prev === "monthly" ? "yearly" : "monthly"); + setBillingCycle((prev) => (prev === "monthly" ? "yearly" : "monthly")); }, []); const getPrice = useCallback((planPrice, cycle) => { if (cycle === "yearly") { - const monthlyPrice = parseFloat(planPrice.replace('$', '')) || 0; + const monthlyPrice = parseFloat(planPrice.replace("$", "")) || 0; const yearlyPrice = (monthlyPrice * 12 * 0.83).toFixed(0); // 17% discount return `$${yearlyPrice}/year`; } @@ -65,9 +77,9 @@ export default function Pricing() { opacity: 1, transition: { staggerChildren: 0.1, - delayChildren: 0.2 - } - } + delayChildren: 0.2, + }, + }, }; const itemVariants = { @@ -75,8 +87,8 @@ export default function Pricing() { visible: { opacity: 1, y: 0, - transition: { duration: 0.6 } - } + transition: { duration: 0.6 }, + }, }; const cardVariants = { @@ -84,17 +96,17 @@ export default function Pricing() { visible: { opacity: 1, scale: 1, - transition: { duration: 0.5 } + transition: { duration: 0.5 }, }, hover: { y: -10, scale: 1.02, - transition: { duration: 0.3 } - } + transition: { duration: 0.3 }, + }, }; return ( -
+
{/* FIXED: Progress Bar */} - Choose the perfect plan for your trading journey with lightning-fast signals and enterprise-grade tools. + Choose the perfect plan for your trading journey with lightning-fast + signals and enterprise-grade tools. {/* FIXED: Working Billing Toggle */} @@ -154,7 +167,9 @@ export default function Pricing() { transition={{ type: "spring" }} >
- + Monthly @@ -163,29 +178,32 @@ export default function Pricing() { onClick={toggleBillingCycle} whileTap={{ scale: 0.95 }} whileHover={{ scale: 1.05 }} + aria-label="Toggle billing cycle" > - + Yearly
- {billingCycle === 'yearly' && ( + {billingCycle === "yearly" && (
{plans.map((plan) => { - const isHovered = hoveredPlan === plan?.name; const currentPrice = getPrice(plan?.price, billingCycle); return ( setHoveredPlan(plan.name)} - onHoverEnd={() => setHoveredPlan(null)} > {/* Card Background */}
- {/* Popular Badge */} {plan.highlight && ( -

- {plan.name} -

+

{plan.name}

{currentPrice} @@ -266,33 +278,46 @@ export default function Pricing() { {plan.features.map((feature, i) => ( -
- {feature.available ? : } +
+ {feature.available ? ( + + ) : ( + + )}
- {feature.label} + + {feature.label} + ))}
{/* CTA Button */} handlePlanClick(plan.name)} whileHover={{ scale: 1.05 }} whileTap={{ scale: 0.98 }} > - {plan.name === "Explorer" ? "Get Started Free" : "Upgrade Now"} + {plan.name === "Explorer" + ? "Get Started Free" + : "Upgrade Now"} @@ -329,7 +354,9 @@ export default function Pricing() { {plans.map((plan) => (
-
{plan.name}
+
+ {plan.name} +
{getPrice(plan.price, billingCycle)}
@@ -345,14 +372,20 @@ export default function Pricing() { {comparisonFeatures.map((feature, index) => ( - + {feature.name} {plans.map((plan) => { - const available = plan.features.find(f => - f.label?.toLowerCase().includes(feature.name.toLowerCase()) - )?.available || false; + const available = + plan.features.find((f) => + f.label + ?.toLowerCase() + .includes(feature.name.toLowerCase()), + )?.available || false; return ( {available ? ( @@ -406,7 +439,9 @@ export default function Pricing() { > setOpenFaqIndex(openFaqIndex === index ? null : index)} + onClick={() => + setOpenFaqIndex(openFaqIndex === index ? null : index) + } whileTap={{ scale: 0.98 }} >
@@ -443,7 +478,10 @@ export default function Pricing() { {/* FIXED: Payment Modal */} - +

- Secure payment processing is rolling out soon. - You'll be the first to know when it's live! + Secure payment processing is rolling out soon. You'll be the first + to know when it's live!

); -} \ No newline at end of file +} diff --git a/src/components/contributors.css b/src/components/Sections/contributors.css similarity index 59% rename from src/components/contributors.css rename to src/components/Sections/contributors.css index d12e3f4..11c9766 100644 --- a/src/components/contributors.css +++ b/src/components/Sections/contributors.css @@ -4,61 +4,61 @@ --primary-200: #d6b4fc; --primary-300: #c084fc; --primary-400: #a855f7; - --primary-500: #9333ea; + --primary-500: #9333ea; --primary-600: #7e22ce; --primary-700: #6b21a8; --primary-800: #581c87; - --primary-900: #3b0764; - + --primary-900: #3b0764; + --secondary-400: #60a5fa; - --secondary-500: #3b82f6; + --secondary-500: #3b82f6; --secondary-600: #2563eb; --secondary-700: #1d4ed8; - + --accent-cyan: #06b6d4; --accent-teal: #14b8a6; - + --gold-400: #facc15; - --gold-500: #eab308; + --gold-500: #eab308; --gold-600: #ca8a04; --silver-300: #d1d5db; --silver-400: #9ca3af; --bronze-400: #f59e0b; --bronze-500: #d97706; - - --bg-body: #0a0315; - --bg-surface: #120537; + + --bg-body: #0a0315; + --bg-surface: #120537; --bg-card: rgba(18, 5, 55, 0.8); --bg-glass: rgba(255, 255, 255, 0.08); --bg-glass-hover: rgba(255, 255, 255, 0.12); - + --text-primary: #ffffff; --text-secondary: #e5d8ff; --text-muted: #a78bfa; --text-accent: var(--gold-500); - + --border-base: rgba(147, 51, 234, 0.2); --border-hover: rgba(59, 130, 246, 0.4); --border-active: rgba(147, 51, 234, 0.6); - + --gradient-primary: linear-gradient(135deg, var(--primary-500) 0%, var(--primary-700) 50%, var(--primary-900) 100%); --gradient-secondary: linear-gradient(135deg, var(--secondary-500) 0%, var(--accent-cyan) 50%, var(--secondary-700) 100%); --gradient-gold: linear-gradient(135deg, var(--gold-400) 0%, var(--gold-500) 50%, #ca8a04 100%); --gradient-silver: linear-gradient(135deg, #e5e7eb 0%, #d1d5db 50%, #9ca3af 100%); --gradient-bronze: linear-gradient(135deg, var(--bronze-400) 0%, var(--bronze-500) 50%, #b45309 100%); - --gradient-glass: linear-gradient(135deg, rgba(147,51,234,0.1) 0%, rgba(59,130,246,0.1) 50%, rgba(6,182,212,0.1) 100%); - + --gradient-glass: linear-gradient(135deg, rgba(147, 51, 234, 0.1) 0%, rgba(59, 130, 246, 0.1) 50%, rgba(6, 182, 212, 0.1) 100%); + --shadow-sm: 0 4px 14px rgba(10, 3, 21, 0.4); --shadow-md: 0 12px 32px rgba(10, 3, 21, 0.6); --shadow-lg: 0 24px 64px rgba(10, 3, 21, 0.8); --shadow-purple-glow: 0 0 32px rgba(147, 51, 234, 0.4); --shadow-blue-glow: 0 0 32px rgba(59, 130, 246, 0.4); --shadow-gold-glow: 0 0 24px rgba(234, 179, 8, 0.6); - + --radius-sm: 12px; --radius-md: 20px; --radius-lg: 28px; - + --transition-fast: all 0.25s cubic-bezier(0.4, 0, 0.2, 1); --transition-normal: all 0.4s cubic-bezier(0.4, 0, 0.2, 1); --transition-slow: all 0.6s cubic-bezier(0.4, 0, 0.2, 1); @@ -86,19 +86,29 @@ body::before { left: 0; width: 100%; height: 100%; - background-image: - radial-gradient(circle at 20% 80%, rgba(147,51,234,0.1) 0%, transparent 50%), - radial-gradient(circle at 80% 20%, rgba(59,130,246,0.1) 0%, transparent 50%), - radial-gradient(circle at 40% 40%, rgba(6,182,212,0.08) 0%, transparent 50%); + background-image: + radial-gradient(circle at 20% 80%, rgba(147, 51, 234, 0.1) 0%, transparent 50%), + radial-gradient(circle at 80% 20%, rgba(59, 130, 246, 0.1) 0%, transparent 50%), + radial-gradient(circle at 40% 40%, rgba(6, 182, 212, 0.08) 0%, transparent 50%); pointer-events: none; z-index: -1; animation: bgShift 20s ease-in-out infinite; } @keyframes bgShift { - 0%, 100% { transform: scale(1) rotate(0deg); } - 33% { transform: scale(1.05) rotate(1deg); } - 66% { transform: scale(1.02) rotate(-1deg); } + + 0%, + 100% { + transform: scale(1) rotate(0deg); + } + + 33% { + transform: scale(1.05) rotate(1deg); + } + + 66% { + transform: scale(1.02) rotate(-1deg); + } } .contributors-page { @@ -115,6 +125,7 @@ body::before { opacity: 0; transform: translateY(40px) scale(0.95); } + to { opacity: 1; transform: translateY(0) scale(1); @@ -122,34 +133,62 @@ body::before { } @keyframes cosmicFloat { - 0%, 100% { transform: translateY(0px) rotate(0deg); } - 33% { transform: translateY(-12px) rotate(1deg); } - 66% { transform: translateY(-6px) rotate(-1deg); } + + 0%, + 100% { + transform: translateY(0px) rotate(0deg); + } + + 33% { + transform: translateY(-12px) rotate(1deg); + } + + 66% { + transform: translateY(-6px) rotate(-1deg); + } } @keyframes pulseNeon { - 0%, 100% { + + 0%, + 100% { box-shadow: var(--shadow-md); border-color: var(--border-base); } - 50% { + + 50% { box-shadow: var(--shadow-purple-glow), var(--shadow-lg); border-color: var(--border-active); } } @keyframes shimmerWave { - 0% { background-position: -200% 0; } - 100% { background-position: 200% 0; } + 0% { + background-position: -200% 0; + } + + 100% { + background-position: 200% 0; + } } @keyframes rotateGlow { - from { filter: hue-rotate(0deg) brightness(1); } - to { filter: hue-rotate(360deg) brightness(1.1); } + from { + filter: hue-rotate(0deg) brightness(1); + } + + to { + filter: hue-rotate(360deg) brightness(1.1); + } } -.fade-in-up { animation: fadeInUp 1s cubic-bezier(0.25, 0.46, 0.45, 0.94) forwards; } -.cosmic-float { animation: cosmicFloat 4s ease-in-out infinite; } +.fade-in-up { + animation: fadeInUp 1s cubic-bezier(0.25, 0.46, 0.45, 0.94) forwards; +} + +.cosmic-float { + animation: cosmicFloat 4s ease-in-out infinite; +} .contributors-header { text-align: center; @@ -177,7 +216,10 @@ body::before { animation: shimmerWave 3s infinite; } -.contributors-header > * { position: relative; z-index: 2; } +.contributors-header>* { + position: relative; + z-index: 2; +} .contributors-title { font-size: clamp(3rem, 6vw, 5rem); @@ -241,12 +283,11 @@ body::before { left: -150%; width: 150%; height: 100%; - background: linear-gradient(90deg, - transparent, - rgba(147,51,234,0.1), - rgba(59,130,246,0.1), - transparent - ); + background: linear-gradient(90deg, + transparent, + rgba(147, 51, 234, 0.1), + rgba(59, 130, 246, 0.1), + transparent); transition: left 0.8s; } @@ -398,14 +439,14 @@ body::before { .contributors-grid { display: grid; - grid-template-columns: repeat(auto-fill, minmax(380px, 1fr)); + grid-template-columns: repeat(auto-fill, minmax(340px, 1fr)); gap: 2.5rem; margin-top: 2rem; + contain: layout style; } .contributor-card { - background: var(--bg-card); - backdrop-filter: blur(32px); + background: linear-gradient(168deg, rgba(18, 5, 55, 0.95) 0%, rgba(10, 3, 21, 0.98) 100%); border: 1px solid var(--border-base); border-radius: var(--radius-lg); padding: 2.5rem; @@ -415,37 +456,79 @@ body::before { cursor: pointer; height: 100%; box-shadow: var(--shadow-md); + display: flex; + flex-direction: column; } +/* Top animated gradient bar */ .contributor-card::before { content: ''; position: absolute; top: 0; left: 0; right: 0; - height: 4px; + height: 3px; background: var(--gradient-primary); transform: scaleX(0); - transition: transform 0.4s ease; + transform-origin: left; + transition: transform 0.5s cubic-bezier(0.22, 1, 0.36, 1); z-index: 2; } +/* Inner corner accent glow */ +.contributor-card::after { + content: ''; + position: absolute; + top: -80px; + right: -80px; + width: 160px; + height: 160px; + border-radius: 50%; + background: radial-gradient(circle, rgba(147, 51, 234, 0.12) 0%, transparent 70%); + transition: var(--transition-slow); + pointer-events: none; +} + .contributor-card:hover::before { transform: scaleX(1); } +.contributor-card:hover::after { + top: -40px; + right: -40px; + background: radial-gradient(circle, rgba(59, 130, 246, 0.2) 0%, transparent 70%); +} + .contributor-card:hover { - transform: translateY(-16px) scale(1.03); - box-shadow: var(--shadow-purple-glow), var(--shadow-lg); + transform: translateY(-10px) scale(1.02); + box-shadow: + 0 0 24px rgba(147, 51, 234, 0.25), + 0 20px 60px rgba(10, 3, 21, 0.7), + inset 0 1px 0 rgba(255, 255, 255, 0.06); border-color: var(--border-hover); - animation: pulseNeon 2.5s infinite; + background: linear-gradient(168deg, rgba(24, 8, 70, 0.98) 0%, rgba(14, 5, 35, 1) 100%); +} + +/* ===== RANK-SPECIFIC CARD THEMES ===== */ +.contributor-card.contributor-rank-gold { + border-color: rgba(234, 179, 8, 0.2); + background: linear-gradient(168deg, rgba(30, 18, 5, 0.95) 0%, rgba(10, 3, 21, 0.98) 100%); } -.contributor-rank-gold { - border-left: 6px solid transparent; - background: linear-gradient(to right, var(--gold-500), transparent) 0 0 / 6px 100%; - background-origin: border-box; - background-repeat: no-repeat; +.contributor-card.contributor-rank-gold::before { + background: var(--gradient-gold); +} + +.contributor-card.contributor-rank-gold::after { + background: radial-gradient(circle, rgba(234, 179, 8, 0.1) 0%, transparent 70%); +} + +.contributor-card.contributor-rank-gold:hover { + border-color: rgba(234, 179, 8, 0.4); + box-shadow: + 0 0 28px rgba(234, 179, 8, 0.2), + 0 20px 60px rgba(10, 3, 21, 0.7), + inset 0 1px 0 rgba(234, 179, 8, 0.1); } .contributor-rank-gold .contributor-rank { @@ -453,14 +536,23 @@ body::before { -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; - text-shadow: 0 4px 16px rgba(234, 179, 8, 0.4); } -.contributor-rank-silver { - border-left: 6px solid transparent; - background: linear-gradient(to right, var(--silver-300), transparent) 0 0 / 6px 100%; - background-origin: border-box; - background-repeat: no-repeat; +.contributor-card.contributor-rank-silver { + border-color: rgba(209, 213, 219, 0.15); + background: linear-gradient(168deg, rgba(20, 15, 35, 0.95) 0%, rgba(10, 3, 21, 0.98) 100%); +} + +.contributor-card.contributor-rank-silver::before { + background: var(--gradient-silver); +} + +.contributor-card.contributor-rank-silver:hover { + border-color: rgba(209, 213, 219, 0.35); + box-shadow: + 0 0 28px rgba(209, 213, 219, 0.15), + 0 20px 60px rgba(10, 3, 21, 0.7), + inset 0 1px 0 rgba(209, 213, 219, 0.08); } .contributor-rank-silver .contributor-rank { @@ -470,11 +562,21 @@ body::before { background-clip: text; } -.contributor-rank-bronze { - border-left: 6px solid transparent; - background: linear-gradient(to right, var(--bronze-500), transparent) 0 0 / 6px 100%; - background-origin: border-box; - background-repeat: no-repeat; +.contributor-card.contributor-rank-bronze { + border-color: rgba(217, 119, 6, 0.2); + background: linear-gradient(168deg, rgba(25, 12, 5, 0.95) 0%, rgba(10, 3, 21, 0.98) 100%); +} + +.contributor-card.contributor-rank-bronze::before { + background: var(--gradient-bronze); +} + +.contributor-card.contributor-rank-bronze:hover { + border-color: rgba(217, 119, 6, 0.4); + box-shadow: + 0 0 28px rgba(217, 119, 6, 0.2), + 0 20px 60px rgba(10, 3, 21, 0.7), + inset 0 1px 0 rgba(217, 119, 6, 0.08); } .contributor-rank-bronze .contributor-rank { @@ -482,29 +584,45 @@ body::before { -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; - text-shadow: 0 4px 16px rgba(217, 119, 6, 0.4); } -.contributor-rank-contributor { - border-left: 6px solid transparent; - background: linear-gradient(to right, var(--primary-400), transparent) 0 0 / 6px 100%; - background-origin: border-box; - background-repeat: no-repeat; +.contributor-card.contributor-rank-contributor { + border-color: rgba(168, 85, 247, 0.15); +} + +.contributor-card.contributor-rank-contributor::before { + background: linear-gradient(90deg, var(--primary-500), var(--accent-cyan)); } .contributor-header { display: flex; align-items: center; gap: 1.5rem; - margin-bottom: 2rem; + margin-bottom: 1.75rem; } .contributor-avatar-wrapper { position: relative; - width: 90px; - height: 90px; + width: 80px; + height: 80px; flex-shrink: 0; - animation: cosmicFloat 6s ease-in-out infinite; +} + +/* Ring around avatar */ +.contributor-avatar-wrapper::before { + content: ''; + position: absolute; + inset: -4px; + border-radius: 50%; + background: conic-gradient(from 180deg, var(--primary-500), var(--accent-cyan), var(--primary-500)); + opacity: 0; + transition: var(--transition-normal); + z-index: 0; +} + +.contributor-card:hover .contributor-avatar-wrapper::before { + opacity: 1; + animation: rotateGlow 3s linear infinite; } .contributor-avatar { @@ -512,73 +630,97 @@ body::before { height: 100%; border-radius: 50%; object-fit: cover; - border: 4px solid var(--border-base); + border: 3px solid rgba(147, 51, 234, 0.3); transition: var(--transition-normal); - box-shadow: var(--shadow-sm); + box-shadow: 0 4px 16px rgba(10, 3, 21, 0.6); + position: relative; + z-index: 1; + background: var(--bg-surface); } .contributor-card:hover .contributor-avatar { - border-color: var(--secondary-500); - transform: scale(1.15) rotate(5deg); - box-shadow: var(--shadow-blue-glow), 0 0 32px rgba(59, 130, 246, 0.6); - animation: rotateGlow 2s linear infinite; + border-color: transparent; + transform: scale(1.08); + box-shadow: 0 0 20px rgba(59, 130, 246, 0.3); +} + +.contributor-basic-info { + min-width: 0; } .contributor-basic-info h2 { - font-size: 1.6rem; + font-size: 1.45rem; font-weight: 800; margin-bottom: 0.5rem; - background: var(--gradient-secondary); + background: linear-gradient(135deg, #fff 0%, var(--text-secondary) 100%); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.contributor-card:hover .contributor-basic-info h2 { + background: var(--gradient-secondary); + -webkit-background-clip: text; + background-clip: text; } .contributor-rank { - font-size: 1rem; + font-size: 0.78rem; font-weight: 700; - padding: 0.5rem 1.25rem; - border-radius: var(--radius-md); + padding: 0.35rem 0.9rem; + border-radius: 100px; text-transform: uppercase; - letter-spacing: 1px; - font-family: 'SF Mono', monospace; - box-shadow: var(--shadow-sm); + letter-spacing: 1.5px; + font-family: 'SF Mono', 'Roboto Mono', monospace; + background: rgba(255, 255, 255, 0.06); + border: 1px solid rgba(255, 255, 255, 0.08); + display: inline-block; } .contributor-stats { - display: flex; - gap: 2rem; - margin-bottom: 2.5rem; - padding: 1.5rem 0; - border-top: 1px solid var(--border-base); - border-bottom: 1px solid var(--border-base); + display: grid; + grid-template-columns: 1fr 1fr; + gap: 0; + margin-bottom: 1.75rem; + padding: 0; + background: rgba(255, 255, 255, 0.03); + border-radius: var(--radius-sm); + border: 1px solid rgba(255, 255, 255, 0.04); + overflow: hidden; } .contributor-stat-item { - flex: 1; text-align: center; - padding: 1rem; + padding: 1.1rem 0.75rem; + position: relative; +} + +.contributor-stat-item:first-child { + border-right: 1px solid rgba(255, 255, 255, 0.06); } .contributor-stat-label { display: block; - font-size: 0.9rem; + font-size: 0.7rem; color: var(--text-muted); text-transform: uppercase; - letter-spacing: 1px; - margin-bottom: 0.5rem; + letter-spacing: 1.5px; + margin-bottom: 0.4rem; font-weight: 600; } .contributor-stat-value { - font-size: 2.2rem; + font-size: 2rem; font-weight: 900; background: var(--gradient-gold); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; - line-height: 1; - font-family: 'SF Mono', monospace; + line-height: 1.1; + font-family: 'SF Mono', 'Roboto Mono', monospace; } .btn { @@ -608,11 +750,10 @@ body::before { left: -100%; width: 100%; height: 100%; - background: linear-gradient(90deg, - transparent, - rgba(255,255,255,0.3), - transparent - ); + background: linear-gradient(90deg, + transparent, + rgba(255, 255, 255, 0.3), + transparent); transition: left 0.6s; z-index: 1; } @@ -649,8 +790,18 @@ body::before { .contributor-actions { display: flex; - gap: 1.5rem; + gap: 0.875rem; flex-wrap: wrap; + margin-top: auto; +} + +.contributor-actions .btn { + flex: 1; + min-width: 0; + justify-content: center; + padding: 0.875rem 1.25rem; + font-size: 0.85rem; + border-radius: var(--radius-sm); } /* ===== ELEGANT MODAL ===== */ @@ -674,7 +825,9 @@ body::before { max-width: 700px; max-height: 85vh; width: 92%; - overflow-y: auto; + display: flex; + flex-direction: column; + overflow: hidden; animation: scaleIn 0.4s cubic-bezier(0.34, 1.56, 0.64, 1); box-shadow: var(--shadow-lg), var(--shadow-purple-glow); position: relative; @@ -702,9 +855,9 @@ body::before { align-items: center; padding: 2.5rem 2.5rem 2rem; border-bottom: 1px solid var(--border-base); - position: sticky; - top: 0; - background: linear-gradient(to bottom, var(--bg-surface), transparent); + position: relative; + flex-shrink: 0; + background: var(--bg-surface); z-index: 10; } @@ -742,7 +895,9 @@ body::before { .contributors-modal-body { padding: 0 2.5rem 2.5rem; - max-height: 550px; + overflow-y: auto; + flex: 1; + -webkit-overflow-scrolling: touch; } .contributors-modal-pr-list { @@ -843,54 +998,108 @@ body::before { } @media (max-width: 1024px) { - .contributors-grid { grid-template-columns: repeat(auto-fill, minmax(340px, 1fr)); } + .contributors-grid { + grid-template-columns: repeat(auto-fill, minmax(340px, 1fr)); + } } @media (max-width: 768px) { - .contributors-page { padding: 1.5rem 1rem; } - .contributors-controls { - flex-direction: column; - align-items: stretch; + .contributors-page { + padding: 1.5rem 1rem; + } + + .contributors-controls { + justify-content: center; + align-items: center; gap: 1.5rem; } - .contributors-search-input { min-width: auto; } - .contributors-grid { - grid-template-columns: 1fr; - gap: 2rem; + + .contributors-search-input { + min-width: 200px; } - .contributor-stats { - flex-direction: column; - gap: 1.5rem; + + .contributors-grid { + grid-template-columns: 1fr; + gap: 2rem; } - .contributor-actions { flex-direction: column; } - .contributors-stats { gap: 1.5rem; } - .contributors-modal-header { - flex-direction: column; - gap: 1.5rem; - text-align: center; + + .contributor-stats { + flex-direction: column; + gap: 1.5rem; + } + + .contributor-actions { + flex-direction: column; + } + + .contributors-stats { + gap: 1.5rem; + } + + .contributors-modal-header { + flex-direction: column; + gap: 1.5rem; + text-align: center; + } + + .contributors-filters { + flex-direction: column; + } + .contributors-modal { + max-height: 90vh; + width: 95%; } } @media (max-width: 480px) { - .contributors-header { padding: 2.5rem 1.5rem; } - .contributors-stats { flex-direction: column; align-items: center; } - .contributors-stat-card { min-width: 280px; } - .contributor-card { padding: 2rem; } + .contributors-header { + padding: 2.5rem 1.5rem; + } + + .contributors-stats { + flex-direction: column; + align-items: center; + } + + .contributors-stat-card { + min-width: 280px; + } + + .contributor-card { + padding: 2rem; + } } -.contributor-card, .btn, .contributors-select, .contributors-search-input { - will-change: transform; +.contributor-card, +.btn, +.contributors-select, +.contributors-search-input { backface-visibility: hidden; transform: translateZ(0); } +.contributor-card:hover, +.btn:hover, +.contributors-select:hover, +.contributors-search-input:focus { + will-change: transform; +} + +.contributor-card { + contain: layout style; +} + @media (prefers-reduced-motion: reduce) { - * { animation-duration: 0.01ms !important; transition-duration: 0.2s !important; } + * { + animation-duration: 0.01ms !important; + transition-duration: 0.2s !important; + } } /* Custom Scrollbar for Grid */ .contributors-grid::-webkit-scrollbar { - width: 12px; height: 12px; + width: 12px; + height: 12px; } .contributors-grid::-webkit-scrollbar-track { @@ -909,192 +1118,318 @@ body::before { @media print { :root { --bg-body: white; - --bg-card: rgba(255,255,255,0.9); + --bg-card: rgba(255, 255, 255, 0.9); --text-primary: black; } - .btn, .contributors-modal { display: none !important; } + + .btn, + .contributors-modal { + display: none !important; + } } -/* Project admin section card */ +/* ===== PROJECT ADMIN SECTION — PREMIUM CARD ===== */ .project-admin-section { - margin: 3rem 0; - padding: 0; + display: flex; + justify-content: center; + align-items: center; + margin: 3rem auto; + padding: 0 1rem; + } .project-admin-container { - background: rgba(18, 5, 55, 0.9); - backdrop-filter: blur(40px); - border: 2px solid rgba(147, 51, 234, 0.6); + background: + linear-gradient(165deg, rgba(24, 10, 65, 0.97) 0%, rgba(12, 4, 30, 0.99) 60%, rgba(18, 8, 50, 0.97) 100%) padding-box, + linear-gradient(90deg, var(--gold-500), var(--primary-500), var(--accent-cyan), var(--gold-500)) border-box; + background-size: 100% 100%, 300% 100%; + border: 3px solid transparent; border-radius: var(--radius-lg); padding: 3rem; position: relative; overflow: hidden; - box-shadow: var(--shadow-lg), 0 0 40px rgba(147, 51, 234, 0.3); + box-shadow: + 0 24px 80px rgba(10, 3, 21, 0.7), + 0 0 40px rgba(147, 51, 234, 0.15); transition: var(--transition-slow); - max-width: 600px; - margin: 0 auto 3rem; + max-width: 640px; + margin: 3rem auto; + animation: shimmerWave 4s linear infinite; } -.project-admin-container::before { + + +/* Corner accent orb */ +.project-admin-container::after { content: ''; position: absolute; - top: 0; - left: 0; - right: 0; - height: 6px; - background: var(--gradient-primary); - z-index: 2; + top: -100px; + right: -100px; + width: 250px; + height: 250px; + border-radius: 50%; + background: radial-gradient(circle, rgba(234, 179, 8, 0.08) 0%, transparent 65%); + pointer-events: none; + transition: var(--transition-slow); } .project-admin-container:hover { - transform: translateY(-8px) scale(1.02); - box-shadow: var(--shadow-purple-glow), 0 0 60px rgba(147, 51, 234, 0.5); - border-color: var(--secondary-500); + transform: translateY(-6px); + box-shadow: + 0 0 32px rgba(234, 179, 8, 0.15), + 0 32px 80px rgba(10, 3, 21, 0.8), + inset 0 1px 0 rgba(234, 179, 8, 0.08); + border-color: rgba(234, 179, 8, 0.3); +} + +.project-admin-container:hover::after { + top: -60px; + right: -60px; + background: radial-gradient(circle, rgba(234, 179, 8, 0.14) 0%, transparent 65%); } .project-admin-header { display: flex; align-items: center; - gap: 2rem; + gap: 1.75rem; margin-bottom: 2rem; - padding-bottom: 1.5rem; - border-bottom: 1px solid var(--border-base); + padding-bottom: 1.75rem; + border-bottom: 1px solid rgba(234, 179, 8, 0.1); + position: relative; + z-index: 1; } .project-admin-avatar-wrapper { position: relative; - width: 110px; - height: 110px; + width: 120px; + height: 120px; flex-shrink: 0; } +/* Rotating ring around admin avatar */ +.project-admin-avatar-wrapper::before { + content: ''; + position: absolute; + inset: -5px; + border-radius: 50%; + background: conic-gradient(from 0deg, var(--gold-500), var(--primary-500), var(--accent-cyan), var(--gold-500)); + animation: rotateGlow 4s linear infinite; + z-index: 0; +} + +.project-admin-avatar-wrapper::after { + content: ''; + position: absolute; + inset: -2px; + border-radius: 50%; + background: var(--bg-surface); + z-index: 0; +} + .project-admin-avatar { width: 100%; height: 100%; border-radius: 50%; - border: 5px solid var(--secondary-500); - box-shadow: 0 0 32px rgba(59, 130, 246, 0.6); + border: none; + box-shadow: 0 4px 20px rgba(10, 3, 21, 0.6); transition: var(--transition-normal); object-fit: cover; + position: relative; + z-index: 1; } .project-admin-container:hover .project-admin-avatar { - transform: scale(1.1) rotate(5deg); - border-color: var(--gold-500); - box-shadow: 0 0 48px rgba(234, 179, 8, 0.8); + transform: scale(1.06); + box-shadow: 0 0 24px rgba(234, 179, 8, 0.3); } .admin-badge { position: absolute; - bottom: -8px; - right: -8px; - width: 40px; - height: 40px; + bottom: 0; + right: 0; + transform: translate(25%, 25%); + width: 36px; + height: 36px; background: var(--gradient-gold); border-radius: 50%; display: flex; align-items: center; justify-content: center; - font-size: 1.2rem; - box-shadow: var(--shadow-gold-glow); - animation: cosmicFloat 3s ease-in-out infinite; + font-size: 1rem; + box-shadow: + 0 0 12px rgba(234, 179, 8, 0.6), + 0 2px 8px rgba(10, 3, 21, 0.5); + z-index: 2; + border: 2px solid var(--bg-surface); +} + +.project-admin-info { + position: relative; + z-index: 1; } .project-admin-info h2 { - font-size: 2rem; + font-size: 1.75rem; font-weight: 900; - background: var(--gradient-gold); + background: linear-gradient(135deg, var(--gold-400) 0%, #fff 50%, var(--gold-500) 100%); + background-size: 200% 100%; -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; - margin-bottom: 0.5rem; + margin-bottom: 0.35rem; + letter-spacing: -0.02em; } .project-admin-username { - font-size: 1.4rem; - color: var(--secondary-500); - font-weight: 700; - margin-bottom: 0.5rem; - font-family: 'SF Mono', monospace; + font-size: 1.15rem; + color: var(--secondary-400); + font-weight: 600; + margin-bottom: 0.35rem; + font-family: 'SF Mono', 'Roboto Mono', monospace; + opacity: 0.9; } .project-admin-description { - color: var(--text-secondary); - font-size: 1.1rem; + color: var(--text-muted); + font-size: 0.95rem; font-style: italic; + opacity: 0.85; } .project-admin-repo { - margin-bottom: 2.5rem; + margin-bottom: 2rem; + position: relative; + z-index: 1; } .project-admin-repo-title { - font-size: 1.2rem; + font-size: 0.8rem; color: var(--text-muted); text-transform: uppercase; - letter-spacing: 1px; - margin-bottom: 1rem; - font-weight: 600; + letter-spacing: 2px; + margin-bottom: 0.75rem; + font-weight: 700; } .project-admin-repo-link { display: flex; align-items: center; + justify-content: space-between; gap: 1rem; - padding: 1.25rem 2rem; - background: var(--bg-glass); - border: 2px solid var(--border-hover); - border-radius: var(--radius-md); + padding: 1rem 1.5rem; + background: rgba(255, 255, 255, 0.03); + border: 1px solid rgba(255, 255, 255, 0.06); + border-radius: var(--radius-sm); cursor: pointer; transition: var(--transition-normal); font-weight: 600; - font-size: 1.15rem; + font-size: 1.05rem; + color: var(--text-primary); } .project-admin-repo-link:hover { - background: var(--secondary-500); - border-color: var(--secondary-500); - transform: translateX(8px); - box-shadow: var(--shadow-blue-glow); + background: rgba(59, 130, 246, 0.1); + border-color: rgba(59, 130, 246, 0.3); + transform: translateX(4px); + box-shadow: 0 0 16px rgba(59, 130, 246, 0.15); } .repo-icon { - font-size: 1.5rem; + font-size: 1.3rem; transition: var(--transition-fast); } .project-admin-repo-link:hover .repo-icon { - transform: scale(1.2) rotate(360deg); + transform: scale(1.15); } .project-admin-actions { display: flex; - gap: 1.5rem; + gap: 1rem; flex-wrap: wrap; + position: relative; + z-index: 1; } .project-admin-btn { flex: 1; - min-width: 200px; + min-width: 180px; justify-content: center; - font-size: 1.05rem; - padding: 1.25rem 2rem; + font-size: 0.95rem; + padding: 1rem 1.75rem; + border-radius: var(--radius-sm); } @media (max-width: 768px) { .project-admin-header { flex-direction: column; text-align: center; - gap: 1.5rem; + gap: 1.25rem; } - + .project-admin-actions { flex-direction: column; } - + .project-admin-container { - margin: 0 0 2rem; + margin: 2rem auto; padding: 2rem; + max-width: 100%; + text-align: center; + } + + .project-admin-avatar-wrapper { + width: 110px; + height: 110px; } } + +.contributors-content { + display: flex; + flex-direction: column; + gap: 3rem; + +} + +/* ===== PAGINATION ===== */ +.contributors-pagination { + display: flex; + justify-content: center; + align-items: center; + gap: 0.5rem; + margin-top: 3rem; + flex-wrap: wrap; +} + +.pagination-btn { + background: var(--bg-glass); + border: 1px solid var(--border-base); + color: var(--text-secondary); + padding: 0.5rem 1rem; + border-radius: var(--radius-sm); + cursor: pointer; + transition: var(--transition-fast); + font-family: inherit; + font-weight: 600; +} + +.pagination-btn:hover:not(:disabled) { + background: var(--bg-glass-hover); + border-color: var(--secondary-500); + color: white; + transform: translateY(-2px); +} + +.pagination-btn.active { + background: var(--gradient-primary); + color: white; + border-color: transparent; + box-shadow: var(--shadow-purple-glow); +} + +.pagination-btn:disabled { + opacity: 0.5; + cursor: not-allowed; + pointer-events: none; +} \ No newline at end of file diff --git a/src/components/Signup.css b/src/components/Signup.css deleted file mode 100644 index 25a3444..0000000 --- a/src/components/Signup.css +++ /dev/null @@ -1,44 +0,0 @@ -/* Reuse Login CSS for consistency */ -@import "./Login.css"; - -/* Signup specific overrides if any */ -.signup-card { - max-width: 520px; -} - -.terms-checkbox { - margin-bottom: 25px; - display: flex; - align-items: center; - /* Vertical center */ - justify-content: center; - /* Horizontal center */ - gap: 12px; - font-size: 0.9rem; - color: var(--text-muted); - line-height: 1.4; -} - -.terms-checkbox input { - margin: 0; - /* Remove manual offset */ - accent-color: var(--neon-cyan); - width: 17px; - height: 17px; - cursor: pointer; - flex-shrink: 0; -} - -.terms-checkbox label { - cursor: pointer; - user-select: none; -} - -.terms-link { - color: var(--neon-cyan); - font-weight: 500; -} - -.terms-link:hover { - text-decoration: underline; -} \ No newline at end of file diff --git a/src/components/__tests__/Footer.test.jsx b/src/components/__tests__/Footer.test.jsx new file mode 100644 index 0000000..3f5381a --- /dev/null +++ b/src/components/__tests__/Footer.test.jsx @@ -0,0 +1,63 @@ +import { render, screen, fireEvent, waitFor } from '@testing-library/react'; +import { describe, it, expect } from 'vitest'; +import { BrowserRouter } from 'react-router-dom'; +import Footer from '../Footer'; + +// Wrap component with providers +const renderWithProviders = (component) => { + return render( + + {component} + + ); +}; + +describe('Footer Component', () => { + it('renders the newsletter subscription form', () => { + renderWithProviders(
); + expect(screen.getByPlaceholderText(/enter your email/i)).toBeInTheDocument(); + }); + + it('validates invalid email format', async () => { + renderWithProviders(
); + + const input = screen.getByPlaceholderText(/enter your email/i); + const button = screen.getByRole('button', { name: /subscribe/i }); + + // Disable HTML5 validation to allow custom validation logic to run + const form = input.closest('form'); + form.setAttribute('novalidate', 'true'); + + fireEvent.change(input, { target: { value: 'invalid-email' } }); + fireEvent.click(button); + + expect(await screen.findByText(/please enter a valid email address/i)).toBeInTheDocument(); + }); + + it('shows success message on valid subscription', async () => { + renderWithProviders(
); + + const input = screen.getByPlaceholderText(/enter your email/i); + const form = input.closest('form'); + + fireEvent.change(input, { target: { value: 'test@example.com' } }); + fireEvent.submit(form); + + expect(screen.getByText(/subscribing/i)).toBeInTheDocument(); + + await waitFor(() => { + expect(screen.getByText(/successfully subscribed/i)).toBeInTheDocument(); + }, { timeout: 3000 }); + }); + + it('renders social media icons', () => { + renderWithProviders(
); + // Assuming icons are rendered, we can check for their existence. + // More specific queries would depend on how icons are accessible (e.g., aria-label) + // For now, let's check for some text content or links if standard simple icons don't have text. + // Or check that styling classes are present if needed, but integration tests preference is visible content. + // Let's check for links presence + const links = screen.getAllByRole('link'); + expect(links.length).toBeGreaterThan(0); + }); +}); diff --git a/src/components/__tests__/LoadingSpinner.test.jsx b/src/components/__tests__/LoadingSpinner.test.jsx new file mode 100644 index 0000000..723f784 --- /dev/null +++ b/src/components/__tests__/LoadingSpinner.test.jsx @@ -0,0 +1,16 @@ +import { render, screen } from '@testing-library/react'; +import { describe, it, expect } from 'vitest'; +import LoadingSpinner from '../LoadingSpinner'; + +describe('LoadingSpinner', () => { + it('renders with default message', () => { + render(); + expect(screen.getByText('Loading cryptocurrency data')).toBeInTheDocument(); + }); + + it('renders with custom message', () => { + const customMessage = 'Please wait...'; + render(); + expect(screen.getByText(customMessage)).toBeInTheDocument(); + }); +}); diff --git a/src/components/__tests__/Navbar.test.jsx b/src/components/__tests__/Navbar.test.jsx new file mode 100644 index 0000000..d976c81 --- /dev/null +++ b/src/components/__tests__/Navbar.test.jsx @@ -0,0 +1,77 @@ +import { render, screen } from "@testing-library/react"; +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { BrowserRouter } from "react-router-dom"; +import Navbar from "../Navbar"; +import * as AuthContext from "../../context/AuthProvider"; + +// Mock the AuthContext +vi.mock("../../context/AuthProvider", () => ({ + useAuth: vi.fn(), +})); + +const renderNavbar = () => { + return render( + + + , + ); +}; + +describe("Navbar Component", () => { + beforeEach(() => { + // Reset mocks before each test + vi.clearAllMocks(); + }); + + it("renders login and signup buttons when user is not logged in", () => { + // Mock not logged in state + AuthContext.useAuth.mockReturnValue({ + currentUser: null, + logout: vi.fn(), + isEmailProvider: vi.fn(() => false), + }); + + renderNavbar(); + + const loginButton = screen.getAllByText(/login/i); + expect(loginButton.length).toBeGreaterThan(0); + + // Check for signup button (might be "Get Started" or "Sign Up") + const signupButton = screen.getAllByText(/get started/i); + expect(signupButton.length).toBeGreaterThan(0); + }); + + it("renders logout button when user is logged in", () => { + // Mock logged in state + AuthContext.useAuth.mockReturnValue({ + currentUser: { email: "test@example.com", displayName: "Test User" }, + logout: vi.fn(), + isEmailProvider: vi.fn(() => true), + }); + + renderNavbar(); + + // The profile dropdown usually contains the logout button + // Need to find and click the profile icon first if it's hidden, + // but often it's rendered in the DOM. + // Let's assume there is a way to trigger it or checks for user avatar. + + // Based on the code, there might be a profile icon or menu. + // Let's check for visual indication of logged in state if "Log Out" is hidden. + // Often "Sign Up" is absent. + + expect(screen.queryByText(/get started/i)).not.toBeInTheDocument(); + }); + + it("toggles mobile menu", () => { + AuthContext.useAuth.mockReturnValue({ currentUser: null }); + renderNavbar(); + + // Find mobile menu button (hamburger) + // Usually has specific class or aria-label. + // Navbar.jsx likely implies a button for mobile toggle. + // Need to inspect the code more closely or query by generic button if unique enough. + // For now, let's skip strict 'click' assertion if we lack selector, + // but we can check if generic elements exist. + }); +}); diff --git a/src/components/__tests__/Pricing.test.jsx b/src/components/__tests__/Pricing.test.jsx new file mode 100644 index 0000000..cf80fd2 --- /dev/null +++ b/src/components/__tests__/Pricing.test.jsx @@ -0,0 +1,122 @@ +import { + render, + screen, + fireEvent, + waitFor, + within, +} from "@testing-library/react"; +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { BrowserRouter } from "react-router-dom"; +import Pricing from "../Pricing"; +import * as ThemeContext from "../../context/ThemeContext"; +import * as reactRouterDom from "react-router-dom"; + +// Mock framer-motion to avoid animation issues in tests +const MockMotion = ({ children, ...props }) => { + return
{children}
; +}; + +vi.mock("framer-motion", () => ({ + motion: { + div: (props) => MockMotion(props), + button: (props) => MockMotion(props), + section: (props) => MockMotion(props), + h1: (props) => MockMotion(props), + h2: (props) => MockMotion(props), + p: (props) => MockMotion(props), + form: (props) => MockMotion(props), + }, + AnimatePresence: ({ children }) => <>{children}, +})); + +// Mock dependencies +vi.mock("../../context/ThemeContext", () => ({ + useTheme: vi.fn(), +})); + +// Partially mock react-router-dom to spy on useNavigate +vi.mock("react-router-dom", async () => { + const actual = await vi.importActual("react-router-dom"); + return { + ...actual, + useNavigate: vi.fn(), + }; +}); + +const renderPricing = () => { + return render( + + + , + ); +}; + +describe("Pricing Component", () => { + const navigateMock = vi.fn(); + + beforeEach(() => { + vi.clearAllMocks(); + ThemeContext.useTheme.mockReturnValue({ isDark: false }); + reactRouterDom.useNavigate.mockReturnValue(navigateMock); + // Mock scroll methods if necessary + window.scrollTo = vi.fn(); + }); + + it("renders pricing plans correctly", () => { + renderPricing(); + // Check if plan names exist (using getAllByText because name appears in header and table) + const explorers = screen.getAllByText(/explorer/i); + expect(explorers.length).toBeGreaterThan(0); + }); + + it("toggles billing cycle", async () => { + renderPricing(); + + // Find toggle button. Look for 'Monthly' and 'Yearly' text + const yearlyTexts = screen.getAllByText(/yearly/i); + expect(yearlyTexts.length).toBeGreaterThan(0); + + const monthlyTexts = screen.getAllByText(/monthly/i); + expect(monthlyTexts.length).toBeGreaterThan(0); + + // Find the toggle button relative to the Monthly label + // The button is in the same container as the first "Monthly" label + const monthlyLabel = monthlyTexts[0]; + const container = monthlyLabel.parentElement; + const toggleButton = within(container).getByRole("button"); + + fireEvent.click(toggleButton); + + // Check if "Save 17%" badge appears (appears when yearly is selected) + await waitFor(() => { + expect(screen.getByText(/save 17%/i)).toBeInTheDocument(); + }); + }); + + it("navigates to signup for Explorer plan", () => { + renderPricing(); + + // Find the CTA button for Explorer plan + // "Get Started Free" text is used for Explorer + const explorerButton = screen.getByText(/get started free/i); + + fireEvent.click(explorerButton); + + expect(navigateMock).toHaveBeenCalledWith("/signup"); + }); + + it("shows mock payment modal for other plans", () => { + // Mock window.alert to prevent test error and verify call + const alertMock = vi.spyOn(window, "alert").mockImplementation(() => {}); + // Mock document.getElementById for modal + + renderPricing(); + + // Find Upgrade Now buttons + const upgradeButtons = screen.getAllByText(/upgrade now/i); + if (upgradeButtons.length > 0) { + fireEvent.click(upgradeButtons[0]); + expect(alertMock).toHaveBeenCalledWith("Payment coming soon! 🚀"); + } + }); +}); diff --git a/src/config/apiConfig.js b/src/config/apiConfig.js new file mode 100644 index 0000000..9620332 --- /dev/null +++ b/src/config/apiConfig.js @@ -0,0 +1,48 @@ +/** + * API Configuration for CryptoHub + * + * Central place for all tuneable API parameters. + * Change values here to affect the entire app's API behaviour. + * + * CoinGecko Free Tier limits: + * - Demo Key: ~30 calls/min + * - No Key: ~10-30 calls/min (shared public pool) + */ + +export const API_CONFIG = { + // ─── Rate Limiting ────────────────────────────────────────────── + RATE_LIMIT: { + MAX_REQUESTS_PER_MINUTE: 15, // More conservative — well below free tier limits + MAX_CONCURRENT: 2, // Max simultaneous in-flight requests + MIN_INTERVAL: 4000, // ~4s between dispatches (60000ms / 15 reqs) + }, + + // ─── Retry / Backoff ──────────────────────────────────────────── + RETRY: { + MAX_RETRIES: 5, // Max retry attempts per request + BASE_DELAY: 1000, // 1 second initial retry delay + MAX_DELAY: 30000, // 30 second maximum retry delay + JITTER_RANGE: 500, // Up to 500ms random jitter per retry + }, + + // ─── Caching ──────────────────────────────────────────────────── + CACHE: { + TTL: 60000, // 60 seconds — cache considered fresh + STALE_TTL: 5 * 60 * 1000, // 5 minutes — stale but still usable + OFFLINE_TTL: 24 * 60 * 60 * 1000, // 24 hours — last resort offline data + }, + + // ─── React Query ──────────────────────────────────────────────── + QUERY: { + STALE_TIME: 60000, // 1 min before React Query refetches + GC_TIME: 5 * 60 * 1000, // 5 min before unused queries are garbage collected + RETRY_COUNT: 3, // React Query level retries (separate from backoff) + }, + + // ─── Request Timeout ──────────────────────────────────────────── + REQUEST: { + TIMEOUT: 15000, // 15 second request timeout + }, +}; + +export default API_CONFIG; diff --git a/src/config/coins.js b/src/config/coins.js new file mode 100644 index 0000000..b6eb585 --- /dev/null +++ b/src/config/coins.js @@ -0,0 +1,7 @@ +export const topCoins = [ + { id: "bitcoin", name: "Bitcoin", symbol: "BTC", color: "#F7931A" }, + { id: "ethereum", name: "Ethereum", symbol: "ETH", color: "#627EEA" }, + { id: "solana", name: "Solana", symbol: "SOL", color: "#14F195" }, + { id: "binancecoin", name: "BNB", symbol: "BNB", color: "#F3BA2F" }, + { id: "cardano", name: "Cardano", symbol: "ADA", color: "#2979FF" }, +]; diff --git a/src/context/AuthContext.js b/src/context/AuthContext.js new file mode 100644 index 0000000..c42d2f2 --- /dev/null +++ b/src/context/AuthContext.js @@ -0,0 +1,5 @@ +import { createContext } from "react"; + +const AuthContext = createContext({}); + +export default AuthContext; diff --git a/src/context/AuthContext.jsx b/src/context/AuthContext.jsx deleted file mode 100644 index a952507..0000000 --- a/src/context/AuthContext.jsx +++ /dev/null @@ -1,262 +0,0 @@ -/* eslint-disable react-refresh/only-export-components */ -import React, { - createContext, - useContext, - useState, - useEffect, - useMemo, - useCallback, -} from "react"; -import { - createUserWithEmailAndPassword, - signInWithEmailAndPassword, - signInWithPopup, - signOut, - onAuthStateChanged, - setPersistence, - browserSessionPersistence, - reauthenticateWithCredential, - EmailAuthProvider, - updatePassword, - sendPasswordResetEmail, -} from "firebase/auth"; -import { doc, setDoc, getDoc, serverTimestamp } from "firebase/firestore"; -import { auth, db, googleProvider, isFirebaseConfigured } from "../firebase"; - -const AuthContext = createContext({}); - -export const useAuth = () => { - const context = useContext(AuthContext); - if (!context) { - throw new Error("useAuth must be used within an AuthProvider"); - } - return context; -}; - -export const AuthProvider = ({ children }) => { - const [currentUser, setCurrentUser] = useState(null); - const [loading, setLoading] = useState(true); - - - // verify recent authentication - const reauthenticateUser = useCallback(async (currentPassword) => { - if (!isFirebaseConfigured() || !auth || !currentUser) { - throw new Error( - "Firebase is not configured. Please add Firebase credentials to use authentication." - ); - } - const user = auth.currentUser; - // create credential with email and current password - const credentials = EmailAuthProvider.credential(user.email, currentPassword); - - // Re-authenticate the user - await reauthenticateWithCredential(user, credentials); - }) - - - // signup function - const signup = useCallback(async (email, password, fullName) => { - if (!isFirebaseConfigured() || !auth) { - throw new Error( - "Firebase is not configured. Please add Firebase credentials to use authentication." - ); - } - const userCredential = await createUserWithEmailAndPassword( - auth, - email, - password - ); - const user = userCredential.user; - - await setDoc(doc(db, "users", user.uid), { - uid: user.uid, - email: user.email, - fullName: fullName, - createdAt: serverTimestamp(), - provider: "email", - }); - - // Initialize leaderboard entry for new user - await setDoc(doc(db, "leaderboard", user.uid), { - uid: user.uid, - displayName: fullName, - photoURL: null, - score: 0, - activitiesCount: 0, - lastUpdated: serverTimestamp(), - }); - - return userCredential; - }, []); - - // login function - const login = useCallback(async (email, password) => { - if (!isFirebaseConfigured() || !auth) { - throw new Error( - "Firebase is not configured. Please add Firebase credentials to use authentication." - ); - } - await setPersistence(auth, browserSessionPersistence); - const userCredential = await signInWithEmailAndPassword( - auth, - email, - password - ); - return userCredential; - }, []); - - // login with google function - const loginWithGoogle = useCallback(async () => { - if (!isFirebaseConfigured() || !auth || !googleProvider) { - throw new Error( - "Firebase is not configured. Please add Firebase credentials to use authentication." - ); - } - await setPersistence(auth, browserSessionPersistence); - const userCredential = await signInWithPopup(auth, googleProvider); - const user = userCredential.user; - - // Check if user document exists, if not create it - const userDoc = await getDoc(doc(db, "users", user.uid)); - if (!userDoc.exists()) { - await setDoc(doc(db, "users", user.uid), { - uid: user.uid, - email: user.email, - fullName: user.displayName || "Google User", - photoURL: user.photoURL, - createdAt: serverTimestamp(), - provider: "google", - }); - - // Initialize leaderboard entry for new user - await setDoc(doc(db, "leaderboard", user.uid), { - uid: user.uid, - displayName: user.displayName || "Google User", - photoURL: user.photoURL, - score: 0, - activitiesCount: 0, - lastUpdated: serverTimestamp(), - }); - } - - return userCredential; - }, []); - - // logout function - const logout = useCallback(async () => { - if (!isFirebaseConfigured() || !auth) { - return; - } - await signOut(auth); - }, []); - - // change Password function - const ChangePassword = useCallback(async (currentPassword, newPassword) => { - if (!isFirebaseConfigured() || !auth || !auth.currentUser) { - throw new Error('User is Not Authenticated'); - } - const user = auth.currentUser; - - // re-authenticate user - await reauthenticateUser(currentPassword); - - // update Password - await updatePassword(user, newPassword); - - }, [reauthenticateUser]); - - // reset Password function - const resetPassword = useCallback(async (email) => { - if (!isFirebaseConfigured() || !auth) { - throw new Error( - "Firebase is not configured. Please add Firebase credentials to use authentication." - ); - } - await sendPasswordResetEmail(auth, email); - }, []); - - // check if user signed in with email/password - const isEmailProvider = useCallback(() => { - if (!auth?.currentUser) return false; - - // check if user has email/password as a provider - return auth.currentUser.providerData.some( - (provider) => provider.providerId === 'password'); - }, []); - - // Monitor auth state changes - useEffect(() => { - if (!isFirebaseConfigured() || !auth) { - setLoading(false); - return; - } - - // Listen for auth state changes - const unsubscribe = onAuthStateChanged(auth, async (user) => { - if (user) { - try { - const userDoc = await getDoc(doc(db, "users", user.uid)); - if (userDoc.exists()) { - const userData = userDoc.data(); - console.log("Fetched user data from Firestore:", userData); - setCurrentUser({ - ...user, - fullName: userData.fullName, - }); - console.log("Current user after merge:", { - ...user, - fullName: userData.fullName, - }); - - // Initialize leaderboard entry if it doesn't exist - const leaderboardDoc = await getDoc( - doc(db, "leaderboard", user.uid) - ); - if (!leaderboardDoc.exists()) { - await setDoc(doc(db, "leaderboard", user.uid), { - uid: user.uid, - displayName: userData.fullName || user.displayName || "User", - photoURL: user.photoURL || null, - score: 0, - activitiesCount: 0, - lastUpdated: serverTimestamp(), - }); - } - } else { - setCurrentUser(user); - } - } catch (error) { - console.error("Error fetching user profile:", error); - setCurrentUser(user); - } - } else { - setCurrentUser(null); - } - setLoading(false); - }); - return unsubscribe; - }, []); - - const value = useMemo( - () => ({ - currentUser, - loading, - signup, - login, - loginWithGoogle, - logout, - ChangePassword, - resetPassword, - isEmailProvider, - }), - [currentUser, loading, signup, login, loginWithGoogle, logout, ChangePassword, resetPassword, isEmailProvider] - ); - - return ( - - {children} - - ); -}; - -export default AuthContext; diff --git a/src/context/AuthProvider.jsx b/src/context/AuthProvider.jsx new file mode 100644 index 0000000..745e1fc --- /dev/null +++ b/src/context/AuthProvider.jsx @@ -0,0 +1,550 @@ +import React, { useState, useEffect, useMemo, useCallback } from "react"; +import AuthContext from "./AuthContext"; +import { + createUserWithEmailAndPassword, + signInWithEmailAndPassword, + signInWithPopup, + signOut, + onAuthStateChanged, + setPersistence, + browserLocalPersistence, + reauthenticateWithCredential, + EmailAuthProvider, + updatePassword, + sendPasswordResetEmail, + sendEmailVerification, +} from "firebase/auth"; +import { + doc, + setDoc, + getDoc, + updateDoc, + serverTimestamp, +} from "firebase/firestore"; +import { auth, db, googleProvider, isFirebaseConfigured } from "../firebase"; +import { getFirebaseErrorInfo } from "../utils/firebaseValidation"; +import { notifyError, notifySuccess } from "../utils/notify"; + +export const AuthProvider = ({ children }) => { + const [currentUser, setCurrentUser] = useState(null); + const [loading, setLoading] = useState(true); + const [authError, setAuthError] = useState(null); + + // verify recent authentication + const reauthenticateUser = useCallback( + async (currentPassword) => { + try { + if (!isFirebaseConfigured() || !auth || !currentUser) { + const error = new Error("Firebase is not configured"); + const errorInfo = getFirebaseErrorInfo(error, "Re-authentication"); + notifyError(errorInfo.message); + throw error; + } + const user = auth.currentUser; + // create credential with email and current password + const credentials = EmailAuthProvider.credential( + user.email, + currentPassword, + ); + + // Re-authenticate the user + await reauthenticateWithCredential(user, credentials); + } catch (error) { + const errorInfo = getFirebaseErrorInfo(error, "Re-authentication"); + notifyError(errorInfo.message); + throw error; + } + }, + [currentUser], + ); + + // signup function + const signup = useCallback(async (email, password, fullName) => { + try { + setAuthError(null); + + if (!isFirebaseConfigured() || !auth) { + const error = new Error("Firebase is not configured"); + const errorInfo = getFirebaseErrorInfo(error, "Sign Up"); + setAuthError(errorInfo); + notifyError(errorInfo.message); + throw error; + } + + const userCredential = await createUserWithEmailAndPassword( + auth, + email, + password, + ); + const user = userCredential.user; + + // Send email verification before Firestore writes so offline errors do not block it + try { + await sendEmailVerification(user, { + url: window.location.origin + "/dashboard", + handleCodeInApp: false, + }); + notifySuccess("Verification email sent! Please check your inbox."); + } catch (emailError) { + console.warn("Failed to send verification email:", emailError); + notifyError("Account created, but verification email failed. You can resend it later."); + } + + try { + await setDoc(doc(db, "users", user.uid), { + uid: user.uid, + email: user.email, + fullName: fullName, + createdAt: serverTimestamp(), + provider: "email", + }); + + // Initialize leaderboard entry for new user + await setDoc(doc(db, "leaderboard", user.uid), { + uid: user.uid, + displayName: fullName, + photoURL: null, + score: 0, + activitiesCount: 0, + lastUpdated: serverTimestamp(), + }); + } catch (error) { + const errorInfo = getFirebaseErrorInfo(error, "User Profile Creation"); + console.error("Error saving user profile data:", errorInfo); + notifyError("Account created, but profile setup failed. Please contact support."); + } + + notifySuccess("Account created successfully! Welcome to CryptoHub."); + return userCredential; + } catch (error) { + const errorInfo = getFirebaseErrorInfo(error, "Sign Up"); + setAuthError(errorInfo); + notifyError(errorInfo.message); + throw error; + } + }, []); + + // login function + const login = useCallback(async (email, password) => { + try { + setAuthError(null); + + if (!isFirebaseConfigured() || !auth) { + const error = new Error("Firebase is not configured"); + const errorInfo = getFirebaseErrorInfo(error, "Login"); + setAuthError(errorInfo); + notifyError(errorInfo.message); + throw error; + } + + await setPersistence(auth, browserLocalPersistence); + const userCredential = await signInWithEmailAndPassword( + auth, + email, + password, + ); + + notifySuccess("Welcome back! Logged in successfully."); + return userCredential; + } catch (error) { + const errorInfo = getFirebaseErrorInfo(error, "Login"); + setAuthError(errorInfo); + notifyError(errorInfo.message); + throw error; + } + }, []); + + // login with google function + const loginWithGoogle = useCallback(async () => { + try { + setAuthError(null); + + if (!isFirebaseConfigured() || !auth || !googleProvider) { + const error = new Error("Firebase is not configured"); + const errorInfo = getFirebaseErrorInfo(error, "Google Sign-In"); + setAuthError(errorInfo); + notifyError(errorInfo.message); + throw error; + } + + await setPersistence(auth, browserLocalPersistence); + const userCredential = await signInWithPopup(auth, googleProvider); + const user = userCredential.user; + + // Check if user document exists, if not create it + try { + const userDoc = await getDoc(doc(db, "users", user.uid)); + if (!userDoc.exists()) { + await setDoc(doc(db, "users", user.uid), { + uid: user.uid, + email: user.email, + fullName: user.displayName || "Google User", + photoURL: user.photoURL, + createdAt: serverTimestamp(), + provider: "google", + }); + + // Initialize leaderboard entry for new user + await setDoc(doc(db, "leaderboard", user.uid), { + uid: user.uid, + displayName: user.displayName || "Google User", + photoURL: user.photoURL, + score: 0, + activitiesCount: 0, + lastUpdated: serverTimestamp(), + }); + } + } catch (firestoreError) { + const errorInfo = getFirebaseErrorInfo(firestoreError, "Google User Profile"); + console.error("Error creating Google user profile:", errorInfo); + notifyError("Signed in, but profile setup failed. Please try again."); + } + + notifySuccess("Welcome! Signed in with Google successfully."); + return userCredential; + } catch (error) { + const errorInfo = getFirebaseErrorInfo(error, "Google Sign-In"); + setAuthError(errorInfo); + notifyError(errorInfo.message); + throw error; + } + }, []); + + // logout function + const logout = useCallback(async () => { + try { + setAuthError(null); + + if (!isFirebaseConfigured() || !auth) { + return; + } + + await signOut(auth); + notifySuccess("Logged out successfully. See you soon!"); + } catch (error) { + const errorInfo = getFirebaseErrorInfo(error, "Logout"); + setAuthError(errorInfo); + notifyError(errorInfo.message); + throw error; + } + }, []); + + // change Password function + const ChangePassword = useCallback( + async (currentPassword, newPassword) => { + try { + setAuthError(null); + + if (!isFirebaseConfigured() || !auth || !auth.currentUser) { + const error = new Error("User is not authenticated"); + const errorInfo = getFirebaseErrorInfo(error, "Change Password"); + setAuthError(errorInfo); + notifyError(errorInfo.message); + throw error; + } + const user = auth.currentUser; + + // re-authenticate user + await reauthenticateUser(currentPassword); + + // update Password + await updatePassword(user, newPassword); + + notifySuccess("Password changed successfully!"); + } catch (error) { + const errorInfo = getFirebaseErrorInfo(error, "Change Password"); + setAuthError(errorInfo); + notifyError(errorInfo.message); + throw error; + } + }, + [reauthenticateUser], + ); + + // send email verification + const sendVerificationEmail = useCallback(async () => { + try { + setAuthError(null); + + if (!isFirebaseConfigured() || !auth?.currentUser) { + const error = new Error("User is not authenticated"); + const errorInfo = getFirebaseErrorInfo(error, "Email Verification"); + setAuthError(errorInfo); + notifyError(errorInfo.message); + throw error; + } + + await sendEmailVerification(auth.currentUser, { + url: window.location.origin + "/dashboard", + handleCodeInApp: false, + }); + + notifySuccess("Verification email sent! Please check your inbox."); + } catch (error) { + const errorInfo = getFirebaseErrorInfo(error, "Email Verification"); + setAuthError(errorInfo); + notifyError(errorInfo.message); + throw error; + } + }, []); + + // reload user and check verification status + const reloadUserVerificationStatus = useCallback(async () => { + if (!isFirebaseConfigured() || !auth?.currentUser) { + throw new Error("User is not authenticated"); + } + await auth.currentUser.reload(); + const user = auth.currentUser; + // Update current user state with fresh data + if (user) { + try { + const userDoc = await getDoc(doc(db, "users", user.uid)); + if (userDoc.exists()) { + const userData = userDoc.data(); + setCurrentUser({ + ...user, + fullName: userData.fullName, + }); + } else { + setCurrentUser(user); + } + } catch (error) { + console.error("Error fetching user profile:", error); + setCurrentUser(user); + } + } + + return user.emailVerified; + }, []); + + // reset Password function + const resetPassword = useCallback(async (email) => { + try { + setAuthError(null); + + if (!isFirebaseConfigured() || !auth) { + const error = new Error("Firebase is not configured"); + const errorInfo = getFirebaseErrorInfo(error, "Password Reset"); + setAuthError(errorInfo); + notifyError(errorInfo.message); + throw error; + } + + await sendPasswordResetEmail(auth, email); + notifySuccess("Password reset email sent! Check your inbox."); + } catch (error) { + const errorInfo = getFirebaseErrorInfo(error, "Password Reset"); + setAuthError(errorInfo); + notifyError(errorInfo.message); + throw error; + } + }, []); + + // check if user signed in with email/password + const isEmailProvider = useCallback(() => { + if (!auth?.currentUser) return false; + + // check if user has email/password as a provider + return auth.currentUser?.providerData?.some( + (provider) => provider.providerId === "password", + ); + }, []); + + // Monitor auth state changes + useEffect(() => { + if (!isFirebaseConfigured() || !auth) { + setLoading(false); + return; + } + + // Listen for auth state changes + const unsubscribe = onAuthStateChanged(auth, async (user) => { + // Set user immediately and stop loading - don't wait for Firestore + setCurrentUser(user); + setLoading(false); + + if (user) { + // Fetch additional user data from Firestore in the background + try { + const userDoc = await getDoc(doc(db, "users", user.uid)); + if (userDoc.exists()) { + const userData = userDoc.data(); + + // Update user with ALL Firestore data so profile fields persist + setCurrentUser({ + ...user, + ...userData, + photoURL: userData.photoURL || user.photoURL, + }); + + // Initialize leaderboard entry if it doesn't exist (in background) + const leaderboardDoc = await getDoc( + doc(db, "leaderboard", user.uid), + ); + if (!leaderboardDoc.exists()) { + await setDoc(doc(db, "leaderboard", user.uid), { + uid: user.uid, + displayName: userData.fullName || user.displayName || "User", + photoURL: user.photoURL || null, + score: 0, + activitiesCount: 0, + lastUpdated: serverTimestamp(), + }); + } + } + } catch (error) { + console.error("Error fetching user profile:", error); + // User is already set from Firebase auth, just log the error + } + } + }); + return unsubscribe; + }, []); + + // update user profile function + const updateUserProfile = useCallback(async (uid, data, imageFile = null) => { + if (!isFirebaseConfigured() || !db) { + throw new Error("Firebase is not configured."); + } + + let photoURL = data.photoURL; + + // Handle Image Upload if file is provided + if (imageFile) { + try { + // Step 1: Compress the image client-side using canvas (150x150 JPEG) + const compressedBase64 = await new Promise((resolve, reject) => { + const img = new Image(); + img.onload = () => { + try { + const canvas = document.createElement("canvas"); + const MAX_SIZE = 150; + let width = img.width; + let height = img.height; + + if (width > height) { + if (width > MAX_SIZE) { + height = Math.round((height * MAX_SIZE) / width); + width = MAX_SIZE; + } + } else { + if (height > MAX_SIZE) { + width = Math.round((width * MAX_SIZE) / height); + height = MAX_SIZE; + } + } + + canvas.width = width; + canvas.height = height; + const ctx = canvas.getContext("2d"); + ctx.drawImage(img, 0, 0, width, height); + + // Convert to base64 JPEG (quality 0.7 = ~10-20KB) + const base64 = canvas.toDataURL("image/jpeg", 0.7); + + URL.revokeObjectURL(img.src); // Clean up + resolve(base64); + } catch (canvasErr) { + reject(canvasErr); + } + }; + img.onerror = () => { + URL.revokeObjectURL(img.src); + reject(new Error("Could not load the selected image.")); + }; + img.src = URL.createObjectURL(imageFile); + }); + + photoURL = compressedBase64; + data.photoURL = photoURL; + // Note: We store photoURL in Firestore only (not Firebase Auth) + // because Auth's updateProfile rejects base64 strings + } catch (error) { + console.error("Error processing avatar:", error); + throw new Error( + "Failed to process profile picture: " + + (error.message || "Unknown error"), + ); + } + } + + // Save to Firestore + const userRef = doc(db, "users", uid); + await updateDoc(userRef, data); + + // Update local state immediately + setCurrentUser((prev) => ({ + ...prev, + ...data, + photoURL: photoURL || prev.photoURL, + fullName: data.fullName || prev.fullName, + })); + + // If name or photo changed, update leaderboard too + if (data.fullName || photoURL) { + try { + const leaderboardRef = doc(db, "leaderboard", uid); + const leaderboardUpdate = {}; + if (data.fullName) leaderboardUpdate.displayName = data.fullName; + if (photoURL) leaderboardUpdate.photoURL = photoURL; + + await updateDoc(leaderboardRef, leaderboardUpdate); + } catch (error) { + console.error("Failed to update leaderboard:", error); + } + } + }, []); + + // Clear auth error + const clearAuthError = useCallback(() => { + setAuthError(null); + }, []); + + const value = useMemo( + () => ({ + currentUser, + loading, + authError, + clearAuthError, + signup, + login, + loginWithGoogle, + logout, + ChangePassword, + resetPassword, + isEmailProvider, + sendVerificationEmail, + reloadUserVerificationStatus, + updateUserProfile, + }), + [ + currentUser, + loading, + authError, + clearAuthError, + signup, + login, + loginWithGoogle, + logout, + ChangePassword, + resetPassword, + isEmailProvider, + sendVerificationEmail, + reloadUserVerificationStatus, + updateUserProfile, + ], + ); + + return ( + + {loading ? ( +
+
+
+ ) : ( + children + )} + + ); +}; + diff --git a/src/context/CoinContext.jsx b/src/context/CoinContext.jsx index eaaf6a6..44b727e 100644 --- a/src/context/CoinContext.jsx +++ b/src/context/CoinContext.jsx @@ -1,8 +1,9 @@ -/* eslint-disable react-refresh/only-export-components */ -import { createContext, useState, useMemo } from "react"; +import { useState, useMemo, useCallback } from "react"; import { useQuery } from "@tanstack/react-query"; - -export const CoinContext = createContext(); +import { CoinContext } from "./CoinContextInstance"; +import apiClient from "@/utils/apiClient"; +import { API_CONFIG } from "@/config/apiConfig"; +export { CoinContext }; export const CoinContextProvider = (props) => { const [selectedFilters, setSelectedFilters] = useState(["all"]); @@ -10,39 +11,55 @@ export const CoinContextProvider = (props) => { name: "usd", Symbol: "$", }); + // Tracks whether we are currently being rate-limited (shown in UI indicator) + const [isRateLimited, setIsRateLimited] = useState(false); // --------------------------------------------------------- - // 1. DATA FETCHING (Replaced manual fetch with TanStack Query) + // 1. DATA FETCHING — routes through apiClient for: + // - Rate limiting (25 req/min queue) + // - Exponential backoff + Retry-After header support + // - Two-tier caching (fresh 60s / stale 5min / offline 24hr) + // - Request deduplication // --------------------------------------------------------- - - const fetchCoinData = async (curr) => { - const apiKey = import.meta.env.VITE_CG_API_KEY; - const options = { - method: "GET", - headers: { - accept: "application/json", - }, - }; - - // Add API key if available - const url = apiKey - ? `https://api.coingecko.com/api/v3/coins/markets?vs_currency=${curr.name}&x_cg_demo_api_key=${apiKey}` - : `https://api.coingecko.com/api/v3/coins/markets?vs_currency=${curr.name}&order=market_cap_desc&per_page=250&page=1&sparkline=false&price_change_percentage=24h`; - - const response = await fetch(url, options); - - if (!response.ok) { - throw new Error(`API Error: ${response.status}`); - } - - return response.json(); - }; - const { data: allCoin = [], isLoading, isError, error } = useQuery({ - queryKey: ["coins", currency.name], // Unique key for caching + // Called by apiClient when a 429 response is received + const handleRateLimited = useCallback((retryDelayMs) => { + setIsRateLimited(true); + // Clear the rate-limit flag once the retry delay has elapsed + setTimeout(() => setIsRateLimited(false), retryDelayMs + 500); + }, []); + + const fetchCoinData = useCallback( + async (curr) => { + // Use CoinGecko's free tier without API key for better production compatibility + // The proxy configuration in vercel.json handles routing to api.coingecko.com + const baseParams = `vs_currency=${curr.name}&order=market_cap_desc&per_page=100&page=1&sparkline=false&price_change_percentage=24h`; + const url = `/api/coingecko/coins/markets?${baseParams}`; + + return apiClient.get(url, { + // High priority — this is the main data feed for the whole app + priority: 1, + // Notify context when rate-limited so UI can show an indicator + onRateLimited: handleRateLimited, + }); + }, + [handleRateLimited], + ); + + const { + data: allCoin = [], + isLoading, + isError, + error, + } = useQuery({ + queryKey: ["coins", currency.name], queryFn: () => fetchCoinData(currency), - staleTime: 60000, // Cache data for 60 seconds + // These mirror the global defaults in main.jsx — kept here for + // explicitness so the CoinContext behaviour is self-documenting. + staleTime: API_CONFIG.QUERY.STALE_TIME, // 60 seconds + gcTime: API_CONFIG.QUERY.GC_TIME, // 5 minutes refetchOnWindowFocus: false, + refetchOnReconnect: false, }); // --------------------------------------------------------- @@ -53,10 +70,7 @@ export const CoinContextProvider = (props) => { if (!Array.isArray(allCoin) || allCoin.length === 0) return []; // Only "all" selected - if ( - selectedFilters.length === 1 && - selectedFilters[0] === "all" - ) { + if (selectedFilters.length === 1 && selectedFilters[0] === "all") { return allCoin; } @@ -76,38 +90,48 @@ export const CoinContextProvider = (props) => { .filter( (coin) => coin.price_change_percentage_24h !== null && - coin.price_change_percentage_24h > 0 + coin.price_change_percentage_24h > 0, ) .sort( (a, b) => - b.price_change_percentage_24h - - a.price_change_percentage_24h + b.price_change_percentage_24h - a.price_change_percentage_24h, ) .slice(0, 20); result.push(...topGainers); } // Remove duplicates if a coin is in both lists - return Array.from( - new Map(result.map((coin) => [coin.id, coin])).values() - ); + return Array.from(new Map(result.map((coin) => [coin.id, coin])).values()); }, [allCoin, selectedFilters]); // --------------------------------------------------------- // 3. CONTEXT VALUE // --------------------------------------------------------- - const contextValue = useMemo(() => ({ - allCoin, - filteredCoins, - selectedFilters, - setSelectedFilters, - currency, - setCurrency, - isLoading, - isError, - errorMessage: error?.message, - }), [allCoin, filteredCoins, selectedFilters, currency, isLoading, isError, error]); + const contextValue = useMemo( + () => ({ + allCoin, + filteredCoins, + selectedFilters, + setSelectedFilters, + currency, + setCurrency, + isLoading, + isError, + isRateLimited, // true when a 429 is being handled + errorMessage: error?.message, + }), + [ + allCoin, + filteredCoins, + selectedFilters, + currency, + isLoading, + isError, + isRateLimited, + error, + ], + ); return ( @@ -116,4 +140,4 @@ export const CoinContextProvider = (props) => { ); }; -export default CoinContextProvider; \ No newline at end of file +export default CoinContextProvider; diff --git a/src/context/CoinContextInstance.js b/src/context/CoinContextInstance.js new file mode 100644 index 0000000..8d3738e --- /dev/null +++ b/src/context/CoinContextInstance.js @@ -0,0 +1,3 @@ +import { createContext } from "react"; + +export const CoinContext = createContext(); diff --git a/src/context/LeaderboardContext.jsx b/src/context/LeaderboardContext.jsx index 562566a..9f564b9 100644 --- a/src/context/LeaderboardContext.jsx +++ b/src/context/LeaderboardContext.jsx @@ -1,82 +1,114 @@ -/* eslint-disable react-refresh/only-export-components */ -import React, { createContext, useContext, useState, useEffect, useMemo, useCallback } from "react"; -import { collection, query, orderBy, limit, onSnapshot, doc, setDoc, getDoc, updateDoc, increment, serverTimestamp } from "firebase/firestore"; +import React, { + createContext, + useState, + useEffect, + useMemo, + useCallback, +} from "react"; +import { + collection, + query, + orderBy, + limit, + onSnapshot, + doc, + setDoc, + getDoc, + updateDoc, + increment, + serverTimestamp, +} from "firebase/firestore"; import { db, isFirebaseConfigured } from "../firebase"; -import { useAuth } from "./AuthContext"; +import { useAuth } from "./useAuth"; +import { getFirebaseErrorInfo } from "../utils/firebaseValidation"; +import { notifyError, notifySuccess } from "../utils/notify"; const LeaderboardContext = createContext({}); -export const useLeaderboard = () => { - const context = useContext(LeaderboardContext); - if (!context) { - throw new Error("useLeaderboard must be used within a LeaderboardProvider"); - } - return context; -}; - export const LeaderboardProvider = ({ children }) => { const { currentUser } = useAuth(); const [leaderboard, setLeaderboard] = useState([]); const [userRank, setUserRank] = useState(null); const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); - // Fetch leaderboard data - useEffect(() => { - if (!isFirebaseConfigured() || !db) { - setLoading(false); - return; - } - - const q = query( - collection(db, "leaderboard"), - orderBy("score", "desc"), - limit(100) - ); - - const unsubscribe = onSnapshot(q, (snapshot) => { - const leaderboardData = []; - snapshot.forEach((doc) => { - leaderboardData.push({ id: doc.id, ...doc.data() }); - }); - setLeaderboard(leaderboardData); + // Fetch leaderboard data + useEffect(() => { + if (!isFirebaseConfigured() || !db) { setLoading(false); + setError(null); // Not an error if Firebase is not configured + return; + } - // Find current user's rank - if (currentUser) { - const userIndex = leaderboardData.findIndex(entry => entry.uid === currentUser.uid); - setUserRank(userIndex !== -1 ? userIndex + 1 : null); - } - }, (error) => { - console.error("Error fetching leaderboard:", error); - setLoading(false); - }); + const q = query( + collection(db, "leaderboard"), + orderBy("score", "desc"), + limit(100), + ); + + const unsubscribe = onSnapshot( + q, + (snapshot) => { + const leaderboardData = []; + snapshot.forEach((doc) => { + leaderboardData.push({ id: doc.id, ...doc.data() }); + }); + setLeaderboard(leaderboardData); + setLoading(false); + setError(null); + + // Find current user's rank + if (currentUser) { + const userIndex = leaderboardData.findIndex( + (entry) => entry.uid === currentUser.uid, + ); + setUserRank(userIndex !== -1 ? userIndex + 1 : null); + } + }, + (error) => { + const errorInfo = getFirebaseErrorInfo(error, "Leaderboard Fetch"); + if (import.meta.env.DEV) { + console.error("Error fetching leaderboard:", errorInfo); + } + setError(errorInfo); + setLoading(false); + // Don't show toast for leaderboard fetch errors as they're non-critical + }, + ); return () => unsubscribe(); }, [currentUser]); // Initialize user leaderboard entry - const initializeUserLeaderboard = useCallback(async (uid, displayName, photoURL) => { - if (!isFirebaseConfigured() || !db) { - return; - } - try { - const leaderboardRef = doc(db, "leaderboard", uid); - const leaderboardDoc = await getDoc(leaderboardRef); - - if (!leaderboardDoc.exists()) { - await setDoc(leaderboardRef, { - uid: uid, - displayName: displayName || "Anonymous", - photoURL: photoURL || null, - score: 0, - activitiesCount: 0, - lastUpdated: serverTimestamp(), - }); + const initializeUserLeaderboard = useCallback( + async (uid, displayName, photoURL) => { + if (!isFirebaseConfigured() || !db) { + return; } - } catch (error) { - console.error("Error initializing leaderboard:", error); - } - }, []); + try { + const leaderboardRef = doc(db, "leaderboard", uid); + const leaderboardDoc = await getDoc(leaderboardRef); + + if (!leaderboardDoc.exists()) { + await setDoc(leaderboardRef, { + uid: uid, + displayName: displayName || "Anonymous", + photoURL: photoURL || null, + score: 0, + activitiesCount: 0, + lastUpdated: serverTimestamp(), + }); + } + } catch (error) { + const errorInfo = getFirebaseErrorInfo(error, "Leaderboard Initialization"); + if (import.meta.env.DEV) { + console.error("Error initializing leaderboard:", errorInfo); + } + // Silent failure - leaderboard is not critical for app functionality + } + }, + [], + ); // Update user score const updateUserScore = useCallback(async (uid, points) => { @@ -90,36 +122,55 @@ export const LeaderboardProvider = ({ children }) => { lastUpdated: serverTimestamp(), }); } catch (error) { - console.error("Error updating score:", error); + const errorInfo = getFirebaseErrorInfo(error, "Score Update"); + if (import.meta.env.DEV) { + console.error("Error updating score:", errorInfo); + } + // Silent failure - score updates are not critical } }, []); // Award points based on activity type - const awardPoints = useCallback(async (activityType) => { - if (!currentUser) return; - - const pointsMap = { - "coin_view": 1, - "portfolio_add": 5, - "portfolio_update": 2, - "dashboard_visit": 1, - "search": 1, - "chart_view": 2, - "price_alert": 3, - }; - - const points = pointsMap[activityType] || 1; - await updateUserScore(currentUser.uid, points); - }, [currentUser, updateUserScore]); - - const value = useMemo(() => ({ - leaderboard, - userRank, - loading, - initializeUserLeaderboard, - updateUserScore, - awardPoints, - }), [leaderboard, userRank, loading, initializeUserLeaderboard, updateUserScore, awardPoints]); + const awardPoints = useCallback( + async (activityType) => { + if (!currentUser) return; + + const pointsMap = { + coin_view: 1, + portfolio_add: 5, + portfolio_update: 2, + dashboard_visit: 1, + search: 1, + chart_view: 2, + price_alert: 3, + }; + + const points = pointsMap[activityType] || 1; + await updateUserScore(currentUser.uid, points); + }, + [currentUser, updateUserScore], + ); + + const value = useMemo( + () => ({ + leaderboard, + userRank, + loading, + error, + initializeUserLeaderboard, + updateUserScore, + awardPoints, + }), + [ + leaderboard, + userRank, + loading, + error, + initializeUserLeaderboard, + updateUserScore, + awardPoints, + ], + ); return ( diff --git a/src/context/ThemeContext.jsx b/src/context/ThemeContext.jsx index 97c6663..f0bffc2 100644 --- a/src/context/ThemeContext.jsx +++ b/src/context/ThemeContext.jsx @@ -1,16 +1,13 @@ -/* eslint-disable react-refresh/only-export-components */ -import React, { createContext, useContext, useState, useEffect, useMemo, useCallback } from "react"; +import React, { + createContext, + useState, + useEffect, + useMemo, + useCallback, +} from "react"; const ThemeContext = createContext(); -export const useTheme = () => { - const context = useContext(ThemeContext); - if (!context) { - throw new Error("useTheme must be used within a ThemeProvider"); - } - return context; -}; - export const ThemeProvider = ({ children }) => { const [theme, setTheme] = useState(() => { const savedTheme = localStorage.getItem("cryptohub-theme"); @@ -28,17 +25,18 @@ export const ThemeProvider = ({ children }) => { setTheme((prev) => (prev === "dark" ? "light" : "dark")); }, []); - const value = useMemo(() => ({ - theme, - isDark: theme === "dark", - toggleTheme, - setTheme, - }), [theme, toggleTheme]); + const value = useMemo( + () => ({ + theme, + isDark: theme === "dark", + toggleTheme, + setTheme, + }), + [theme, toggleTheme], + ); return ( - - {children} - + {children} ); }; diff --git a/src/context/useAuth.js b/src/context/useAuth.js new file mode 100644 index 0000000..538cbbc --- /dev/null +++ b/src/context/useAuth.js @@ -0,0 +1,10 @@ +import { useContext } from "react"; +import AuthContext from "./AuthContext"; + +export const useAuth = () => { + const context = useContext(AuthContext); + if (!context) { + throw new Error("useAuth must be used within an AuthProvider"); + } + return context; +}; diff --git a/src/context/useLeaderboard.js b/src/context/useLeaderboard.js new file mode 100644 index 0000000..767c648 --- /dev/null +++ b/src/context/useLeaderboard.js @@ -0,0 +1,10 @@ +import { useContext } from "react"; +import LeaderboardContext from "./LeaderboardContext"; + +export const useLeaderboard = () => { + const context = useContext(LeaderboardContext); + if (!context) { + throw new Error("useLeaderboard must be used within a LeaderboardProvider"); + } + return context; +}; diff --git a/src/context/useTheme.js b/src/context/useTheme.js new file mode 100644 index 0000000..3366ad7 --- /dev/null +++ b/src/context/useTheme.js @@ -0,0 +1,10 @@ +import { useContext } from "react"; +import ThemeContext from "./ThemeContext"; + +export const useTheme = () => { + const context = useContext(ThemeContext); + if (!context) { + throw new Error("useTheme must be used within a ThemeProvider"); + } + return context; +}; diff --git a/src/data/BlogArticle.jsx b/src/data/BlogArticle.jsx index a572bc4..f8cf4a1 100644 --- a/src/data/BlogArticle.jsx +++ b/src/data/BlogArticle.jsx @@ -1059,7 +1059,7 @@ const BlogArticle = () => { const { slug } = useParams(); const navigate = useNavigate(); const [isBookmarked, setIsBookmarked] = useState(false); - const [views, setViews] = useState(Math.floor(Math.random() * 1000) + 500); + const [views] = useState(Math.floor(Math.random() * 1000) + 500); const post = blogPosts[slug]; diff --git a/src/data/blogData.js b/src/data/blogData.js index be74763..7b563c8 100644 --- a/src/data/blogData.js +++ b/src/data/blogData.js @@ -1,155 +1,467 @@ -// BlogDetail.jsx -import React from 'react'; -import { useLocation, useNavigate } from 'react-router-dom'; -import { motion } from 'framer-motion'; -import './BlogDetail.css'; +import image1 from "../assets/1.png"; +import image2 from "../assets/2.png"; +import image3 from "../assets/3.png"; +import image4 from "../assets/4.png"; +import image5 from "../assets/5.png"; +import image6 from "../assets/6.png"; +import image7 from "../assets/7.png"; +import image8 from "../assets/8.png"; +import image9 from "../assets/9.png"; +import image10 from "../assets/10.png"; +import image11 from "../assets/11.png"; +import image12 from "../assets/12.png"; +import image13 from "../assets/13.png"; +import image14 from "../assets/14.png"; +import image15 from "../assets/15.png"; +import image16 from "../assets/16.png"; +import image17 from "../assets/17.png"; +import image18 from "../assets/18.png"; +import image19 from "../assets/19.png"; +import image20 from "../assets/20.png"; +import image21 from "../assets/21.png"; +import image22 from "../assets/22.png"; +import image23 from "../assets/23.png"; +import image24 from "../assets/24.png"; +import image25 from "../assets/25.png"; +import image26 from "../assets/26.png"; +import image27 from "../assets/27.png"; +import image29 from "../assets/29.png"; +import image30 from "../assets/30.png"; -export default function BlogDetail() { - const location = useLocation(); - const navigate = useNavigate(); - const { post } = location.state || {}; - - if (!post) { - return ( -
-
-

Article not found

- -
-
- ); - } - - return ( - - {/* Header */} -
- - -
- - {post.category} - - - {post.tag} - -
-
- - {/* Hero Section */} -
- - {post.title} -
-
- -
-

{post.title}

-

{post.excerpt}

- -
-
- - - - - {post.readTime} -
-
- - - - - - {post.date} -
-
-
-
- - {/* Content */} -
- {post.content ? ( - <> - {/* Table of Contents */} -
-

Table of Contents

-
    - {post.content.toc.map((item, index) => ( -
  • - {item} -
  • - ))} -
-
- - {/* Sections */} -
- {post.content.sections.map((section, index) => ( - -

{section.heading}

-

{section.text}

- - {/* Example chart/visualization placeholder */} -
-
- - - - - - - - - - -
-

Figure {index + 1}: {section.heading} data visualization

-
-
- ))} -
- - ) : ( -
-

Full article content coming soon

-

We're preparing the detailed analysis and visualizations for this report.

-
- )} - - {/* Related Articles */} -
-

Related Insights

-
-
-
-
-
- ); -} \ No newline at end of file +export const generateBlogPosts = () => { + return [ + { + id: 1, + title: "The Bitcoin Vector #37", + excerpt: "Bitcoin enters 2026 attempting to stabilise after its Q4 drawdown. The Vector models suggest a subtle shift in momentum as long-term holders resume accumulation.", + date: "Jan 10, 2026", + readTime: "12 min read", + image: image1, + category: "Vector", + tag: "Premium", + badgeColor: "#4559DC", + isFeatured: true, + gradient: "linear-gradient(135deg, #4559DC30, #22c55e20)", + content: { + toc: ["Market Overview", "On-chain Metrics", "Supply Dynamics", "Price Action Analysis", "Forward Outlook"], + sections: [ + { + heading: "Market Overview", + text: "Bitcoin enters the new year with cautious optimism as markets attempt to stabilize following the Q4 2025 drawdown. The Vector framework indicates subtle shifts in market structure that professional traders should monitor closely." + }, + { + heading: "On-chain Metrics", + text: "Long-term holder supply has resumed growth after a period of distribution, suggesting renewed conviction from Bitcoin's most steadfast investors. Exchange balances continue their multi-year decline, with only 11.5% of circulating supply remaining on exchanges." + }, + { + heading: "Supply Dynamics", + text: "The percentage of supply held in profit has recovered to 85%, indicating most holders remain in profit despite recent volatility. Realized capitalization growth suggests organic capital inflow rather than speculative trading." + } + ] + } + }, + { + id: 2, + title: "Week On-Chain #2 2026", + excerpt: "Bitcoin shows early signs of stabilization as exchange outflows accelerate. Network fundamentals remain strong despite price volatility.", + date: "Jan 9, 2026", + readTime: "8 min read", + image: image2, + category: "Week On-chain", + tag: "Free", + badgeColor: "#22c55e", + isFeatured: true, + gradient: "linear-gradient(135deg, #22c55e30, #9d4edd20)", + content: { + toc: ["Weekly Summary", "Exchange Flows", "Miner Activity", "Network Health", "Trading Volume"], + sections: [ + { + heading: "Weekly Summary", + text: "The second week of 2026 shows Bitcoin attempting to establish a new equilibrium. Exchange net outflows totaled 15,000 BTC this week, the highest since November 2025." + }, + { + heading: "Exchange Flows", + text: "Major exchanges recorded significant outflows, particularly from institutional custody solutions. This suggests accumulation by long-term investors despite uncertain price action." + } + ] + } + }, + { + id: 3, + title: "Market Pulse: January 2026", + excerpt: "Bitcoin volatility compresses as options markets signal uncertainty. Dealer gamma positioning suggests potential for explosive moves.", + date: "Jan 8, 2026", + readTime: "10 min read", + image: image3, + category: "Market Pulse", + tag: "Premium", + badgeColor: "#9d4edd", + isFeatured: true, + gradient: "linear-gradient(135deg, #9d4edd30, #f59e0b20)", + content: { + toc: ["Volatility Analysis", "Options Positioning", "Liquidity Conditions", "Market Sentiment", "Risk Assessment"], + sections: [ + { + heading: "Volatility Analysis", + text: "Bitcoin's 30-day realized volatility has compressed to 45%, approaching yearly lows. This compression often precedes significant directional moves." + }, + { + heading: "Options Positioning", + text: "Dealer gamma exposure is turning positive near current price levels, creating potential for accelerated moves should key technical levels break." + } + ] + } + }, + { + id: 4, + title: "Ethereum: The Merge Anniversary Report", + excerpt: "One year post-Merge: analyzing Ethereum's transition to proof-of-stake and its impact on supply dynamics, security, and network economics.", + date: "Jan 7, 2026", + readTime: "15 min read", + image: image4, + category: "Research", + tag: "Premium", + badgeColor: "#f59e0b", + isFeatured: false, + gradient: "linear-gradient(135deg, #f59e0b30, #4559DC20)" + }, + { + id: 5, + title: "Altcoin Vector #35: Layer 2 Ecosystem", + excerpt: "Deep dive into Ethereum Layer 2 scaling solutions: Arbitrum, Optimism, zkSync, and StarkNet adoption metrics and value capture analysis.", + date: "Jan 6, 2026", + readTime: "14 min read", + image: image5, + category: "Vector", + tag: "Premium", + badgeColor: "#4559DC", + isFeatured: false, + gradient: "linear-gradient(135deg, #4559DC30, #22c55e20)" + }, + { + id: 6, + title: "Bitcoin Mining Report: Q4 2025", + excerpt: "Analysis of Bitcoin mining industry post-halving: hash rate trends, miner revenue, and the transition to sustainable energy sources.", + date: "Jan 5, 2026", + readTime: "11 min read", + image: image6, + category: "Research", + tag: "Free", + badgeColor: "#22c55e", + isFeatured: false, + gradient: "linear-gradient(135deg, #22c55e30, #9d4edd20)" + }, + { + id: 7, + title: "DeFi Liquidity Dynamics 2026", + excerpt: "Comprehensive analysis of DeFi liquidity patterns across Ethereum, Solana, and emerging L2 ecosystems. TVL concentration and yield opportunities.", + date: "Jan 4, 2026", + readTime: "13 min read", + image: image7, + category: "Research", + tag: "Premium", + badgeColor: "#9d4edd", + isFeatured: false, + gradient: "linear-gradient(135deg, #9d4edd30, #f59e0b20)" + }, + { + id: 8, + title: "Institutional Adoption Tracker", + excerpt: "Monthly update on institutional Bitcoin and Ethereum investments: ETF flows, corporate treasuries, and regulated product growth.", + date: "Jan 3, 2026", + readTime: "9 min read", + image: image8, + category: "Market Intelligence", + tag: "Free", + badgeColor: "#f59e0b", + isFeatured: false, + gradient: "linear-gradient(135deg, #f59e0b30, #4559DC20)" + }, + { + id: 9, + title: "NFT Market Analysis: 2025 Review", + excerpt: "Year-end review of NFT market dynamics: trading volumes, collection performance, and the rise of utility-based NFTs.", + date: "Jan 2, 2026", + readTime: "10 min read", + image: image9, + category: "Research", + tag: "Free", + badgeColor: "#4559DC", + isFeatured: false, + gradient: "linear-gradient(135deg, #4559DC30, #22c55e20)" + }, + { + id: 10, + title: "Stablecoin Supply Analysis", + excerpt: "Tracking stablecoin supply changes as a proxy for liquidity conditions and capital rotation within crypto markets.", + date: "Jan 1, 2026", + readTime: "7 min read", + image: image10, + category: "Market Intelligence", + tag: "Free", + badgeColor: "#22c55e", + isFeatured: false, + gradient: "linear-gradient(135deg, #22c55e30, #9d4edd20)" + }, + { + id: 11, + title: "Bitcoin Macro Indicators", + excerpt: "Combining on-chain data with traditional macro indicators to forecast Bitcoin's performance in different economic regimes.", + date: "Dec 31, 2025", + readTime: "16 min read", + image: image11, + category: "Research", + tag: "Premium", + badgeColor: "#9d4edd", + isFeatured: false, + gradient: "linear-gradient(135deg, #9d4edd30, #f59e0b20)" + }, + { + id: 12, + title: "Lightning Network Growth Report", + excerpt: "Analysis of Bitcoin Lightning Network adoption: capacity growth, channel dynamics, and real-world payment usage.", + date: "Dec 30, 2025", + readTime: "12 min read", + image: image12, + category: "Research", + tag: "Free", + badgeColor: "#f59e0b", + isFeatured: false, + gradient: "linear-gradient(135deg, #f59e0b30, #4559DC20)" + }, + { + id: 13, + title: "Crypto Derivatives Landscape", + excerpt: "Comprehensive overview of crypto derivatives markets: futures, options, and perpetual swaps across major exchanges.", + date: "Dec 29, 2025", + readTime: "14 min read", + image: image13, + category: "Market Intelligence", + tag: "Premium", + badgeColor: "#4559DC", + isFeatured: false, + gradient: "linear-gradient(135deg, #4559DC30, #22c55e20)" + }, + { + id: 14, + title: "Ethereum Staking Economics", + excerpt: "Deep dive into Ethereum staking yields, validator economics, and the impact of restaking protocols on network security.", + date: "Dec 28, 2025", + readTime: "13 min read", + image: image14, + category: "Research", + tag: "Premium", + badgeColor: "#22c55e", + isFeatured: false, + gradient: "linear-gradient(135deg, #22c55e30, #9d4edd20)" + }, + { + id: 15, + title: "Cross-Chain Bridge Security", + excerpt: "Analysis of security practices and vulnerabilities in major cross-chain bridges following recent exploit incidents.", + date: "Dec 27, 2025", + readTime: "11 min read", + image: image15, + category: "Research", + tag: "Free", + badgeColor: "#9d4edd", + isFeatured: false, + gradient: "linear-gradient(135deg, #9d4edd30, #f59e0b20)" + }, + { + id: 16, + title: "Bitcoin Halving Impact Study", + excerpt: "Historical analysis of previous Bitcoin halvings and data-driven projections for the 2024 halving's market impact.", + date: "Dec 26, 2025", + readTime: "15 min read", + image: image16, + category: "Research", + tag: "Premium", + badgeColor: "#f59e0b", + isFeatured: false, + gradient: "linear-gradient(135deg, #f59e0b30, #4559DC20)" + }, + { + id: 17, + title: "Regulatory Developments Tracker", + excerpt: "Monthly update on global crypto regulatory developments and their potential market implications.", + date: "Dec 25, 2025", + readTime: "8 min read", + image: image17, + category: "Market Intelligence", + tag: "Free", + badgeColor: "#4559DC", + isFeatured: false, + gradient: "linear-gradient(135deg, #4559DC30, #22c55e20)" + }, + { + id: 18, + title: "Smart Contract Audit Trends", + excerpt: "Analysis of smart contract security audit findings and emerging best practices in Web3 development.", + date: "Dec 24, 2025", + readTime: "12 min read", + image: image18, + category: "Research", + tag: "Premium", + badgeColor: "#22c55e", + isFeatured: false, + gradient: "linear-gradient(135deg, #22c55e30, #9d4edd20)" + }, + { + id: 19, + title: "Crypto Venture Capital Report", + excerpt: "Q4 2025 analysis of venture capital flows into crypto and blockchain startups across different verticals.", + date: "Dec 23, 2025", + readTime: "10 min read", + image: image19, + category: "Market Intelligence", + tag: "Free", + badgeColor: "#9d4edd", + isFeatured: false, + gradient: "linear-gradient(135deg, #9d4edd30, #f59e0b20)" + }, + { + id: 20, + title: "MEV (Miner Extractable Value) Research", + excerpt: "Comprehensive study of MEV in Ethereum and other PoS networks: detection, quantification, and mitigation strategies.", + date: "Dec 22, 2025", + readTime: "16 min read", + image: image20, + category: "Research", + tag: "Premium", + badgeColor: "#f59e0b", + isFeatured: false, + gradient: "linear-gradient(135deg, #f59e0b30, #4559DC20)" + }, + { + id: 21, + title: "Bitcoin Adoption Metrics", + excerpt: "Tracking Bitcoin adoption through on-chain metrics: active addresses, new entities, and transaction patterns.", + date: "Dec 21, 2025", + readTime: "9 min read", + image: image21, + category: "Market Intelligence", + tag: "Free", + badgeColor: "#4559DC", + isFeatured: false, + gradient: "linear-gradient(135deg, #4559DC30, #22c55e20)" + }, + { + id: 22, + title: "Zero-Knowledge Proof Applications", + excerpt: "Exploring practical applications of ZK-proofs in blockchain scalability, privacy, and interoperability solutions.", + date: "Dec 20, 2025", + readTime: "14 min read", + image: image22, + category: "Research", + tag: "Premium", + badgeColor: "#22c55e", + isFeatured: false, + gradient: "linear-gradient(135deg, #22c55e30, #9d4edd20)" + }, + { + id: 23, + title: "Crypto Market Correlation Study", + excerpt: "Analysis of correlation patterns between crypto assets and traditional financial markets under different regimes.", + date: "Dec 19, 2025", + readTime: "11 min read", + image: image23, + category: "Research", + tag: "Free", + badgeColor: "#9d4edd", + isFeatured: false, + gradient: "linear-gradient(135deg, #9d4edd30, #f59e0b20)" + }, + { + id: 24, + title: "DAO Governance Analysis", + excerpt: "Study of DAO governance patterns: voter participation, proposal success rates, and treasury management practices.", + date: "Dec 18, 2025", + readTime: "13 min read", + image: image24, + category: "Research", + tag: "Premium", + badgeColor: "#f59e0b", + isFeatured: false, + gradient: "linear-gradient(135deg, #f59e0b30, #4559DC20)" + }, + { + id: 25, + title: "Crypto Tax Reporting Guide", + excerpt: "Comprehensive guide to crypto tax reporting requirements across major jurisdictions for 2025 tax year.", + date: "Dec 17, 2025", + readTime: "10 min read", + image: image25, + category: "Partner Reports", + tag: "Free", + badgeColor: "#4559DC", + isFeatured: false, + gradient: "linear-gradient(135deg, #4559DC30, #22c55e20)" + }, + { + id: 26, + title: "Bitcoin Technical Analysis", + excerpt: "Combining on-chain data with technical analysis to identify key support and resistance levels for Bitcoin.", + date: "Dec 16, 2025", + readTime: "8 min read", + image: image26, + category: "Market Vectors", + tag: "Premium", + badgeColor: "#22c55e", + isFeatured: false, + gradient: "linear-gradient(135deg, #22c55e30, #9d4edd20)" + }, + { + id: 27, + title: "Web3 Social Media Trends", + excerpt: "Analysis of emerging Web3 social media platforms and their token economic models compared to traditional social media.", + date: "Dec 15, 2025", + readTime: "12 min read", + image: image27, + category: "Research", + tag: "Free", + badgeColor: "#9d4edd", + isFeatured: false, + gradient: "linear-gradient(135deg, #9d4edd30, #f59e0b20)" + }, + { + id: 28, + title: "Crypto Insurance Market", + excerpt: "Overview of the growing crypto insurance market: coverage options, premium trends, and risk assessment methodologies.", + date: "Dec 14, 2025", + readTime: "11 min read", + image: image27, + category: "Market Intelligence", + tag: "Premium", + badgeColor: "#f59e0b", + isFeatured: false, + gradient: "linear-gradient(135deg, #f59e0b30, #4559DC20)" + }, + { + id: 29, + title: "Bitcoin Layer 2 Solutions", + excerpt: "Comparative analysis of Bitcoin Layer 2 scaling solutions: Lightning Network, Stacks, Rootstock, and emerging protocols.", + date: "Dec 13, 2025", + readTime: "14 min read", + image: image29, + category: "Research", + tag: "Free", + badgeColor: "#4559DC", + isFeatured: false, + gradient: "linear-gradient(135deg, #4559DC30, #22c55e20)" + }, + { + id: 30, + title: "Crypto Market Forecast 2026", + excerpt: "Data-driven forecast for crypto markets in 2026 based on historical patterns, on-chain indicators, and macro trends.", + date: "Dec 12, 2025", + readTime: "15 min read", + image: image30, + category: "Research", + tag: "Premium", + badgeColor: "#22c55e", + isFeatured: false, + gradient: "linear-gradient(135deg, #22c55e30, #9d4edd20)" + } + ]; + }; \ No newline at end of file diff --git a/src/firebase.js b/src/firebase.js index 21aa647..43fc8dc 100644 --- a/src/firebase.js +++ b/src/firebase.js @@ -1,19 +1,34 @@ import { initializeApp } from "firebase/app"; -import { getAuth, GoogleAuthProvider } from "firebase/auth"; +import { getAuth, GoogleAuthProvider, setPersistence, browserLocalPersistence } from "firebase/auth"; import { getFirestore } from "firebase/firestore"; +import { validateFirebaseConfig } from "./utils/firebaseValidation"; -// Check if Firebase credentials are configured -const isFirebaseConfigured = () => { - return !!import.meta.env.VITE_FIREBASE_API_KEY && - import.meta.env.VITE_FIREBASE_API_KEY !== 'your-firebase-api-key' && - import.meta.env.VITE_FIREBASE_API_KEY !== 'your-actual-firebase-api-key'; -}; - +// Firebase initialization state let app = null; let auth = null; let db = null; let googleProvider = null; +let firebaseValidationResult = null; +let initializationError = null; + +/** + * Check if Firebase credentials are configured + * Enhanced with validation utility + */ +const isFirebaseConfigured = () => { + const configCheck = validateFirebaseConfig(); + return configCheck.isValid; +}; + +/** + * Get detailed Firebase configuration status + * @returns {Object} Validation result with details + */ +const getFirebaseConfigStatus = () => { + return validateFirebaseConfig(); +}; +// Initialize Firebase if configured if (isFirebaseConfigured()) { const firebaseConfig = { apiKey: import.meta.env.VITE_FIREBASE_API_KEY, @@ -27,14 +42,88 @@ if (isFirebaseConfigured()) { try { app = initializeApp(firebaseConfig); auth = getAuth(app); + + // Set default persistence to local storage for login persistence across page refreshes + setPersistence(auth, browserLocalPersistence).catch((error) => { + if (import.meta.env.DEV) { + console.warn('Failed to set auth persistence:', error); + } + initializationError = { + stage: 'persistence', + error: error.message, + code: error.code, + }; + }); + db = getFirestore(app); googleProvider = new GoogleAuthProvider(); + + // Log successful initialization in development + if (import.meta.env.DEV) { + console.log('✓ Firebase initialized successfully'); + } } catch (error) { - console.warn('Firebase initialization failed:', error.message); + if (import.meta.env.DEV) { + console.error('Firebase initialization failed:', error); + } + initializationError = { + stage: 'initialization', + error: error.message, + code: error.code, + stack: error.stack, + }; + + // Clear partially initialized instances + app = null; + auth = null; + db = null; + googleProvider = null; } } else { - console.warn('Firebase not configured. Authentication features will be disabled.'); + // Log configuration status with details + const configStatus = validateFirebaseConfig(); + + if (import.meta.env.DEV) { + console.warn('⚠ Firebase not configured. Authentication features will be disabled.'); + + if (configStatus.missingVars.length > 0) { + console.warn('Missing environment variables:', configStatus.missingVars); + } + if (configStatus.placeholderVars.length > 0) { + console.warn('Placeholder values detected in:', configStatus.placeholderVars); + } + } + + initializationError = { + stage: 'configuration', + details: configStatus, + }; } -export { auth, db, googleProvider, isFirebaseConfigured }; +/** + * Get Firebase initialization error if any + * @returns {Object|null} Error details or null if no error + */ +const getInitializationError = () => { + return initializationError; +}; + +/** + * Check if Firebase is ready to use + * @returns {boolean} True if Firebase is fully initialized and ready + */ +const isFirebaseReady = () => { + return app !== null && auth !== null && db !== null; +}; + +export { + auth, + db, + googleProvider, + isFirebaseConfigured, + getFirebaseConfigStatus, + getInitializationError, + isFirebaseReady, +}; + export default app; diff --git a/src/index.css b/src/index.css index 25237aa..4ce4e59 100644 --- a/src/index.css +++ b/src/index.css @@ -93,7 +93,7 @@ body::before { a { text-decoration: none; color: inherit; - transition: all 0.3s ease; + transition: color 0.2s ease, opacity 0.2s ease; } /* ----------------------------- @@ -304,4 +304,38 @@ body[data-theme="light"] .red { .animate-slide-up { animation: slideUp 0.6s ease-out forwards; +} + +/* Base button */ +button { + cursor: pointer; + transition: all 0.2s ease-in-out; +} + +/* Hover state */ +button:hover { + opacity: 0.9; + transform: translateY(-1px); +} + +/* Active (pressed) state */ +button:active { + transform: scale(0.97); +} + +/* Focus (keyboard accessibility) */ +button:focus-visible { + outline: 2px solid #3b82f6; + outline-offset: 2px; +} + +/* Disabled state */ +button:disabled { + cursor: not-allowed; + opacity: 0.6; +} + +a:hover { + text-decoration: underline; + opacity: 0.85; } \ No newline at end of file diff --git a/src/main.jsx b/src/main.jsx index 24786d7..85a2d82 100644 --- a/src/main.jsx +++ b/src/main.jsx @@ -4,15 +4,68 @@ import "./index.css"; import App from "./App.jsx"; import { BrowserRouter } from "react-router-dom"; import { CoinContextProvider } from "./context/CoinContext"; -import { AuthProvider } from "./context/AuthContext"; +import { AuthProvider } from "./context/AuthProvider"; import { ThemeProvider } from "./context/ThemeContext"; import { LeaderboardProvider } from "./context/LeaderboardContext"; import { HelmetProvider } from 'react-helmet-async'; // 1. Import React Query import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +// 2. Import centralized API config for consistent cache/retry settings +import { API_CONFIG } from './config/apiConfig'; -// 2. Create the client -const queryClient = new QueryClient(); +// 3. Create the client with rate-limit-aware retry and caching configuration +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + // How long (ms) data is considered fresh — no refetch during this window + staleTime: API_CONFIG.QUERY.STALE_TIME, // 60 seconds + + // How long (ms) inactive query data stays in memory before garbage collection + gcTime: API_CONFIG.QUERY.GC_TIME, // 5 minutes + + // Do NOT refetch just because the user switches back to the browser tab. + // This prevents bursts of API calls on tab focus which can trigger rate limits. + refetchOnWindowFocus: false, + + // Do NOT refetch on reconnect — stale cache is shown instead. + // The user can manually refresh if needed. + refetchOnReconnect: false, + + // Smart retry function — respects rate limit and auth errors + retry: (failureCount, error) => { + // Never retry on these — they won't succeed regardless of attempts + const nonRetryableStatuses = new Set([400, 401, 403, 404, 422]); + const status = error?.status || error?.response?.status; + + if (status && nonRetryableStatuses.has(status)) return false; + + // For 429 (rate limited), let our apiClient's exponential backoff handle it. + // React Query should NOT add its own retries on top. + if (status === 429) return false; + + // For everything else (network errors, 5xx) — retry up to configured limit + return failureCount < API_CONFIG.QUERY.RETRY_COUNT; // 3 retries + }, + + // Exponential backoff for React Query's own retry delays + // (complements the backoff inside apiClient.js) + retryDelay: (attemptIndex) => { + const base = API_CONFIG.RETRY.BASE_DELAY; // 1000ms + const max = API_CONFIG.RETRY.MAX_DELAY; // 30000ms + return Math.min(base * Math.pow(2, attemptIndex), max); + }, + }, + + mutations: { + // Mutations (write operations) — retry once on network error only + retry: (failureCount, error) => { + const status = error?.status || error?.response?.status; + if (status && status < 500) return false; // Don't retry client errors + return failureCount < 1; + }, + }, + }, +}); createRoot(document.getElementById("root")).render( @@ -20,7 +73,7 @@ createRoot(document.getElementById("root")).render( - {/* 3. Wrap everything with QueryClientProvider */} + {/* 4. Wrap everything with QueryClientProvider */} diff --git a/src/pages/AIBlog/AIBlogPage.jsx b/src/pages/AIBlog/AIBlogPage.jsx new file mode 100644 index 0000000..4640def --- /dev/null +++ b/src/pages/AIBlog/AIBlogPage.jsx @@ -0,0 +1,107 @@ +import React, { useState } from "react"; +import { useNavigate } from "react-router-dom"; +import { motion } from "framer-motion"; +import AIBlogGenerator from "../../components/AIBlog/AIBlogGenerator"; +import AIBlogCard from "../../components/AIBlog/AIBlogCard"; +import "../../components/AIBlog/AIBlogGenerator.css"; + +/** + * AIBlogPage — standalone page for the AI Blog Generator feature. + * Accessible at /ai-blog. Maintains a session history of generated articles. + */ +export default function AIBlogPage() { + const navigate = useNavigate(); + const [history, setHistory] = useState([]); + + const handleArticleGenerated = (article) => { + setHistory((prev) => [ + { ...article, generatedAt: new Date().toISOString() }, + ...prev, + ]); + }; + + return ( +
+
+ {/* Back Link */} + + + {/* Hero Section */} + +
+ + + + AI-Powered Content Generation +
+ +

+ AI Blog Generator +

+ +

+ Type any crypto topic and instantly get a clean, educational blog + post generated by AI. Perfect for learning about blockchain, + DeFi, staking, and more. +

+
+ + {/* Generator Component */} + + + {/* Previously Generated Articles */} + {history.length > 1 && ( + +
+

+ Previously Generated +

+ + {history.length - 1} + +
+ + {history.slice(1).map((article, index) => ( + { + window.scrollTo({ top: 0, behavior: "smooth" }); + }} + /> + ))} +
+ )} +
+
+ ); +} diff --git a/src/pages/ApiAccess.css b/src/pages/ApiAccess.css new file mode 100644 index 0000000..8cc140a --- /dev/null +++ b/src/pages/ApiAccess.css @@ -0,0 +1,805 @@ +/* ===================================================== + API ACCESS PAGE — CryptoHub Design System + Purple/cyan gradient theme, glass morphism + ===================================================== */ + +.api-access-container { + max-width: 1200px; + margin: 0 auto; + padding-bottom: 80px; + position: relative; + overflow: hidden; +} + +/* ------------------------- + GLASS UTILITY + ------------------------- */ +.glass-panel { + background: rgba(255, 255, 255, 0.03); + border: 1px solid rgba(255, 255, 255, 0.08); + backdrop-filter: blur(20px); + border-radius: 20px; +} + +.glass-card { + background: rgba(139, 92, 246, 0.08); + border: 1px solid rgba(139, 92, 246, 0.2); + backdrop-filter: blur(12px); + border-radius: 16px; +} + +/* ------------------------- + HERO + ------------------------- */ +.aa-hero { + min-height: 52vh; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + position: relative; + text-align: center; + padding: 70px 16px 50px; +} + +.aa-hero-glow { + position: absolute; + top: 35%; + left: 50%; + transform: translate(-50%, -50%); + width: 700px; + height: 700px; + background: radial-gradient( + circle, + rgba(124, 58, 237, 0.18) 0%, + rgba(79, 70, 229, 0.08) 40%, + transparent 70% + ); + z-index: -1; + filter: blur(80px); + pointer-events: none; +} + +.aa-hero-glow-secondary { + position: absolute; + top: 20%; + right: 8%; + width: 400px; + height: 400px; + background: radial-gradient( + circle, + rgba(6, 182, 212, 0.1) 0%, + transparent 65% + ); + z-index: -1; + filter: blur(60px); + pointer-events: none; +} + +.aa-hero-content { + position: relative; + z-index: 2; + max-width: 800px; + margin: 0 auto; +} + +.aa-hero-badge { + display: inline-flex; + align-items: center; + gap: 8px; + padding: 8px 20px; + background: rgba(139, 92, 246, 0.15); + border: 1px solid rgba(139, 92, 246, 0.3); + border-radius: 50px; + color: #a78bfa; + font-size: 0.9rem; + font-weight: 600; + margin-bottom: 24px; +} + +.badge-icon { + font-size: 1rem; +} + +.aa-hero-title { + font-size: clamp(2.5rem, 6vw, 4.5rem); + font-weight: 800; + line-height: 1.1; + margin-bottom: 20px; +} + +.aa-title-purple { + background: linear-gradient(135deg, #8b5cf6, #a78bfa); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +.aa-title-cyan { + background: linear-gradient(135deg, #06b6d4, #22d3ee); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +.aa-hero-subtitle { + font-size: 1.1rem; + color: rgba(255, 255, 255, 0.6); + max-width: 580px; + margin: 0 auto 40px; + line-height: 1.75; +} + +.aa-ext-link { + color: #a78bfa; + text-decoration: underline; + text-underline-offset: 3px; +} + +.aa-hero-stats { + display: flex; + gap: 16px; + justify-content: center; + flex-wrap: wrap; +} + +.aa-stat-card { + display: flex; + align-items: center; + gap: 12px; + padding: 16px 24px; + min-width: 130px; +} + +.aa-stat-card .stat-icon { + font-size: 1.4rem; + color: #a78bfa; + flex-shrink: 0; +} + +.stat-info { + display: flex; + flex-direction: column; +} + +.stat-value { + font-size: 1.25rem; + font-weight: 700; + color: #fff; +} + +.stat-label { + font-size: 0.72rem; + color: rgba(255, 255, 255, 0.45); + text-transform: uppercase; + letter-spacing: 0.05em; +} + +/* ------------------------- + CONTENT SECTIONS + ------------------------- */ +.aa-section { + padding: 0 24px 48px; +} + +.aa-h2 { + display: flex; + align-items: center; + gap: 10px; + font-size: 1.6rem; + font-weight: 700; + color: #fff; + margin-bottom: 20px; +} + +.aa-h2-icon { + color: #a78bfa; + font-size: 1.3rem; +} + +.aa-section-sub { + color: rgba(255, 255, 255, 0.5); + margin-bottom: 20px; + font-size: 0.95rem; +} + +/* ------------------------- + BASE URL + ------------------------- */ +.aa-base-url { + display: flex; + align-items: center; + gap: 16px; + padding: 18px 24px; + flex-wrap: wrap; +} + +.aa-base-label { + font-size: 0.75rem; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.08em; + color: rgba(255, 255, 255, 0.4); + white-space: nowrap; +} + +.aa-base-value { + flex: 1; + font-size: 1rem; + color: #22d3ee; + font-family: "Fira Code", "Consolas", monospace; +} + +/* ------------------------- + API KEY STEPS + ------------------------- */ +.aa-key-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); + gap: 20px; + margin-bottom: 20px; +} + +.aa-key-step { + padding: 28px 24px; + border-radius: 20px; +} + +.aa-step-num { + display: block; + font-size: 2rem; + font-weight: 800; + color: rgba(139, 92, 246, 0.6); + margin-bottom: 12px; + font-variant-numeric: tabular-nums; +} + +.aa-key-step h3 { + font-size: 1rem; + font-weight: 700; + color: #fff; + margin-bottom: 8px; +} + +.aa-key-step p { + font-size: 0.88rem; + color: rgba(255, 255, 255, 0.55); + line-height: 1.65; +} + +.aa-key-step code { + background: rgba(139, 92, 246, 0.15); + border-radius: 4px; + padding: 1px 6px; + color: #c4b5fd; + font-size: 0.85em; +} + +.aa-notice { + display: flex; + align-items: flex-start; + gap: 12px; + padding: 18px 24px; + background: rgba(250, 204, 21, 0.07); + border-color: rgba(250, 204, 21, 0.25); +} + +.aa-notice-icon { + font-size: 1.2rem; + color: #facc15; + flex-shrink: 0; + margin-top: 2px; +} + +.aa-notice p { + font-size: 0.88rem; + color: rgba(255, 255, 255, 0.7); + line-height: 1.6; +} + +.aa-notice code { + background: rgba(250, 204, 21, 0.12); + border-radius: 4px; + padding: 1px 5px; + color: #fde047; + font-size: 0.85em; +} + +/* ------------------------- + ENDPOINT CARDS + ------------------------- */ +.aa-endpoints-list { + display: flex; + flex-direction: column; + gap: 12px; +} + +.aa-endpoint-card { + overflow: hidden; + transition: border-color 0.2s; +} + +.aa-endpoint-card:hover { + border-color: rgba(139, 92, 246, 0.25); +} + +.aa-endpoint-header { + width: 100%; + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; + padding: 18px 24px; + background: transparent; + border: none; + cursor: pointer; + text-align: left; + color: #fff; + transition: background 0.2s; +} + +.aa-endpoint-header:hover { + background: rgba(139, 92, 246, 0.06); +} + +.aa-endpoint-title { + display: flex; + align-items: center; + gap: 12px; + flex-shrink: 0; +} + +.aa-method-badge { + display: inline-block; + padding: 4px 10px; + border-radius: 6px; + font-size: 0.75rem; + font-weight: 700; + letter-spacing: 0.04em; + flex-shrink: 0; +} + +.aa-method-get { + background: rgba(34, 197, 94, 0.15); + color: #4ade80; + border: 1px solid rgba(34, 197, 94, 0.3); +} + +.aa-path { + font-family: "Fira Code", "Consolas", monospace; + font-size: 0.95rem; + color: #22d3ee; +} + +.aa-endpoint-right { + display: flex; + align-items: center; + gap: 12px; + color: rgba(255, 255, 255, 0.5); + font-size: 0.88rem; + flex: 1; + justify-content: flex-end; +} + +.aa-endpoint-desc-inline { + display: -webkit-box; + -webkit-line-clamp: 1; + -webkit-box-orient: vertical; + overflow: hidden; + text-align: right; +} + +/* Endpoint Body */ +.aa-endpoint-body { + border-top: 1px solid rgba(255, 255, 255, 0.06); + padding: 24px; + display: flex; + flex-direction: column; + gap: 24px; +} + +.aa-desc { + color: rgba(255, 255, 255, 0.6); + font-size: 0.92rem; + line-height: 1.65; +} + +.aa-section-label { + font-size: 0.8rem; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.08em; + color: rgba(255, 255, 255, 0.4); + margin-bottom: 12px; +} + +/* Parameters table */ +.aa-params-table { + border-radius: 12px; + overflow: hidden; + border: 1px solid rgba(255, 255, 255, 0.07); +} + +.aa-params-head { + display: grid; + grid-template-columns: 1.2fr 0.7fr 0.6fr 2fr; + padding: 10px 16px; + background: rgba(255, 255, 255, 0.04); + font-size: 0.75rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.06em; + color: rgba(255, 255, 255, 0.35); + gap: 12px; +} + +.aa-params-row { + display: grid; + grid-template-columns: 1.2fr 0.7fr 0.6fr 2fr; + padding: 12px 16px; + border-top: 1px solid rgba(255, 255, 255, 0.05); + gap: 12px; + align-items: start; +} + +.aa-param-name { + color: #c4b5fd; + font-family: "Fira Code", "Consolas", monospace; + font-size: 0.85rem; +} + +.aa-param-type { + color: #94a3b8; + font-size: 0.83rem; +} + +.aa-param-req { + font-size: 0.78rem; + font-weight: 600; + border-radius: 50px; + padding: 2px 10px; + width: fit-content; +} + +.aa-param-req.required { + background: rgba(239, 68, 68, 0.12); + color: #f87171; + border: 1px solid rgba(239, 68, 68, 0.25); +} + +.aa-param-req.optional { + background: rgba(100, 116, 139, 0.15); + color: #94a3b8; + border: 1px solid rgba(100, 116, 139, 0.2); +} + +.aa-param-desc { + color: rgba(255, 255, 255, 0.5); + font-size: 0.83rem; + line-height: 1.5; +} + +/* Code blocks */ +.aa-code-tabs { + display: flex; + align-items: center; + justify-content: space-between; + flex-wrap: wrap; + gap: 8px; + margin-bottom: 10px; +} + +.aa-tab-btns { + display: flex; + gap: 6px; +} + +.aa-tab-btn { + padding: 6px 16px; + border-radius: 50px; + border: 1px solid rgba(255, 255, 255, 0.1); + background: transparent; + color: rgba(255, 255, 255, 0.5); + font-size: 0.82rem; + cursor: pointer; + transition: all 0.2s; +} + +.aa-tab-btn:hover { + border-color: rgba(139, 92, 246, 0.4); + color: #a78bfa; +} + +.aa-tab-btn.active { + background: rgba(139, 92, 246, 0.18); + border-color: rgba(139, 92, 246, 0.4); + color: #a78bfa; + font-weight: 600; +} + +.aa-code-block { + position: relative; + background: rgba(0, 0, 0, 0.35); + border: 1px solid rgba(255, 255, 255, 0.07); + border-radius: 12px; + padding: 20px 20px 20px 20px; + overflow-x: auto; +} + +.aa-code-block pre { + margin: 0; + padding-top: 4px; +} + +.aa-code-block code { + font-family: "Fira Code", "Consolas", monospace; + font-size: 0.82rem; + color: #e2e8f0; + line-height: 1.75; + white-space: pre; +} + +.aa-response-block { + background: rgba(6, 182, 212, 0.04); + border-color: rgba(6, 182, 212, 0.1); +} + +.aa-env-block { + margin-top: 10px; +} + +/* Copy button */ +.aa-copy-btn { + position: absolute; + top: 12px; + right: 12px; + display: flex; + align-items: center; + gap: 5px; + padding: 5px 12px; + border-radius: 8px; + border: 1px solid rgba(255, 255, 255, 0.1); + background: rgba(255, 255, 255, 0.05); + color: rgba(255, 255, 255, 0.55); + font-size: 0.75rem; + cursor: pointer; + transition: all 0.2s; +} + +.aa-copy-btn:hover { + background: rgba(139, 92, 246, 0.15); + border-color: rgba(139, 92, 246, 0.3); + color: #a78bfa; +} + +/* ------------------------- + RATE LIMITS + ------------------------- */ +.aa-rate-table { + overflow: hidden; + margin-bottom: 14px; +} + +.aa-rate-head { + display: grid; + grid-template-columns: 1.5fr 1fr 2fr; + padding: 12px 24px; + background: rgba(255, 255, 255, 0.04); + font-size: 0.75rem; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.06em; + color: rgba(255, 255, 255, 0.35); + gap: 16px; +} + +.aa-rate-row { + display: grid; + grid-template-columns: 1.5fr 1fr 2fr; + padding: 15px 24px; + border-top: 1px solid rgba(255, 255, 255, 0.05); + gap: 16px; + align-items: center; + transition: background 0.2s; +} + +.aa-rate-row:hover { + background: rgba(139, 92, 246, 0.05); +} + +.aa-rate-tier { + font-weight: 600; + color: #fff; + font-size: 0.9rem; +} + +.aa-rate-limit { + color: #4ade80; + font-weight: 600; + font-size: 0.9rem; +} + +.aa-rate-notes { + color: rgba(255, 255, 255, 0.5); + font-size: 0.85rem; +} + +.aa-rate-note { + color: rgba(255, 255, 255, 0.45); + font-size: 0.85rem; + line-height: 1.6; +} + +.aa-rate-note code { + background: rgba(239, 68, 68, 0.12); + color: #f87171; + border-radius: 4px; + padding: 1px 6px; + font-size: 0.85em; +} + +/* ------------------------- + AUTHENTICATION + ------------------------- */ +.aa-auth-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); + gap: 20px; +} + +.aa-auth-card { + padding: 24px; +} + +.aa-auth-card h3 { + font-size: 1rem; + font-weight: 700; + color: #fff; + margin-bottom: 8px; +} + +.aa-auth-card p { + font-size: 0.87rem; + color: rgba(255, 255, 255, 0.5); + margin-bottom: 14px; +} + +/* ------------------------- + FAQ + ------------------------- */ +.aa-faq-list { + display: flex; + flex-direction: column; + gap: 10px; +} + +.aa-faq-item { + overflow: hidden; +} + +.aa-faq-q { + width: 100%; + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; + padding: 18px 24px; + background: transparent; + border: none; + cursor: pointer; + text-align: left; + color: #fff; + font-size: 0.95rem; + font-weight: 600; + transition: background 0.2s; +} + +.aa-faq-q:hover { + background: rgba(139, 92, 246, 0.06); +} + +.aa-faq-q svg { + flex-shrink: 0; + color: #a78bfa; +} + +.aa-faq-a { + padding: 0 24px 20px; + color: rgba(255, 255, 255, 0.55); + font-size: 0.88rem; + line-height: 1.7; + border-top: 1px solid rgba(255, 255, 255, 0.05); +} + +/* ------------------------- + CTA + ------------------------- */ +.aa-cta-section { + padding-bottom: 20px; +} + +.aa-cta { + text-align: center; + padding: 60px 40px; + background: radial-gradient( + ellipse at center, + rgba(124, 58, 237, 0.12) 0%, + rgba(0, 0, 0, 0) 70% + ); +} + +.aa-cta h2 { + font-size: 2rem; + font-weight: 800; + color: #fff; + margin-bottom: 12px; +} + +.aa-cta p { + color: rgba(255, 255, 255, 0.55); + font-size: 1rem; + margin-bottom: 28px; +} + +.aa-cta-btn { + display: inline-flex; + align-items: center; + gap: 8px; + padding: 14px 32px; + background: linear-gradient(135deg, #7c3aed, #6366f1); + border-radius: 50px; + color: #fff; + text-decoration: none; + font-weight: 700; + font-size: 0.95rem; + transition: transform 0.2s, box-shadow 0.2s; + box-shadow: 0 4px 24px rgba(124, 58, 237, 0.3); +} + +.aa-cta-btn:hover { + transform: translateY(-2px); + box-shadow: 0 8px 32px rgba(124, 58, 237, 0.5); +} + +/* ------------------------- + RESPONSIVE + ------------------------- */ +@media (max-width: 768px) { + .aa-params-head, + .aa-params-row { + grid-template-columns: 1.2fr 0.7fr 2fr; + } + .aa-param-req { + display: none; + } + + .aa-rate-head, + .aa-rate-row { + grid-template-columns: 1fr 1fr; + } + .aa-rate-notes { + display: none; + } + + .aa-endpoint-desc-inline { + display: none; + } + + .aa-hero-title { + font-size: 2.2rem; + } +} + +@media (max-width: 540px) { + .aa-params-head, + .aa-params-row { + grid-template-columns: 1fr 2fr; + } + .aa-param-type { + display: none; + } + + .aa-section { + padding: 0 14px 40px; + } +} diff --git a/src/pages/ApiAccess.jsx b/src/pages/ApiAccess.jsx new file mode 100644 index 0000000..4fb4511 --- /dev/null +++ b/src/pages/ApiAccess.jsx @@ -0,0 +1,612 @@ +import React, { useState } from "react"; +import { motion } from "framer-motion"; +import { + FiCode, + FiCopy, + FiCheck, + FiKey, + FiZap, + FiBookOpen, + FiAlertCircle, + FiChevronDown, + FiChevronUp, + FiExternalLink, +} from "react-icons/fi"; +import "./ApiAccess.css"; + +// --------------------------------------------------------------------------- +// Data +// --------------------------------------------------------------------------- +const ENDPOINTS = [ + { + method: "GET", + path: "/coins/markets", + description: "Fetch a list of coins with market data (price, volume, market cap, 24h change).", + params: [ + { name: "vs_currency", type: "string", required: true, desc: "Target currency (e.g. usd, eur, inr)" }, + { name: "order", type: "string", required: false, desc: "market_cap_desc | market_cap_asc | volume_desc" }, + { name: "per_page", type: "number", required: false, desc: "Results per page (1–250). Default: 100" }, + { name: "page", type: "number", required: false, desc: "Page number. Default: 1" }, + { name: "sparkline", type: "boolean", required: false, desc: "Include sparkline 7d data. Default: false" }, + { name: "price_change_percentage", type: "string", required: false, desc: "Comma-separated intervals: 1h,24h,7d,14d,30d" }, + { name: "x_cg_demo_api_key", type: "string", required: false, desc: "Your CoinGecko Demo API Key" }, + ], + example: { + fetch: `const res = await fetch( + "https://api.coingecko.com/api/v3/coins/markets?" + + new URLSearchParams({ + vs_currency: "usd", + order: "market_cap_desc", + per_page: "10", + page: "1", + sparkline: "false", + price_change_percentage: "24h", + x_cg_demo_api_key: "YOUR_API_KEY", + }) +); +const data = await res.json(); +console.log(data);`, + curl: `curl -X GET \\ + "https://api.coingecko.com/api/v3/coins/markets?vs_currency=usd&order=market_cap_desc&per_page=10&page=1" \\ + -H "accept: application/json" \\ + -H "x-cg-demo-api-key: YOUR_API_KEY"`, + }, + responseSnippet: `[ + { + "id": "bitcoin", + "symbol": "btc", + "name": "Bitcoin", + "current_price": 67234.12, + "market_cap": 1324678900000, + "market_cap_rank": 1, + "total_volume": 28456789012, + "price_change_percentage_24h": 2.45, + "circulating_supply": 19700000, + "image": "https://assets.coingecko.com/coins/images/1/large/bitcoin.png" + }, + ... +]`, + }, + { + method: "GET", + path: "/coins/{id}", + description: "Get detailed data for a single coin including description, links, market data, and community stats.", + params: [ + { name: "id", type: "string", required: true, desc: "Coin ID (e.g. bitcoin, ethereum, solana)" }, + { name: "localization", type: "boolean", required: false, desc: "Include all localized languages. Default: true" }, + { name: "tickers", type: "boolean", required: false, desc: "Include tickers data. Default: true" }, + { name: "market_data", type: "boolean", required: false, desc: "Include market data. Default: true" }, + { name: "community_data", type: "boolean", required: false, desc: "Include community data. Default: true" }, + { name: "developer_data", type: "boolean", required: false, desc: "Include developer activity data. Default: true" }, + ], + example: { + fetch: `const res = await fetch( + "https://api.coingecko.com/api/v3/coins/bitcoin?" + + new URLSearchParams({ + localization: "false", + tickers: "false", + market_data: "true", + community_data: "false", + developer_data: "false", + }), + { headers: { "x-cg-demo-api-key": "YOUR_API_KEY" } } +); +const data = await res.json(); +console.log(data.market_data.current_price);`, + curl: `curl -X GET \\ + "https://api.coingecko.com/api/v3/coins/bitcoin?localization=false&tickers=false&market_data=true" \\ + -H "accept: application/json" \\ + -H "x-cg-demo-api-key: YOUR_API_KEY"`, + }, + responseSnippet: `{ + "id": "bitcoin", + "symbol": "btc", + "name": "Bitcoin", + "description": { "en": "Bitcoin is a decentralized..." }, + "market_data": { + "current_price": { "usd": 67234.12, "eur": 61200 }, + "market_cap": { "usd": 1324678900000 }, + "price_change_percentage_24h": 2.45, + "ath": { "usd": 73750 }, + "atl": { "usd": 67.81 } + } +}`, + }, + { + method: "GET", + path: "/coins/{id}/market_chart", + description: "Get historical market data (price, market cap, volume) over a given time range for charting.", + params: [ + { name: "id", type: "string", required: true, desc: "Coin ID (e.g. bitcoin)" }, + { name: "vs_currency", type: "string", required: true, desc: "Target currency (e.g. usd)" }, + { name: "days", type: "string", required: true, desc: "Number of days: 1, 7, 14, 30, 90, 180, 365, max" }, + { name: "interval", type: "string", required: false, desc: "Data interval: daily (optional, auto-calculated otherwise)" }, + ], + example: { + fetch: `const res = await fetch( + "https://api.coingecko.com/api/v3/coins/bitcoin/market_chart?" + + new URLSearchParams({ + vs_currency: "usd", + days: "30", + interval: "daily", + }), + { headers: { "x-cg-demo-api-key": "YOUR_API_KEY" } } +); +const { prices, market_caps, total_volumes } = await res.json(); +// prices: [[timestamp, price], ...]`, + curl: `curl -X GET \\ + "https://api.coingecko.com/api/v3/coins/bitcoin/market_chart?vs_currency=usd&days=30&interval=daily" \\ + -H "accept: application/json" \\ + -H "x-cg-demo-api-key: YOUR_API_KEY"`, + }, + responseSnippet: `{ + "prices": [ + [1706745600000, 42365.12], + [1706832000000, 43102.87], + ... + ], + "market_caps": [ + [1706745600000, 832000000000], + ... + ], + "total_volumes": [ + [1706745600000, 18900000000], + ... + ] +}`, + }, + { + method: "GET", + path: "/search/trending", + description: "Get the top 7 trending coins on CoinGecko in the last 24 hours based on search volume.", + params: [], + example: { + fetch: `const res = await fetch( + "https://api.coingecko.com/api/v3/search/trending", + { headers: { "x-cg-demo-api-key": "YOUR_API_KEY" } } +); +const { coins } = await res.json(); +console.log(coins.map(c => c.item.name));`, + curl: `curl -X GET \\ + "https://api.coingecko.com/api/v3/search/trending" \\ + -H "accept: application/json" \\ + -H "x-cg-demo-api-key: YOUR_API_KEY"`, + }, + responseSnippet: `{ + "coins": [ + { + "item": { + "id": "solana", + "name": "Solana", + "symbol": "SOL", + "market_cap_rank": 5, + "score": 0 + } + }, + ... + ] +}`, + }, +]; + +const RATE_LIMITS = [ + { tier: "Free (No Key)", limit: "10–15 calls/min", notes: "Public access, no key needed. May be throttled." }, + { tier: "Demo Key", limit: "30 calls/min", notes: "Free CoinGecko Demo API key via coingecko.com." }, + { tier: "Analyst", limit: "500 calls/min", notes: "Paid plan — includes priority access." }, + { tier: "Lite", limit: "500 calls/min", notes: "Paid plan — dedicated endpoints." }, + { tier: "Pro", limit: "1000 calls/min", notes: "Highest throughput for production apps." }, +]; + +const FAQS = [ + { + q: "Where does CryptoHub get its data?", + a: "All market data is sourced from the CoinGecko API (v3). CryptoHub uses the public endpoints for prices, market caps, volumes, and coin details.", + }, + { + q: "How do I get a free API key?", + a: "Register at coingecko.com/en/developers/dashboard and generate a free Demo API key. Add it as VITE_CG_API_KEY in your .env file.", + }, + { + q: "Do I need an API key to run CryptoHub?", + a: "No. CryptoHub works without a key using CoinGecko's public endpoints, but you may hit rate limits faster. A free Demo key is recommended.", + }, + { + q: "How do I set the API key in the project?", + a: 'Create a .env file at the project root and add: VITE_CG_API_KEY=your_key_here. Then restart the dev server.', + }, + { + q: "Are there WebSocket / real-time feeds?", + a: "CoinGecko's free tier does not provide WebSocket streams. CryptoHub polls the REST API on an interval to refresh data.", + }, +]; + +// --------------------------------------------------------------------------- +// Small helpers +// --------------------------------------------------------------------------- +const MethodBadge = ({ method }) => ( + {method} +); + +const CopyButton = ({ text }) => { + const [copied, setCopied] = useState(false); + const handleCopy = () => { + navigator.clipboard.writeText(text); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + }; + return ( + + ); +}; + +const EndpointCard = ({ endpoint }) => { + const [open, setOpen] = useState(false); + const [tab, setTab] = useState("fetch"); + + return ( +
+ + + {open && ( +
+

{endpoint.description}

+ + {/* Parameters */} + {endpoint.params.length > 0 && ( +
+

Parameters

+
+
+ Name + Type + Required + Description +
+ {endpoint.params.map((p) => ( +
+ {p.name} + {p.type} + + {p.required ? "Yes" : "No"} + + {p.desc} +
+ ))} +
+
+ )} + + {/* Code Examples */} +
+
+

Example Request

+
+ + +
+
+
+ +
{endpoint.example[tab]}
+
+
+ + {/* Response */} +
+

Sample Response

+
+ +
{endpoint.responseSnippet}
+
+
+
+ )} +
+ ); +}; + +const FaqItem = ({ faq }) => { + const [open, setOpen] = useState(false); + return ( +
+ + {open &&

{faq.a}

} +
+ ); +}; + +// --------------------------------------------------------------------------- +// Main Page +// --------------------------------------------------------------------------- +const ApiAccess = () => { + return ( +
+ {/* Hero */} +
+
+
+ + +
+ + Developer API +
+ +

+ CryptoHub +
+ API Reference +

+ +

+ Everything you need to integrate and build with CryptoHub. All + market data is powered by the{" "} + + CoinGecko API v3 + + . +

+ +
+
+ +
+ 4 + Endpoints +
+
+
+ +
+ Free + Demo Key +
+
+
+ +
+ REST + Protocol +
+
+
+
+
+ + {/* Base URL banner */} +
+
+ Base URL + https://api.coingecko.com/api/v3 + +
+
+ + {/* API Key Setup */} +
+ +

+ API Key Setup +

+
+
+ 01 +

Create an Account

+

+ Visit{" "} + + coingecko.com/developers + {" "} + and sign up for a free account. +

+
+
+ 02 +

Generate a Demo Key

+

+ In the developer dashboard, create a new Demo API key. It's + free and gives you 30 calls/minute. +

+
+
+ 03 +

Add Key to Project

+

+ Create a .env file at the project root and add: +

+
+ +
VITE_CG_API_KEY=your_api_key_here
+
+
+
+ 04 +

Restart Dev Server

+

+ Stop the running server and run npm run dev again. + The key is now picked up automatically by CryptoHub. +

+
+
+ +
+ +

+ Never commit your API key. Add .env{" "} + to your .gitignore to prevent accidental exposure. +

+
+
+
+ + {/* Endpoints */} +
+ +

+ Available Endpoints +

+

+ Click any endpoint to expand its parameters, code examples, and sample response. +

+
+ {ENDPOINTS.map((ep) => ( + + ))} +
+
+
+ + {/* Rate Limits */} +
+ +

+ Rate Limits +

+
+
+ Tier + Rate Limit + Notes +
+ {RATE_LIMITS.map((r) => ( +
+ {r.tier} + {r.limit} + {r.notes} +
+ ))} +
+

+ When a rate limit is exceeded the API returns HTTP 429 Too Many Requests. Implement + exponential back-off in production apps. +

+
+
+ + {/* Authentication */} +
+ +

+ Authentication +

+
+
+

Query Parameter

+

Append your key as a URL query param:

+
+ +
?x_cg_demo_api_key=YOUR_KEY
+
+
+
+

Request Header

+

Pass the key as an HTTP header:

+
+ +
x-cg-demo-api-key: YOUR_KEY
+
+
+
+
+
+ + {/* FAQ */} +
+ +

+ FAQ +

+
+ {FAQS.map((faq) => ( + + ))} +
+
+
+ + {/* CTA */} +
+ +

Ready to build?

+

Get your free CoinGecko Demo API key and start integrating in minutes.

+ + Get Free API Key + +
+
+
+ ); +}; + +export default ApiAccess; diff --git a/src/pages/Callback.jsx b/src/pages/Callback.jsx index c1a5545..d89bfc7 100644 --- a/src/pages/Callback.jsx +++ b/src/pages/Callback.jsx @@ -1,6 +1,6 @@ import React, { useEffect } from 'react'; import { useNavigate } from 'react-router-dom'; -import LoadingSpinner from '../components/LoadingSpinner'; +import LoadingSpinner from '../components/Common/LoadingSpinner'; const Callback = () => { const navigate = useNavigate(); diff --git a/src/pages/Dashboard/Dashboard.jsx b/src/pages/Dashboard/Dashboard.jsx index 2bf9a34..7dd9e5c 100644 --- a/src/pages/Dashboard/Dashboard.jsx +++ b/src/pages/Dashboard/Dashboard.jsx @@ -1,6 +1,6 @@ import React, { useState, useEffect, useCallback } from "react"; -import { useNavigate } from "react-router-dom"; -import { useAuth } from "../../context/AuthContext"; +import { useNavigate, Link } from "react-router-dom"; +import { useAuth } from "../../context/AuthProvider"; import { useTheme } from "../../context/ThemeContext"; const Dashboard = () => { @@ -86,16 +86,16 @@ const Dashboard = () => {
{sidebarOpen && ( -
+ CryptoHub -

+

CryptoHub

-
+ )} -
-
-
+
+
+
+ + + +
+

+ Account +

+
+
+
+

+ Email +

+

+ {currentUser?.email} +

+
+
+

+ Status +

+ + + Active + +
+
+

+ Member Since +

+

+ {currentUser?.metadata?.creationTime + ? new Date( + currentUser.metadata.creationTime, + ).toLocaleDateString("en-US", { + month: "short", + day: "numeric", + year: "numeric", + }) + : "N/A"} +

+
+
+
+
+
+
- {/* Market Overview Widget */} - - - ); + {/* Market Overview Widget */} + + + ); }; // Market Overview Widget Component const MarketOverviewWidget = ({ isDark, navigate }) => { - const { allCoin, currency } = useContext(CoinContext); - const [topGainers, setTopGainers] = useState([]); - const [topCoins, setTopCoins] = useState([]); + const { allCoin, currency } = useContext(CoinContext); + const [topGainers, setTopGainers] = useState([]); + const [topCoins, setTopCoins] = useState([]); - useEffect(() => { - if (allCoin && allCoin.length > 0) { - // Get top 3 gainers - const gainers = [...allCoin] - .sort((a, b) => b.price_change_percentage_24h - a.price_change_percentage_24h) - .slice(0, 3); - setTopGainers(gainers); + useEffect(() => { + if (Array.isArray(allCoin) && allCoin.length > 0) { + // Get top 3 gainers + const gainers = [...allCoin] + .sort( + (a, b) => + (b.price_change_percentage_24h || 0) - + (a.price_change_percentage_24h || 0), + ) + .slice(0, 3); + setTopGainers(gainers); - // Get top 5 coins by market cap - const topByMarketCap = allCoin.slice(0, 5); - setTopCoins(topByMarketCap); - } - }, [allCoin]); + // Get top 5 coins by market cap + const topByMarketCap = allCoin.slice(0, 5); + setTopCoins(topByMarketCap); + } + }, [allCoin]); - return ( -
-
-
-
- - - -
-

- Market Overview -

-
- -
+ return ( +
+
+
+
+ + + +
+

+ Market Overview +

+
+ +
- {/* Top Gainers */} -
-
- - - -

- Top Gainers (24h) -

-
-
- {topGainers.map((coin) => ( -
navigate(`/coin/${coin.id}`)} - className={`p-4 rounded-xl border cursor-pointer transition-all duration-200 hover:scale-105 ${isDark - ? 'bg-[rgba(255,255,255,0.02)] border-[rgba(255,255,255,0.08)] hover:border-[rgba(0,217,255,0.2)]' - : 'bg-gray-50 border-gray-200 hover:border-cyan-300' - }`} - > -
- {coin.name} -
-

- {coin.name} -

-

- {coin.symbol.toUpperCase()} -

-
-
-

- +{coin.price_change_percentage_24h.toFixed(2)}% -

-
- ))} + {/* Top Gainers */} +
+
+ + + +

+ Top Gainers (24h) +

+
+
+ {topGainers.map((coin) => ( +
navigate(`/coin/${coin.id}`)} + className={`p-4 rounded-xl border cursor-pointer transition-all duration-200 hover:scale-105 ${ + isDark + ? "bg-[rgba(255,255,255,0.02)] border-[rgba(255,255,255,0.08)] hover:border-[rgba(0,217,255,0.2)]" + : "bg-gray-50 border-gray-200 hover:border-cyan-300" + }`} + > +
+ {coin.name} +
+

+ {coin.name} +

+

+ {coin.symbol.toUpperCase()} +

+
+

+ +{coin.price_change_percentage_24h?.toFixed(2)}% +

+ ))} +
+
- {/* Top Coins */} -
-
- - - -

- Top Cryptocurrencies -

-
-
- {topCoins.map((coin, index) => ( -
navigate(`/coin/${coin.id}`)} - className={`flex items-center justify-between p-3 rounded-lg cursor-pointer transition-all duration-200 ${isDark - ? 'hover:bg-[rgba(0,217,255,0.05)]' - : 'hover:bg-gray-50' - }`} - > -
- - #{index + 1} - - {coin.name} -
-

- {coin.name} -

-

- {coin.symbol.toUpperCase()} -

-
-
-
-

- {currency.Symbol}{coin.current_price.toLocaleString()} -

-

0 ? 'text-green-400' : 'text-red-400'}`}> - {coin.price_change_percentage_24h > 0 ? '+' : ''}{coin.price_change_percentage_24h.toFixed(2)}% -

-
-
- ))} + {/* Top Coins */} +
+
+ + + +

+ Top Cryptocurrencies +

+
+
+ {topCoins.map((coin, index) => ( +
navigate(`/coin/${coin.id}`)} + className={`flex items-center justify-between p-3 rounded-lg cursor-pointer transition-all duration-200 ${ + isDark ? "hover:bg-[rgba(0,217,255,0.05)]" : "hover:bg-gray-50" + }`} + > +
+ + #{index + 1} + + {coin.name} +
+

+ {coin.name} +

+

+ {coin.symbol.toUpperCase()} +

+
+
+

+ {currency.Symbol} + {coin.current_price?.toLocaleString()} +

+

0 ? "text-green-400" : "text-red-400"}`} + > + {(coin.price_change_percentage_24h || 0) > 0 ? "+" : ""} + {coin.price_change_percentage_24h?.toFixed(2)}% +

+
+ ))}
- ); +
+
+ ); }; export default DashboardContent; diff --git a/src/pages/Dashboard/DashboardLayout.jsx b/src/pages/Dashboard/DashboardLayout.jsx index 6c587bf..79ac32c 100644 --- a/src/pages/Dashboard/DashboardLayout.jsx +++ b/src/pages/Dashboard/DashboardLayout.jsx @@ -1,197 +1,381 @@ -import React, { useState, useEffect, useCallback } from "react"; -import { useNavigate, useLocation, Outlet } from "react-router-dom"; -import { useAuth } from "../../context/AuthContext"; -import { useTheme } from "../../context/ThemeContext"; +import React, { useState, useCallback } from "react"; +import { useNavigate, useLocation, Outlet, Link } from "react-router-dom"; +import { useAuth } from "../../context/useAuth"; +import { useTheme } from "../../context/useTheme"; const DashboardLayout = () => { - const { currentUser, logout } = useAuth(); - const { isDark, toggleTheme } = useTheme(); - const navigate = useNavigate(); - const location = useLocation(); - const [sidebarOpen, setSidebarOpen] = useState(true); + const { currentUser, logout } = useAuth(); + const { isDark } = useTheme(); + const navigate = useNavigate(); + const location = useLocation(); + const [sidebarOpen, setSidebarOpen] = useState(true); - const getFirstName = () => { - if (currentUser?.fullName) { - return currentUser.fullName.split(" ")[0]; - } - return currentUser?.email?.split("@")[0] || "User"; - }; + const getFirstName = () => { + if (currentUser?.fullName) { + return currentUser.fullName.split(" ")[0]; + } + return currentUser?.email?.split("@")[0] || "User"; + }; - const handleLogout = useCallback(async () => { - try { - await logout(); - navigate("/login"); - } catch (error) { - console.error("Failed to log out:", error); - alert("Failed to log out. Please try again."); - } - }, [logout, navigate]); + const handleLogout = useCallback(async () => { + try { + await logout(); + navigate("/login"); + } catch (error) { + console.error("Failed to log out:", error); + alert("Failed to log out. Please try again."); + } + }, [logout, navigate]); - const navigationItems = [ - { - icon: ( - - - - ), - label: "Dashboard", - path: "/dashboard" - }, - { - icon: ( - - - - ), - label: "Market Overview", - path: "/market-overview" - }, - { - icon: ( - - - - ), - label: "Leaderboard", - path: "/leaderboard" - }, - ]; + const navigationItems = [ + { + icon: ( + + + + ), + label: "Home", + path: "/", + }, + { + icon: ( + + + + ), + label: "Dashboard", + path: "/dashboard", + }, + { + icon: ( + + + + ), + label: "Market Overview", + path: "/market-overview", + }, + { + icon: ( + + + + ), + label: "Leaderboard", + path: "/leaderboard", + }, + { + icon: ( + + + + ), + label: "Saved Insights", + path: "/saved-insights", + }, + { + icon: ( + + + + ), + label: "Pricing", + path: "/pricing", + }, + { + icon: ( + + + + ), + label: "Insights", + path: "/blog", + }, + { + icon: ( + + + + ), + label: "Features", + path: "/features", + }, + ]; - return ( -
-
- + {sidebarOpen && ( +
+ {/* Logout Button */} + +
+ )} - {sidebarOpen && ( -
setSidebarOpen(false)} + {/* Collapsed State Logout Icon */} + {!sidebarOpen && ( + + )} +
+ -
-
- + {sidebarOpen && ( +
setSidebarOpen(false)} + /> + )} - {/* Child routes will render here */} - -
-
+
+
+ + + {/* Child routes will render here */} +
- ); +
+
+ ); }; export default DashboardLayout; diff --git a/src/pages/Dashboard/MarketOverview.css b/src/pages/Dashboard/MarketOverview.css new file mode 100644 index 0000000..f80c9ad --- /dev/null +++ b/src/pages/Dashboard/MarketOverview.css @@ -0,0 +1,5 @@ +.market-overview-fullwidth { + width: 100%; + max-width: 100%; + box-sizing: border-box; +} diff --git a/src/pages/Dashboard/MarketOverview.jsx b/src/pages/Dashboard/MarketOverview.jsx index 230e01b..563eba1 100644 --- a/src/pages/Dashboard/MarketOverview.jsx +++ b/src/pages/Dashboard/MarketOverview.jsx @@ -1,211 +1,307 @@ import React, { useState, useEffect, useContext } from "react"; import { useNavigate } from "react-router-dom"; -import { useTheme } from "../../context/ThemeContext"; -import { CoinContext } from "../../context/CoinContext"; +import { useTheme } from "../../context/useTheme"; +import { CoinContext } from "../../context/CoinContextInstance"; +import "./MarketOverview.css"; const MarketOverview = () => { - const { isDark } = useTheme(); - const { allCoin, currency } = useContext(CoinContext); - const navigate = useNavigate(); - const [topGainers, setTopGainers] = useState([]); - const [topLosers, setTopLosers] = useState([]); - const [trending, setTrending] = useState([]); + const { isDark } = useTheme(); + const { allCoin, currency } = useContext(CoinContext); + const navigate = useNavigate(); + const [topGainers, setTopGainers] = useState([]); + const [topLosers, setTopLosers] = useState([]); + const [trending, setTrending] = useState([]); - useEffect(() => { - if (allCoin && allCoin.length > 0) { - // Get top 5 gainers - const gainers = [...allCoin] - .sort((a, b) => b.price_change_percentage_24h - a.price_change_percentage_24h) - .slice(0, 5); - setTopGainers(gainers); + useEffect(() => { + if (allCoin && allCoin.length > 0) { + // Get top 5 gainers + const gainers = [...allCoin] + .sort( + (a, b) => + b.price_change_percentage_24h - a.price_change_percentage_24h, + ) + .slice(0, 5); + setTopGainers(gainers); - // Get top 5 losers - const losers = [...allCoin] - .sort((a, b) => a.price_change_percentage_24h - b.price_change_percentage_24h) - .slice(0, 5); - setTopLosers(losers); + // Get top 5 losers + const losers = [...allCoin] + .sort( + (a, b) => + a.price_change_percentage_24h - b.price_change_percentage_24h, + ) + .slice(0, 5); + setTopLosers(losers); - // Get top 10 by market cap - const top = allCoin.slice(0, 10); - setTrending(top); - } - }, [allCoin]); + // Get top 10 by market cap + const top = allCoin.slice(0, 10); + setTrending(top); + } + }, [allCoin]); - const formatPrice = (price) => { - if (currency.symbol === "₹") { - return `₹${price.toLocaleString()}`; - } else if (currency.symbol === "€") { - return `€${price.toLocaleString()}`; - } else { - return `$${price.toLocaleString()}`; - } - }; + const formatPrice = (price) => { + if (price == null) return "-"; + if (currency.symbol === "₹") { + return `₹${price.toLocaleString()}`; + } else if (currency.symbol === "€") { + return `€${price.toLocaleString()}`; + } else { + return `$${price.toLocaleString()}`; + } + }; - return ( - <> -
-

- - Market Overview - - - - -

-

- Real-time cryptocurrency market insights -

-
+ return ( +
+
+

+ + Market Overview + + + + +

+

+ Real-time cryptocurrency market insights +

+
-
- {/* Top Gainers */} -
-
-
- - - -
-

- Top Gainers 24h -

-
-
- {topGainers.map((coin, index) => ( -
navigate(`/coin/${coin.id}`)} - className={`flex items-center justify-between p-4 rounded-xl cursor-pointer transition-all duration-200 hover:scale-105 ${isDark - ? 'bg-[rgba(255,255,255,0.03)] hover:bg-[rgba(255,255,255,0.05)]' - : 'bg-gray-50 hover:bg-gray-100' - }`} - > -
- - #{index + 1} - - {coin.name} -
-

- {coin.name} -

-

- {formatPrice(coin.current_price)} -

-
-
- - +{coin.price_change_percentage_24h?.toFixed(2)}% - -
- ))} -
+
+ {/* Top Gainers */} +
+
+
+ + + +
+

+ Top Gainers 24h +

+
+
+ {topGainers.map((coin, index) => ( +
navigate(`/coin/${coin.id}`)} + className={`flex items-center justify-between p-4 rounded-xl cursor-pointer transition-all duration-200 hover:scale-105 ${ + isDark + ? "bg-[rgba(255,255,255,0.03)] hover:bg-[rgba(255,255,255,0.05)]" + : "bg-gray-50 hover:bg-gray-100" + }`} + > +
+ + #{index + 1} + + {coin.name} +
+

+ {coin.name} +

+

+ {formatPrice(coin.current_price)} +

+
+ + +{coin.price_change_percentage_24h?.toFixed(2)}% + +
+ ))} +
+
- {/* Top Losers */} -
-
-
- - - -
-

- Top Losers 24h -

-
-
- {topLosers.map((coin, index) => ( -
navigate(`/coin/${coin.id}`)} - className={`flex items-center justify-between p-4 rounded-xl cursor-pointer transition-all duration-200 hover:scale-105 ${isDark - ? 'bg-[rgba(255,255,255,0.03)] hover:bg-[rgba(255,255,255,0.05)]' - : 'bg-gray-50 hover:bg-gray-100' - }`} - > -
- - #{index + 1} - - {coin.name} -
-

- {coin.name} -

-

- {formatPrice(coin.current_price)} -

-
-
- - {coin.price_change_percentage_24h?.toFixed(2)}% - -
- ))} -
-
+ {/* Top Losers */} +
+
+
+ + +
- - {/* Trending Cryptocurrencies */} -
-
-
- - - -
-

- Top 10 by Market Cap -

+

+ Top Losers 24h +

+
+
+ {topLosers.map((coin, index) => ( +
navigate(`/coin/${coin.id}`)} + className={`flex items-center justify-between p-4 rounded-xl cursor-pointer transition-all duration-200 hover:scale-105 ${ + isDark + ? "bg-[rgba(255,255,255,0.03)] hover:bg-[rgba(255,255,255,0.05)]" + : "bg-gray-50 hover:bg-gray-100" + }`} + > +
+ + #{index + 1} + + {coin.name} +
+

+ {coin.name} +

+

+ {formatPrice(coin.current_price)} +

+
-
- {trending.map((coin) => ( -
navigate(`/coin/${coin.id}`)} - className={`p-4 rounded-xl cursor-pointer transition-all duration-200 hover:scale-105 ${isDark - ? 'bg-[rgba(255,255,255,0.02)] hover:bg-[rgba(255,255,255,0.03)]' - : 'bg-gray-50 hover:bg-gray-100' - }`} - > -
- {coin.name} -
-

- {coin.name} -

-

- {coin.symbol.toUpperCase()} -

-
- 0 ? "text-green-400" : "text-red-400"}> - {coin.price_change_percentage_24h?.toFixed(2)}% - -
-
-

- {formatPrice(coin.current_price)} -

-

- MCap: {currency.symbol}{(coin.market_cap / 1e9).toFixed(2)}B -

-
-
- ))} + + {coin.price_change_percentage_24h?.toFixed(2)}% + +
+ ))} +
+
+
+ + {/* Trending Cryptocurrencies */} +
+
+
+ + + +
+

+ Top 10 by Market Cap +

+
+
+ {trending.map((coin) => ( +
navigate(`/coin/${coin.id}`)} + className={`p-4 rounded-xl cursor-pointer transition-all duration-200 hover:scale-105 ${ + isDark + ? "bg-[rgba(255,255,255,0.02)] hover:bg-[rgba(255,255,255,0.03)]" + : "bg-gray-50 hover:bg-gray-100" + }`} + > +
+ {coin.name} +
+

+ {coin.name} +

+

+ {coin.symbol.toUpperCase()} +

+ 0 + ? "text-green-400" + : "text-red-400" + } + > + {coin.price_change_percentage_24h?.toFixed(2)}% + +
+
+

+ {formatPrice(coin.current_price)} +

+

+ MCap: {currency.symbol} + {coin.market_cap + ? (coin.market_cap / 1e9).toFixed(2) + : "0.00"} + B +

+
- - ); + ))} +
+
+
+ ); }; export default MarketOverview; diff --git a/src/pages/Dashboard/Profile.jsx b/src/pages/Dashboard/Profile.jsx new file mode 100644 index 0000000..c96cae8 --- /dev/null +++ b/src/pages/Dashboard/Profile.jsx @@ -0,0 +1,415 @@ +import React, { useState } from "react"; +import { useAuth } from "../../context/useAuth"; +import { useTheme } from "../../context/useTheme"; +import { notifySuccess, notifyError } from "../../utils/notify"; // Assuming this exists based on Login.jsx +import { + FiUser, + FiMail, + FiPhone, + FiCalendar, + FiMapPin, + FiActivity, + FiDollarSign, + FiSave, + FiShield, + FiCamera, +} from "react-icons/fi"; + +const Profile = () => { + const { currentUser, updateUserProfile } = useAuth(); + const { isDark } = useTheme(); + const [loading, setLoading] = useState(false); + const [imageFile, setImageFile] = useState(null); + const [imagePreview, setImagePreview] = useState(null); + const fileInputRef = React.useRef(null); + + const [formData, setFormData] = useState({ + fullName: "", + email: "", + gender: "", + dateOfBirth: "", + phoneNumber: "", + country: "", + userId: "", + preferredCurrency: "USD", + tradingExperience: "Beginner", + riskTolerance: "Medium", + }); + + React.useEffect(() => { + if (currentUser) { + setFormData((prev) => ({ + ...prev, + fullName: currentUser.fullName || currentUser.displayName || "", + email: currentUser.email || "", + userId: currentUser.uid || "", + phoneNumber: currentUser.phoneNumber || "", + photoURL: currentUser.photoURL, + gender: currentUser.gender || "", + dateOfBirth: currentUser.dateOfBirth || "", + country: currentUser.country || "", + preferredCurrency: currentUser.preferredCurrency || "USD", + tradingExperience: currentUser.tradingExperience || "Beginner", + riskTolerance: currentUser.riskTolerance || "Medium", + })); + if (currentUser.photoURL) { + setImagePreview(currentUser.photoURL); + } + } + }, [currentUser]); + + const handleChange = (e) => { + const { name, value } = e.target; + setFormData((prev) => ({ + ...prev, + [name]: value, + })); + }; + + const handleImageChange = (e) => { + const file = e.target.files[0]; + if (file) { + setImageFile(file); + const reader = new FileReader(); + reader.onloadend = () => { + setImagePreview(reader.result); + }; + reader.readAsDataURL(file); + } + }; + + const triggerFileInput = () => { + fileInputRef.current.click(); + }; + + const handleSubmit = async (e) => { + e.preventDefault(); + setLoading(true); + try { + // Exclude read-only fields from the update payload + const { ...updateData } = formData; + await updateUserProfile(currentUser.uid, updateData, imageFile); + notifySuccess("Profile updated successfully!"); + setImageFile(null); // Reset file input after successful upload + } catch (error) { + console.error("Error updating profile:", error); + notifyError( + error.message || "Failed to update profile. Please try again.", + ); + } finally { + setLoading(false); + } + }; + + const inputClasses = `w-full px-4 py-3 rounded-xl border outline-none transition-all duration-200 ${ + isDark + ? "bg-[#0f0f1f] border-[rgba(255,255,255,0.08)] text-white focus:border-[#00d9ff] focus:ring-1 focus:ring-[#00d9ff]" + : "bg-white border-gray-200 text-gray-900 focus:border-purple-500 focus:ring-1 focus:ring-purple-500" + }`; + + const labelClasses = `block mb-2 text-sm font-medium ${isDark ? "text-gray-400" : "text-gray-600"}`; + const sectionClasses = `p-6 rounded-2xl border ${isDark ? "bg-[#0a0a1a] border-[rgba(255,255,255,0.05)]" : "bg-white border-gray-100 shadow-sm"}`; + + return ( +
+
+
+

+ My Profile +

+

+ Manage your personal information and trading preferences +

+
+ +
+ +
+
+
+ {imagePreview ? ( + Profile + ) : ( +
+ {formData.fullName ? ( + formData.fullName.charAt(0).toUpperCase() + ) : ( + + )} +
+ )} +
+
+ +
+ +
+

+ Click to upload new profile picture +

+ {imageFile && ( +

+ New image selected - Click Save Changes to apply +

+ )} +
+ +
+ {/* Personal Information */} +
+
+
+ +
+

+ Personal Information +

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

+ Trading Profile +

+
+ +
+
+ +
+ + +
+
+ +
+ +
+ {["Beginner", "Intermediate", "Expert"].map((level) => ( + + ))} +
+
+ +
+ +
+
+ + + {formData.riskTolerance} Risk + +
+ { + const val = parseInt(e.target.value); + const tolerance = + val === 0 ? "Low" : val === 1 ? "Medium" : "High"; + setFormData((prev) => ({ + ...prev, + riskTolerance: tolerance, + })); + }} + className="w-full accent-[#00d9ff] h-2 bg-gray-700/30 rounded-lg appearance-none cursor-pointer" + /> +
+ Conservative + Moderate + Aggressive +
+
+
+
+
+
+
+ ); +}; + +export default Profile; diff --git a/src/pages/Feedback.css b/src/pages/Feedback.css new file mode 100644 index 0000000..e48fca9 --- /dev/null +++ b/src/pages/Feedback.css @@ -0,0 +1,82 @@ +.feedback-container { + min-height: 85vh; + display: flex; + justify-content: center; + align-items: center; + padding: 80px 20px; + background: linear-gradient(180deg, #0f0f19 0%, #140c2e 100%); +} + +.feedback-card { + width: 100%; + max-width: 650px; + padding: 40px; + border-radius: 20px; + background: rgba(30, 20, 60, 0.6); + backdrop-filter: blur(20px); + border: 1px solid rgba(139, 92, 246, 0.2); + box-shadow: 0 0 40px rgba(139, 92, 246, 0.15); +} + +.feedback-title { + font-size: 26px; + font-weight: 600; + margin-bottom: 30px; + color: #ffffff; + text-align: center; +} + +.feedback-form { + display: flex; + flex-direction: column; + gap: 18px; +} + +.feedback-form input, +.feedback-form textarea { + padding: 14px 16px; + border-radius: 12px; + border: 1px solid rgba(139, 92, 246, 0.3); + background: rgba(20, 15, 35, 0.7); + color: #ffffff; + font-size: 14px; + outline: none; + transition: 0.3s ease; +} + +.feedback-form input::placeholder, +.feedback-form textarea::placeholder { + color: rgba(255, 255, 255, 0.5); +} + +.feedback-form input:focus, +.feedback-form textarea:focus { + border-color: #8b5cf6; + box-shadow: 0 0 10px rgba(139, 92, 246, 0.5); +} + +.feedback-button { + margin-top: 10px; + padding: 14px; + border-radius: 12px; + border: none; + background: linear-gradient(90deg, #8b5cf6, #a855f7); + color: white; + font-weight: 500; + cursor: pointer; + transition: 0.3s ease; +} + +.feedback-button:hover { + transform: translateY(-2px); + box-shadow: 0 10px 20px rgba(139, 92, 246, 0.4); +} + +.feedback-success { + background: rgba(34, 197, 94, 0.15); + color: #22c55e; + padding: 10px 14px; + border-radius: 8px; + text-align: center; + font-size: 14px; +} diff --git a/src/pages/Feedback.jsx b/src/pages/Feedback.jsx new file mode 100644 index 0000000..75b820e --- /dev/null +++ b/src/pages/Feedback.jsx @@ -0,0 +1,80 @@ +import React, { useState } from "react"; +import "./Feedback.css"; // reuse existing theme styling + +const Feedback = () => { + const [formData, setFormData] = useState({ + name: "", + email: "", + message: "", + }); + + const [submitted, setSubmitted] = useState(false); + + const handleChange = (e) => { + setFormData({ + ...formData, + [e.target.name]: e.target.value, + }); + }; + + const handleSubmit = (e) => { + e.preventDefault(); + + if (!formData.message.trim()) return; + + setSubmitted(true); + + setFormData({ + name: "", + email: "", + message: "", + }); + }; + + return ( +
+
+

Share Your Feedback

+ + {submitted && ( +
+ ✅ Thank you! Your feedback has been submitted. +
+ )} + +
+ + + + +