lyra-web3-playground 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/deploy.yml +53 -0
- package/LICENSE +21 -0
- package/README.md +47 -0
- package/index.html +42 -0
- package/package.json +75 -0
- package/postcss.config.js +12 -0
- package/public/404.html +21 -0
- package/public/icon.svg +10 -0
- package/public/manifest.json +17 -0
- package/public/robots.txt +34 -0
- package/server.json +9 -0
- package/src/App.tsx +38 -0
- package/src/components/Accessibility/AccessibilityButton.tsx +266 -0
- package/src/components/Accessibility/AccessibilityPanel.tsx +792 -0
- package/src/components/Accessibility/AccessibleButton.tsx +251 -0
- package/src/components/Accessibility/AccessibleIcon.tsx +134 -0
- package/src/components/Accessibility/Announcer.tsx +108 -0
- package/src/components/Accessibility/CodeOutputCaption.tsx +307 -0
- package/src/components/Accessibility/ColorBlindFilters.tsx +103 -0
- package/src/components/Accessibility/DwellClick.tsx +165 -0
- package/src/components/Accessibility/HighContrast.tsx +199 -0
- package/src/components/Accessibility/KeyboardNavigation.tsx +260 -0
- package/src/components/Accessibility/LiveAnnouncer.tsx +82 -0
- package/src/components/Accessibility/ReadingGuide.tsx +72 -0
- package/src/components/Accessibility/ReducedMotion.tsx +148 -0
- package/src/components/Accessibility/SkipLink.tsx +21 -0
- package/src/components/Accessibility/SkipLinks.tsx +79 -0
- package/src/components/Accessibility/VisualFeedback.tsx +118 -0
- package/src/components/Accessibility/index.ts +68 -0
- package/src/components/Accessibility/useFeedback.ts +69 -0
- package/src/components/AuthModal.tsx +243 -0
- package/src/components/ConsentModal.tsx +205 -0
- package/src/components/Footer.tsx +94 -0
- package/src/components/LanguageSelector.tsx +60 -0
- package/src/components/NavBar.tsx +335 -0
- package/src/components/WalletConnect.tsx +274 -0
- package/src/hooks/useMarketData.ts +413 -0
- package/src/lib/supabase.ts +43 -0
- package/src/main.tsx +15 -0
- package/src/pages/MarketsPage.tsx +612 -0
- package/src/services/marketData.ts +476 -0
- package/src/stores/accessibilityStore.ts +405 -0
- package/src/stores/authStore.ts +209 -0
- package/src/stores/i18nStore.ts +397 -0
- package/src/stores/themeStore.ts +47 -0
- package/src/stores/walletStore.ts +30 -0
- package/src/styles/accessibility.css +582 -0
- package/src/styles/index.css +637 -0
- package/src/types/contracts.ts +34 -0
- package/src/types/index.ts +11 -0
- package/src/utils/chainConfigs.ts +467 -0
- package/src/utils/helpers.ts +116 -0
- package/src/vite-env.d.ts +40 -0
- package/tailwind.config.js +80 -0
- package/tsconfig.json +31 -0
- package/tsconfig.node.json +10 -0
- package/vite.config.ts +26 -0
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
name: Deploy to GitHub Pages
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
workflow_dispatch:
|
|
7
|
+
|
|
8
|
+
permissions:
|
|
9
|
+
contents: read
|
|
10
|
+
pages: write
|
|
11
|
+
id-token: write
|
|
12
|
+
|
|
13
|
+
concurrency:
|
|
14
|
+
group: "pages"
|
|
15
|
+
cancel-in-progress: true
|
|
16
|
+
|
|
17
|
+
jobs:
|
|
18
|
+
build:
|
|
19
|
+
runs-on: ubuntu-latest
|
|
20
|
+
steps:
|
|
21
|
+
- name: Checkout
|
|
22
|
+
uses: actions/checkout@v4
|
|
23
|
+
|
|
24
|
+
- name: Setup Node
|
|
25
|
+
uses: actions/setup-node@v4
|
|
26
|
+
with:
|
|
27
|
+
node-version: '20'
|
|
28
|
+
cache: 'npm'
|
|
29
|
+
|
|
30
|
+
- name: Install dependencies
|
|
31
|
+
run: npm ci
|
|
32
|
+
|
|
33
|
+
- name: Build
|
|
34
|
+
run: npm run build
|
|
35
|
+
|
|
36
|
+
- name: Setup Pages
|
|
37
|
+
uses: actions/configure-pages@v4
|
|
38
|
+
|
|
39
|
+
- name: Upload artifact
|
|
40
|
+
uses: actions/upload-pages-artifact@v3
|
|
41
|
+
with:
|
|
42
|
+
path: './dist'
|
|
43
|
+
|
|
44
|
+
deploy:
|
|
45
|
+
environment:
|
|
46
|
+
name: github-pages
|
|
47
|
+
url: ${{ steps.deployment.outputs.page_url }}
|
|
48
|
+
runs-on: ubuntu-latest
|
|
49
|
+
needs: build
|
|
50
|
+
steps:
|
|
51
|
+
- name: Deploy to GitHub Pages
|
|
52
|
+
id: deployment
|
|
53
|
+
uses: actions/deploy-pages@v4
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 nirholas
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# Crypto Market Data
|
|
2
|
+
|
|
3
|
+
Live cryptocurrency and DeFi market dashboard with data from CoinGecko and DeFiLlama.
|
|
4
|
+
|
|
5
|
+
[Live Crypto Market Data Dashboard](https://nirholas.github.io/crypto-market-data)
|
|
6
|
+
|
|
7
|
+
Cryptocurrency prices and market data. That's it! With a basic UI. I originally built this within [Lyra Web3 Playground](https://nirholas.github.io/crypto-market-data) and I thought it might be helpful for those looking for something very simple, a good learning experience to build + deploy, or maybe you're like me and just enjoy building and sharing simple yet useful creations. Live data, hooked up to CoinGecko and Defillama.
|
|
8
|
+
|
|
9
|
+
## Features
|
|
10
|
+
|
|
11
|
+
- 🪙 **Cryptocurrencies** - Top coins by market cap with 7-day sparklines
|
|
12
|
+
- 🏦 **DeFi Protocols** - Protocol TVL rankings from DeFiLlama
|
|
13
|
+
- 📈 **Yields** - DeFi yield farming opportunities
|
|
14
|
+
- ⛓️ **Chains** - Blockchain TVL comparison
|
|
15
|
+
- 🌙 **Dark Mode** - Toggle between light and dark themes
|
|
16
|
+
- 📱 **Responsive** - Works on desktop and mobile
|
|
17
|
+
|
|
18
|
+
## Quick Start
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
# Install dependencies
|
|
22
|
+
npm install
|
|
23
|
+
|
|
24
|
+
# Start development server
|
|
25
|
+
npm run dev
|
|
26
|
+
|
|
27
|
+
# Build for production
|
|
28
|
+
npm run build
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Deploy to GitHub Pages
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
npm run build
|
|
35
|
+
# Upload dist/ folder to GitHub Pages
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Data Sources
|
|
39
|
+
|
|
40
|
+
- [CoinGecko](https://www.coingecko.com) - Cryptocurrency prices and market data
|
|
41
|
+
- [DeFiLlama](https://defillama.com) - DeFi protocol TVL and yield data
|
|
42
|
+
|
|
43
|
+
## License
|
|
44
|
+
|
|
45
|
+
MIT © [nich](https://github.com/nirholas)
|
|
46
|
+
|
|
47
|
+
|
package/index.html
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<link rel="icon" type="image/svg+xml" href="icon.svg" />
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
|
|
7
|
+
<meta name="description" content="Crypto Market Data - Live cryptocurrency and DeFi market dashboard with data from CoinGecko and DeFiLlama." />
|
|
8
|
+
<meta name="keywords" content="crypto, cryptocurrency, bitcoin, ethereum, defi, market data, prices, coingecko, defillama" />
|
|
9
|
+
<meta name="theme-color" content="#6366f1" />
|
|
10
|
+
<meta name="author" content="nirholas" />
|
|
11
|
+
|
|
12
|
+
<!-- Open Graph / Facebook -->
|
|
13
|
+
<meta property="og:type" content="website" />
|
|
14
|
+
<meta property="og:url" content="https://nirholas.github.io/crypto-market-data/" />
|
|
15
|
+
<meta property="og:title" content="Crypto Market Data" />
|
|
16
|
+
<meta property="og:description" content="Live cryptocurrency and DeFi market dashboard." />
|
|
17
|
+
|
|
18
|
+
<!-- Twitter -->
|
|
19
|
+
<meta name="twitter:card" content="summary" />
|
|
20
|
+
<meta name="twitter:title" content="Crypto Market Data" />
|
|
21
|
+
<meta name="twitter:description" content="Live cryptocurrency and DeFi market dashboard." />
|
|
22
|
+
|
|
23
|
+
<title>Crypto Market Data</title>
|
|
24
|
+
<!-- GitHub Pages SPA redirect handler -->
|
|
25
|
+
<script type="text/javascript">
|
|
26
|
+
(function(l) {
|
|
27
|
+
if (l.search[1] === '/' ) {
|
|
28
|
+
var decoded = l.search.slice(1).split('&').map(function(s) {
|
|
29
|
+
return s.replace(/~and~/g, '&')
|
|
30
|
+
}).join('?');
|
|
31
|
+
window.history.replaceState(null, null,
|
|
32
|
+
l.pathname.slice(0, -1) + decoded + l.hash
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
}(window.location))
|
|
36
|
+
</script>
|
|
37
|
+
</head>
|
|
38
|
+
<body>
|
|
39
|
+
<div id="root"></div>
|
|
40
|
+
<script type="module" src="src/main.tsx"></script>
|
|
41
|
+
</body>
|
|
42
|
+
</html>
|
package/package.json
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "lyra-web3-playground",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Lyra Web3 Playground - Learn blockchain development with interactive examples. Compile and deploy Solidity in your browser.",
|
|
5
|
+
"author": {
|
|
6
|
+
"name": "nich",
|
|
7
|
+
"url": "https://github.com/nirholas"
|
|
8
|
+
},
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "https://github.com/nirholas/lyra-web3-playground.git"
|
|
12
|
+
},
|
|
13
|
+
"homepage": "https://lyra.works/",
|
|
14
|
+
"type": "module",
|
|
15
|
+
"scripts": {
|
|
16
|
+
"dev": "vite",
|
|
17
|
+
"build": "tsc && vite build",
|
|
18
|
+
"preview": "vite preview",
|
|
19
|
+
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 20",
|
|
20
|
+
"test": "vitest",
|
|
21
|
+
"test:ui": "vitest --ui",
|
|
22
|
+
"test:coverage": "vitest --coverage"
|
|
23
|
+
},
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"@icons-pack/react-simple-icons": "^10.0.0",
|
|
26
|
+
"@monaco-editor/react": "^4.6.0",
|
|
27
|
+
"@solana/web3.js": "^1.87.0",
|
|
28
|
+
"@supabase/supabase-js": "^2.39.0",
|
|
29
|
+
"clsx": "^2.0.0",
|
|
30
|
+
"ethers": "^6.9.0",
|
|
31
|
+
"lucide-react": "^0.294.0",
|
|
32
|
+
"react": "^18.2.0",
|
|
33
|
+
"react-dom": "^18.2.0",
|
|
34
|
+
"react-live": "^4.1.8",
|
|
35
|
+
"react-router-dom": "^6.21.0",
|
|
36
|
+
"tailwind-merge": "^2.2.0",
|
|
37
|
+
"viem": "^2.0.0",
|
|
38
|
+
"zustand": "^4.4.7"
|
|
39
|
+
},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"@testing-library/jest-dom": "^6.1.5",
|
|
42
|
+
"@testing-library/react": "^14.1.2",
|
|
43
|
+
"@types/react": "^18.2.43",
|
|
44
|
+
"@types/react-dom": "^18.2.17",
|
|
45
|
+
"@typescript-eslint/eslint-plugin": "^6.14.0",
|
|
46
|
+
"@typescript-eslint/parser": "^6.14.0",
|
|
47
|
+
"@vitejs/plugin-react": "^4.2.1",
|
|
48
|
+
"@vitest/ui": "^4.0.15",
|
|
49
|
+
"autoprefixer": "^10.4.16",
|
|
50
|
+
"eslint": "^8.55.0",
|
|
51
|
+
"eslint-plugin-react-hooks": "^4.6.0",
|
|
52
|
+
"eslint-plugin-react-refresh": "^0.4.5",
|
|
53
|
+
"jsdom": "^23.0.1",
|
|
54
|
+
"postcss": "^8.4.32",
|
|
55
|
+
"tailwindcss": "^3.3.6",
|
|
56
|
+
"typescript": "^5.2.2",
|
|
57
|
+
"vite": "^7.2.6",
|
|
58
|
+
"vitest": "^4.0.15"
|
|
59
|
+
},
|
|
60
|
+
"license": "MIT",
|
|
61
|
+
"keywords": [
|
|
62
|
+
"web3",
|
|
63
|
+
"blockchain",
|
|
64
|
+
"ethereum",
|
|
65
|
+
"solana",
|
|
66
|
+
"ai",
|
|
67
|
+
"machine-learning",
|
|
68
|
+
"dapp",
|
|
69
|
+
"smart-contracts",
|
|
70
|
+
"nft",
|
|
71
|
+
"defi",
|
|
72
|
+
"playground",
|
|
73
|
+
"education"
|
|
74
|
+
]
|
|
75
|
+
}
|
package/public/404.html
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<title>Crypto Market Data</title>
|
|
6
|
+
<script>
|
|
7
|
+
// GitHub Pages SPA redirect
|
|
8
|
+
var pathSegmentsToKeep = 1;
|
|
9
|
+
var l = window.location;
|
|
10
|
+
l.replace(
|
|
11
|
+
l.protocol + '//' + l.hostname + (l.port ? ':' + l.port : '') +
|
|
12
|
+
l.pathname.split('/').slice(0, 1 + pathSegmentsToKeep).join('/') + '/?/' +
|
|
13
|
+
l.pathname.slice(1).split('/').slice(pathSegmentsToKeep).join('/').replace(/&/g, '~and~') +
|
|
14
|
+
(l.search ? '&' + l.search.slice(1).replace(/&/g, '~and~') : '') +
|
|
15
|
+
l.hash
|
|
16
|
+
);
|
|
17
|
+
</script>
|
|
18
|
+
</head>
|
|
19
|
+
<body>
|
|
20
|
+
</body>
|
|
21
|
+
</html>
|
package/public/icon.svg
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
|
|
2
|
+
<defs>
|
|
3
|
+
<linearGradient id="grad" x1="0%" y1="0%" x2="100%" y2="100%">
|
|
4
|
+
<stop offset="0%" style="stop-color:#6366f1"/>
|
|
5
|
+
<stop offset="100%" style="stop-color:#8b5cf6"/>
|
|
6
|
+
</linearGradient>
|
|
7
|
+
</defs>
|
|
8
|
+
<circle cx="50" cy="50" r="45" fill="url(#grad)"/>
|
|
9
|
+
<text x="50" y="65" font-size="40" text-anchor="middle" fill="white" font-family="Arial, sans-serif" font-weight="bold">📊</text>
|
|
10
|
+
</svg>
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "Crypto Market Data",
|
|
3
|
+
"short_name": "Markets",
|
|
4
|
+
"description": "Live cryptocurrency and DeFi market dashboard",
|
|
5
|
+
"start_url": "/crypto-market-data/",
|
|
6
|
+
"display": "standalone",
|
|
7
|
+
"background_color": "#111827",
|
|
8
|
+
"theme_color": "#6366f1",
|
|
9
|
+
"icons": [
|
|
10
|
+
{
|
|
11
|
+
"src": "icon.svg",
|
|
12
|
+
"sizes": "any",
|
|
13
|
+
"type": "image/svg+xml",
|
|
14
|
+
"purpose": "any maskable"
|
|
15
|
+
}
|
|
16
|
+
]
|
|
17
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# Lyra Web3 Playground - robots.txt
|
|
2
|
+
# https://lyra.works
|
|
3
|
+
|
|
4
|
+
# Allow all crawlers
|
|
5
|
+
User-agent: *
|
|
6
|
+
Allow: /
|
|
7
|
+
|
|
8
|
+
# AI Crawlers - explicitly allowed
|
|
9
|
+
User-agent: GPTBot
|
|
10
|
+
Allow: /
|
|
11
|
+
|
|
12
|
+
User-agent: ChatGPT-User
|
|
13
|
+
Allow: /
|
|
14
|
+
|
|
15
|
+
User-agent: Claude-Web
|
|
16
|
+
Allow: /
|
|
17
|
+
|
|
18
|
+
User-agent: Anthropic-AI
|
|
19
|
+
Allow: /
|
|
20
|
+
|
|
21
|
+
User-agent: Google-Extended
|
|
22
|
+
Allow: /
|
|
23
|
+
|
|
24
|
+
User-agent: Bingbot
|
|
25
|
+
Allow: /
|
|
26
|
+
|
|
27
|
+
User-agent: Googlebot
|
|
28
|
+
Allow: /
|
|
29
|
+
|
|
30
|
+
# Sitemap location
|
|
31
|
+
Sitemap: https://lyra.works/sitemap.xml
|
|
32
|
+
|
|
33
|
+
# Crawl-delay for politeness (optional)
|
|
34
|
+
Crawl-delay: 1
|
package/server.json
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
|
|
3
|
+
"name": "io.github.nirholas/crypto-market-data",
|
|
4
|
+
"version": "1.0.0",
|
|
5
|
+
"title": "Crypto Market Data",
|
|
6
|
+
"description": "Real-time crypto prices OHLCV orderbook - Bitcoin Ethereum 10000+ tokens",
|
|
7
|
+
"keywords": ["crypto", "market-data", "prices", "ohlcv", "bitcoin", "ethereum"],
|
|
8
|
+
"repository": {"url": "https://github.com/nirholas/crypto-market-data", "source": "github"}
|
|
9
|
+
}
|
package/src/App.tsx
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Crypto Market Data App
|
|
3
|
+
* https://github.com/nirholas/crypto-market-data
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { useEffect } from 'react';
|
|
7
|
+
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
|
|
8
|
+
import { useThemeStore } from './stores/themeStore';
|
|
9
|
+
import MarketsPage from './pages/MarketsPage';
|
|
10
|
+
import Footer from './components/Footer';
|
|
11
|
+
|
|
12
|
+
const basename = import.meta.env.BASE_URL;
|
|
13
|
+
|
|
14
|
+
function App() {
|
|
15
|
+
const { mode } = useThemeStore();
|
|
16
|
+
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
document.documentElement.classList.toggle('dark', mode === 'dark');
|
|
19
|
+
}, [mode]);
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<Router basename={basename}>
|
|
23
|
+
<div className={`min-h-screen ${mode === 'dark' ? 'dark' : ''}`}>
|
|
24
|
+
<div className="bg-gray-50 dark:bg-gray-900 text-gray-900 dark:text-gray-100 min-h-screen">
|
|
25
|
+
<main className="pb-20 md:pb-0">
|
|
26
|
+
<Routes>
|
|
27
|
+
<Route path="/" element={<MarketsPage />} />
|
|
28
|
+
<Route path="/markets" element={<MarketsPage />} />
|
|
29
|
+
</Routes>
|
|
30
|
+
</main>
|
|
31
|
+
<Footer />
|
|
32
|
+
</div>
|
|
33
|
+
</div>
|
|
34
|
+
</Router>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export default App;
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ✨ built by nich
|
|
3
|
+
* 🌐 GitHub: github.com/nirholas
|
|
4
|
+
* ♿ Floating Accessibility Button - Always accessible, never intrusive
|
|
5
|
+
* 💫 One-click access to inclusive features
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { useState, useEffect, useCallback } from 'react';
|
|
9
|
+
import { Accessibility, X, Settings, Volume2, VolumeX, Moon, Sun, Minus, Plus, Eye } from 'lucide-react';
|
|
10
|
+
import { useAccessibilityStore } from '@/stores/accessibilityStore';
|
|
11
|
+
import AccessibilityPanel from './AccessibilityPanel';
|
|
12
|
+
|
|
13
|
+
export default function AccessibilityButton() {
|
|
14
|
+
const [isPanelOpen, setIsPanelOpen] = useState(false);
|
|
15
|
+
const [isQuickMenuOpen, setIsQuickMenuOpen] = useState(false);
|
|
16
|
+
const [position, setPosition] = useState({ x: 20, y: window.innerHeight - 80 });
|
|
17
|
+
const [isDragging, setIsDragging] = useState(false);
|
|
18
|
+
const [dragOffset, setDragOffset] = useState({ x: 0, y: 0 });
|
|
19
|
+
|
|
20
|
+
const { settings, updateSetting, speak } = useAccessibilityStore();
|
|
21
|
+
|
|
22
|
+
// Handle dragging for repositioning
|
|
23
|
+
const handleMouseDown = (e: React.MouseEvent) => {
|
|
24
|
+
if (e.button === 0) {
|
|
25
|
+
setIsDragging(true);
|
|
26
|
+
setDragOffset({
|
|
27
|
+
x: e.clientX - position.x,
|
|
28
|
+
y: e.clientY - position.y
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const handleMouseMove = useCallback((e: MouseEvent) => {
|
|
34
|
+
if (isDragging) {
|
|
35
|
+
const newX = Math.max(0, Math.min(window.innerWidth - 60, e.clientX - dragOffset.x));
|
|
36
|
+
const newY = Math.max(0, Math.min(window.innerHeight - 60, e.clientY - dragOffset.y));
|
|
37
|
+
setPosition({ x: newX, y: newY });
|
|
38
|
+
}
|
|
39
|
+
}, [isDragging, dragOffset]);
|
|
40
|
+
|
|
41
|
+
const handleMouseUp = useCallback(() => {
|
|
42
|
+
setIsDragging(false);
|
|
43
|
+
}, []);
|
|
44
|
+
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
if (isDragging) {
|
|
47
|
+
window.addEventListener('mousemove', handleMouseMove);
|
|
48
|
+
window.addEventListener('mouseup', handleMouseUp);
|
|
49
|
+
return () => {
|
|
50
|
+
window.removeEventListener('mousemove', handleMouseMove);
|
|
51
|
+
window.removeEventListener('mouseup', handleMouseUp);
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
}, [isDragging, handleMouseMove, handleMouseUp]);
|
|
55
|
+
|
|
56
|
+
// Keyboard shortcut: Alt + A to open accessibility panel
|
|
57
|
+
useEffect(() => {
|
|
58
|
+
const handleKeyDown = (e: KeyboardEvent) => {
|
|
59
|
+
if (e.altKey && e.key === 'a') {
|
|
60
|
+
e.preventDefault();
|
|
61
|
+
setIsPanelOpen(prev => !prev);
|
|
62
|
+
speak(isPanelOpen ? 'Closing accessibility panel' : 'Opening accessibility panel');
|
|
63
|
+
}
|
|
64
|
+
if (e.key === 'Escape' && isPanelOpen) {
|
|
65
|
+
setIsPanelOpen(false);
|
|
66
|
+
speak('Accessibility panel closed');
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
window.addEventListener('keydown', handleKeyDown);
|
|
71
|
+
return () => window.removeEventListener('keydown', handleKeyDown);
|
|
72
|
+
}, [isPanelOpen, speak]);
|
|
73
|
+
|
|
74
|
+
const toggleDarkMode = () => {
|
|
75
|
+
document.documentElement.classList.toggle('dark');
|
|
76
|
+
speak(document.documentElement.classList.contains('dark') ? 'Dark mode enabled' : 'Light mode enabled');
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const increaseFontSize = () => {
|
|
80
|
+
const sizes = ['normal', 'large', 'x-large', 'xx-large'] as const;
|
|
81
|
+
const currentIndex = sizes.indexOf(settings.fontSize);
|
|
82
|
+
if (currentIndex < sizes.length - 1) {
|
|
83
|
+
updateSetting('fontSize', sizes[currentIndex + 1]);
|
|
84
|
+
speak(`Font size increased to ${sizes[currentIndex + 1]}`);
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const decreaseFontSize = () => {
|
|
89
|
+
const sizes = ['normal', 'large', 'x-large', 'xx-large'] as const;
|
|
90
|
+
const currentIndex = sizes.indexOf(settings.fontSize);
|
|
91
|
+
if (currentIndex > 0) {
|
|
92
|
+
updateSetting('fontSize', sizes[currentIndex - 1]);
|
|
93
|
+
speak(`Font size decreased to ${sizes[currentIndex - 1]}`);
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const toggleHighContrast = () => {
|
|
98
|
+
updateSetting('highContrast', !settings.highContrast);
|
|
99
|
+
speak(settings.highContrast ? 'High contrast disabled' : 'High contrast enabled');
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const toggleTextToSpeech = () => {
|
|
103
|
+
updateSetting('textToSpeech', !settings.textToSpeech);
|
|
104
|
+
if (!settings.textToSpeech) {
|
|
105
|
+
setTimeout(() => speak('Text to speech enabled'), 100);
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
return (
|
|
110
|
+
<>
|
|
111
|
+
{/* Main Floating Button */}
|
|
112
|
+
<button
|
|
113
|
+
onMouseDown={handleMouseDown}
|
|
114
|
+
onClick={() => {
|
|
115
|
+
if (!isDragging) {
|
|
116
|
+
setIsQuickMenuOpen(prev => !prev);
|
|
117
|
+
}
|
|
118
|
+
}}
|
|
119
|
+
className="fixed z-50 w-14 h-14 rounded-full bg-gradient-to-br from-primary-600 to-secondary-600 text-white shadow-lg hover:shadow-xl transition-shadow flex items-center justify-center cursor-move group"
|
|
120
|
+
style={{ left: position.x, top: position.y }}
|
|
121
|
+
aria-label="Open accessibility quick menu"
|
|
122
|
+
aria-expanded={isQuickMenuOpen}
|
|
123
|
+
aria-haspopup="true"
|
|
124
|
+
title="Accessibility options (Alt + A for full panel)"
|
|
125
|
+
>
|
|
126
|
+
<Accessibility className="w-7 h-7 group-hover:scale-110 transition-transform" aria-hidden="true" />
|
|
127
|
+
</button>
|
|
128
|
+
|
|
129
|
+
{/* Quick Menu Popup */}
|
|
130
|
+
{isQuickMenuOpen && (
|
|
131
|
+
<div
|
|
132
|
+
className="fixed z-50 bg-white dark:bg-gray-900 rounded-2xl shadow-2xl p-4 border border-gray-200 dark:border-gray-700 min-w-[280px]"
|
|
133
|
+
style={{
|
|
134
|
+
left: Math.min(position.x, window.innerWidth - 300),
|
|
135
|
+
top: Math.max(20, position.y - 280)
|
|
136
|
+
}}
|
|
137
|
+
role="menu"
|
|
138
|
+
aria-label="Accessibility quick menu"
|
|
139
|
+
>
|
|
140
|
+
<div className="flex items-center justify-between mb-4">
|
|
141
|
+
<h3 className="font-bold text-lg flex items-center gap-2">
|
|
142
|
+
<Accessibility className="w-5 h-5 text-primary-600" />
|
|
143
|
+
Quick Access
|
|
144
|
+
</h3>
|
|
145
|
+
<button
|
|
146
|
+
onClick={() => setIsQuickMenuOpen(false)}
|
|
147
|
+
className="p-1 hover:bg-gray-100 dark:hover:bg-gray-800 rounded"
|
|
148
|
+
aria-label="Close quick menu"
|
|
149
|
+
>
|
|
150
|
+
<X className="w-5 h-5" />
|
|
151
|
+
</button>
|
|
152
|
+
</div>
|
|
153
|
+
|
|
154
|
+
<div className="space-y-3">
|
|
155
|
+
{/* Font Size Controls */}
|
|
156
|
+
<div className="flex items-center justify-between p-2 bg-gray-50 dark:bg-gray-800 rounded-lg">
|
|
157
|
+
<span className="text-sm font-medium">Text Size</span>
|
|
158
|
+
<div className="flex items-center gap-2">
|
|
159
|
+
<button
|
|
160
|
+
onClick={decreaseFontSize}
|
|
161
|
+
className="p-2 hover:bg-gray-200 dark:hover:bg-gray-700 rounded-lg transition"
|
|
162
|
+
aria-label="Decrease font size"
|
|
163
|
+
disabled={settings.fontSize === 'normal'}
|
|
164
|
+
>
|
|
165
|
+
<Minus className="w-4 h-4" />
|
|
166
|
+
</button>
|
|
167
|
+
<span className="text-sm min-w-[60px] text-center capitalize">{settings.fontSize}</span>
|
|
168
|
+
<button
|
|
169
|
+
onClick={increaseFontSize}
|
|
170
|
+
className="p-2 hover:bg-gray-200 dark:hover:bg-gray-700 rounded-lg transition"
|
|
171
|
+
aria-label="Increase font size"
|
|
172
|
+
disabled={settings.fontSize === 'xx-large'}
|
|
173
|
+
>
|
|
174
|
+
<Plus className="w-4 h-4" />
|
|
175
|
+
</button>
|
|
176
|
+
</div>
|
|
177
|
+
</div>
|
|
178
|
+
|
|
179
|
+
{/* Quick Toggles */}
|
|
180
|
+
<button
|
|
181
|
+
onClick={toggleDarkMode}
|
|
182
|
+
className="w-full flex items-center justify-between p-3 hover:bg-gray-50 dark:hover:bg-gray-800 rounded-lg transition"
|
|
183
|
+
role="menuitem"
|
|
184
|
+
>
|
|
185
|
+
<span className="flex items-center gap-3">
|
|
186
|
+
{document.documentElement.classList.contains('dark') ? (
|
|
187
|
+
<Sun className="w-5 h-5 text-yellow-500" />
|
|
188
|
+
) : (
|
|
189
|
+
<Moon className="w-5 h-5 text-gray-600" />
|
|
190
|
+
)}
|
|
191
|
+
<span>Dark Mode</span>
|
|
192
|
+
</span>
|
|
193
|
+
<span className={`text-xs px-2 py-1 rounded ${
|
|
194
|
+
document.documentElement.classList.contains('dark')
|
|
195
|
+
? 'bg-green-100 text-green-700 dark:bg-green-900 dark:text-green-300'
|
|
196
|
+
: 'bg-gray-100 dark:bg-gray-700'
|
|
197
|
+
}`}>
|
|
198
|
+
{document.documentElement.classList.contains('dark') ? 'On' : 'Off'}
|
|
199
|
+
</span>
|
|
200
|
+
</button>
|
|
201
|
+
|
|
202
|
+
<button
|
|
203
|
+
onClick={toggleHighContrast}
|
|
204
|
+
className="w-full flex items-center justify-between p-3 hover:bg-gray-50 dark:hover:bg-gray-800 rounded-lg transition"
|
|
205
|
+
role="menuitem"
|
|
206
|
+
>
|
|
207
|
+
<span className="flex items-center gap-3">
|
|
208
|
+
<Eye className="w-5 h-5 text-blue-500" />
|
|
209
|
+
<span>High Contrast</span>
|
|
210
|
+
</span>
|
|
211
|
+
<span className={`text-xs px-2 py-1 rounded ${
|
|
212
|
+
settings.highContrast
|
|
213
|
+
? 'bg-green-100 text-green-700 dark:bg-green-900 dark:text-green-300'
|
|
214
|
+
: 'bg-gray-100 dark:bg-gray-700'
|
|
215
|
+
}`}>
|
|
216
|
+
{settings.highContrast ? 'On' : 'Off'}
|
|
217
|
+
</span>
|
|
218
|
+
</button>
|
|
219
|
+
|
|
220
|
+
<button
|
|
221
|
+
onClick={toggleTextToSpeech}
|
|
222
|
+
className="w-full flex items-center justify-between p-3 hover:bg-gray-50 dark:hover:bg-gray-800 rounded-lg transition"
|
|
223
|
+
role="menuitem"
|
|
224
|
+
>
|
|
225
|
+
<span className="flex items-center gap-3">
|
|
226
|
+
{settings.textToSpeech ? (
|
|
227
|
+
<Volume2 className="w-5 h-5 text-green-500" />
|
|
228
|
+
) : (
|
|
229
|
+
<VolumeX className="w-5 h-5 text-gray-500" />
|
|
230
|
+
)}
|
|
231
|
+
<span>Text to Speech</span>
|
|
232
|
+
</span>
|
|
233
|
+
<span className={`text-xs px-2 py-1 rounded ${
|
|
234
|
+
settings.textToSpeech
|
|
235
|
+
? 'bg-green-100 text-green-700 dark:bg-green-900 dark:text-green-300'
|
|
236
|
+
: 'bg-gray-100 dark:bg-gray-700'
|
|
237
|
+
}`}>
|
|
238
|
+
{settings.textToSpeech ? 'On' : 'Off'}
|
|
239
|
+
</span>
|
|
240
|
+
</button>
|
|
241
|
+
|
|
242
|
+
{/* Full Settings Button */}
|
|
243
|
+
<button
|
|
244
|
+
onClick={() => {
|
|
245
|
+
setIsQuickMenuOpen(false);
|
|
246
|
+
setIsPanelOpen(true);
|
|
247
|
+
}}
|
|
248
|
+
className="w-full flex items-center justify-center gap-2 p-3 bg-gradient-to-r from-primary-600 to-secondary-600 text-white rounded-lg hover:opacity-90 transition"
|
|
249
|
+
role="menuitem"
|
|
250
|
+
>
|
|
251
|
+
<Settings className="w-5 h-5" />
|
|
252
|
+
<span>All Accessibility Settings</span>
|
|
253
|
+
</button>
|
|
254
|
+
|
|
255
|
+
<p className="text-xs text-gray-500 text-center mt-2">
|
|
256
|
+
Keyboard shortcut: <kbd className="px-1 py-0.5 bg-gray-200 dark:bg-gray-700 rounded">Alt + A</kbd>
|
|
257
|
+
</p>
|
|
258
|
+
</div>
|
|
259
|
+
</div>
|
|
260
|
+
)}
|
|
261
|
+
|
|
262
|
+
{/* Full Accessibility Panel */}
|
|
263
|
+
<AccessibilityPanel isOpen={isPanelOpen} onClose={() => setIsPanelOpen(false)} />
|
|
264
|
+
</>
|
|
265
|
+
);
|
|
266
|
+
}
|