Created
April 12, 2026 10:16
-
-
Save jayva1/fa34bd8e5998066366cc1fcf80af0a83 to your computer and use it in GitHub Desktop.
DMND Competency Test - BTC Dashboard by Jay Valiya
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| .counter { | |
| font-size: 16px; | |
| padding: 5px 10px; | |
| border-radius: 5px; | |
| color: var(--accent); | |
| background: var(--accent-bg); | |
| border: 2px solid transparent; | |
| transition: border-color 0.3s; | |
| margin-bottom: 24px; | |
| &:hover { | |
| border-color: var(--accent-border); | |
| } | |
| &:focus-visible { | |
| outline: 2px solid var(--accent); | |
| outline-offset: 2px; | |
| } | |
| } | |
| .hero { | |
| position: relative; | |
| .base, | |
| .framework, | |
| .vite { | |
| inset-inline: 0; | |
| margin: 0 auto; | |
| } | |
| .base { | |
| width: 170px; | |
| position: relative; | |
| z-index: 0; | |
| } | |
| .framework, | |
| .vite { | |
| position: absolute; | |
| } | |
| .framework { | |
| z-index: 1; | |
| top: 34px; | |
| height: 28px; | |
| transform: perspective(2000px) rotateZ(300deg) rotateX(44deg) rotateY(39deg) | |
| scale(1.4); | |
| } | |
| .vite { | |
| z-index: 0; | |
| top: 107px; | |
| height: 26px; | |
| width: auto; | |
| transform: perspective(2000px) rotateZ(300deg) rotateX(40deg) rotateY(39deg) | |
| scale(0.8); | |
| } | |
| } | |
| #center { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 25px; | |
| place-content: center; | |
| place-items: center; | |
| flex-grow: 1; | |
| @media (max-width: 1024px) { | |
| padding: 32px 20px 24px; | |
| gap: 18px; | |
| } | |
| } | |
| #next-steps { | |
| display: flex; | |
| border-top: 1px solid var(--border); | |
| text-align: left; | |
| & > div { | |
| flex: 1 1 0; | |
| padding: 32px; | |
| @media (max-width: 1024px) { | |
| padding: 24px 20px; | |
| } | |
| } | |
| .icon { | |
| margin-bottom: 16px; | |
| width: 22px; | |
| height: 22px; | |
| } | |
| @media (max-width: 1024px) { | |
| flex-direction: column; | |
| text-align: center; | |
| } | |
| } | |
| #docs { | |
| border-right: 1px solid var(--border); | |
| @media (max-width: 1024px) { | |
| border-right: none; | |
| border-bottom: 1px solid var(--border); | |
| } | |
| } | |
| #next-steps ul { | |
| list-style: none; | |
| padding: 0; | |
| display: flex; | |
| gap: 8px; | |
| margin: 32px 0 0; | |
| .logo { | |
| height: 18px; | |
| } | |
| a { | |
| color: var(--text-h); | |
| font-size: 16px; | |
| border-radius: 6px; | |
| background: var(--social-bg); | |
| display: flex; | |
| padding: 6px 12px; | |
| align-items: center; | |
| gap: 8px; | |
| text-decoration: none; | |
| transition: box-shadow 0.3s; | |
| &:hover { | |
| box-shadow: var(--shadow); | |
| } | |
| .button-icon { | |
| height: 18px; | |
| width: 18px; | |
| } | |
| } | |
| @media (max-width: 1024px) { | |
| margin-top: 20px; | |
| flex-wrap: wrap; | |
| justify-content: center; | |
| li { | |
| flex: 1 1 calc(50% - 8px); | |
| } | |
| a { | |
| width: 100%; | |
| justify-content: center; | |
| box-sizing: border-box; | |
| } | |
| } | |
| } | |
| #spacer { | |
| height: 88px; | |
| border-top: 1px solid var(--border); | |
| @media (max-width: 1024px) { | |
| height: 48px; | |
| } | |
| } | |
| .ticks { | |
| position: relative; | |
| width: 100%; | |
| &::before, | |
| &::after { | |
| content: ''; | |
| position: absolute; | |
| top: -4.5px; | |
| border: 5px solid transparent; | |
| } | |
| &::before { | |
| left: 0; | |
| border-left-color: var(--border); | |
| } | |
| &::after { | |
| right: 0; | |
| border-right-color: var(--border); | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import { BrowserRouter } from 'react-router-dom'; | |
| import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; | |
| import { ThemeProvider, useTheme } from './context/ThemeContext'; | |
| import BTCDashboard from './pages/BTCDashboard'; | |
| import './index.css'; | |
| const queryClient = new QueryClient({ | |
| defaultOptions: { | |
| queries: { | |
| retry: 2, | |
| refetchOnWindowFocus: false, | |
| }, | |
| }, | |
| }); | |
| function ThemeToggle() { | |
| const { theme, toggleTheme } = useTheme(); | |
| return ( | |
| <button | |
| onClick={toggleTheme} | |
| className="p-2 rounded-lg bg-gray-200 dark:bg-gray-700 hover:bg-gray-300 dark:hover:bg-gray-600 transition-colors" | |
| aria-label="Toggle theme" | |
| > | |
| {theme === 'dark' ? ( | |
| <svg className="w-5 h-5 text-yellow-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | |
| <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z" /> | |
| </svg> | |
| ) : ( | |
| <svg className="w-5 h-5 text-gray-700" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | |
| <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z" /> | |
| </svg> | |
| )} | |
| </button> | |
| ); | |
| } | |
| function AppContent() { | |
| const { theme } = useTheme(); | |
| return ( | |
| <div className={`min-h-screen transition-colors duration-300 ${theme === 'dark' ? 'bg-gray-900' : 'bg-gray-50'}`}> | |
| {/* Header */} | |
| <header className={`border-b ${theme === 'dark' ? 'bg-gray-800 border-gray-700' : 'bg-white border-gray-200'}`}> | |
| <div className="max-w-6xl mx-auto px-4 py-4 flex items-center justify-between"> | |
| <div className="flex items-center gap-3"> | |
| <div className="w-8 h-8 rounded-lg bg-bitcoin flex items-center justify-center"> | |
| <span className="text-white font-bold text-sm">₿</span> | |
| </div> | |
| <h1 className={`text-xl font-bold ${theme === 'dark' ? 'text-white' : 'text-gray-900'}`}> | |
| Bitcoin Dashboard | |
| </h1> | |
| </div> | |
| <ThemeToggle /> | |
| </div> | |
| </header> | |
| {/* Main Content */} | |
| <main className="max-w-6xl mx-auto px-4 py-8"> | |
| <BTCDashboard /> | |
| </main> | |
| </div> | |
| ); | |
| } | |
| function App() { | |
| return ( | |
| <QueryClientProvider client={queryClient}> | |
| <ThemeProvider> | |
| <BrowserRouter> | |
| <AppContent /> | |
| </BrowserRouter> | |
| </ThemeProvider> | |
| </QueryClientProvider> | |
| ); | |
| } | |
| export default App; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import { useQuery } from '@tanstack/react-query'; | |
| import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts'; | |
| interface PricePoint { | |
| timestamp: number; | |
| price: number; | |
| } | |
| interface BTCData { | |
| prices: PricePoint[]; | |
| currentPrice: number; | |
| priceChange24h: number; | |
| lastUpdated: Date; | |
| } | |
| async function fetchBTCPrice(): Promise<BTCData> { | |
| // CoinGecko free API - get Bitcoin price with 24h history | |
| const response = await fetch( | |
| 'https://api.coingecko.com/api/v3/coins/bitcoin/market_chart?vs_currency=usd&days=1&interval=hourly' | |
| ); | |
| if (!response.ok) throw new Error('Failed to fetch BTC price'); | |
| const data = await response.json(); | |
| const prices: PricePoint[] = data.prices.map(([timestamp, price]: [number, number]) => ({ | |
| timestamp, | |
| price, | |
| })); | |
| const currentPrice = prices[prices.length - 1]?.price ?? 0; | |
| const priceChange24h = ((currentPrice - prices[0]?.price) / prices[0]?.price) * 100; | |
| return { | |
| prices, | |
| currentPrice, | |
| priceChange24h, | |
| lastUpdated: new Date(), | |
| }; | |
| } | |
| export default function BTCDashboard() { | |
| const { data, isLoading, isError, refetch } = useQuery<BTCData>({ | |
| queryKey: ['btcPrice'], | |
| queryFn: fetchBTCPrice, | |
| refetchInterval: 60000, // Poll every 60 seconds | |
| staleTime: 30000, | |
| }); | |
| if (isLoading) { | |
| return ( | |
| <div className="flex items-center justify-center h-64"> | |
| <div className="animate-pulse text-gray-400">Loading Bitcoin data...</div> | |
| </div> | |
| ); | |
| } | |
| if (isError) { | |
| return ( | |
| <div className="flex flex-col items-center justify-center h-64 gap-4"> | |
| <div className="text-red-400">Failed to fetch BTC price</div> | |
| <button | |
| onClick={() => refetch()} | |
| className="px-4 py-2 bg-bitcoin text-white rounded-lg hover:bg-bitcoin-dark transition-colors" | |
| > | |
| Retry | |
| </button> | |
| </div> | |
| ); | |
| } | |
| const formatPrice = (price: number) => | |
| new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(price); | |
| const formatTime = (timestamp: number) => | |
| new Date(timestamp).toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' }); | |
| const formatDate = (timestamp: number) => | |
| new Date(timestamp).toLocaleDateString('en-US', { month: 'short', day: 'numeric' }); | |
| return ( | |
| <div className="space-y-6"> | |
| {/* Stats Cards */} | |
| <div className="grid grid-cols-1 md:grid-cols-3 gap-4"> | |
| <div className="bg-white dark:bg-gray-800 rounded-xl p-6 shadow-sm border border-gray-200 dark:border-gray-700"> | |
| <div className="text-sm text-gray-500 dark:text-gray-400 mb-1">Bitcoin Price</div> | |
| <div className="text-2xl font-bold text-gray-900 dark:text-white"> | |
| {formatPrice(data?.currentPrice ?? 0)} | |
| </div> | |
| <div className="text-xs text-gray-400 mt-1">Live • Updates every 60s</div> | |
| </div> | |
| <div className="bg-white dark:bg-gray-800 rounded-xl p-6 shadow-sm border border-gray-200 dark:border-gray-700"> | |
| <div className="text-sm text-gray-500 dark:text-gray-400 mb-1">24h Change</div> | |
| <div className={`text-2xl font-bold ${(data?.priceChange24h ?? 0) >= 0 ? 'text-green-500' : 'text-red-500'}`}> | |
| {(data?.priceChange24h ?? 0) >= 0 ? '+' : ''}{data?.priceChange24h?.toFixed(2)}% | |
| </div> | |
| <div className="text-xs text-gray-400 mt-1">Last 24 hours</div> | |
| </div> | |
| <div className="bg-white dark:bg-gray-800 rounded-xl p-6 shadow-sm border border-gray-200 dark:border-gray-700"> | |
| <div className="text-sm text-gray-500 dark:text-gray-400 mb-1">Last Updated</div> | |
| <div className="text-lg font-semibold text-gray-900 dark:text-white"> | |
| {data?.lastUpdated?.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', second: '2-digit' })} | |
| </div> | |
| <div className="text-xs text-gray-400 mt-1">{data?.lastUpdated?.toLocaleDateString('en-US', { month: 'short', day: 'numeric' })}</div> | |
| </div> | |
| </div> | |
| {/* Price Chart */} | |
| <div className="bg-white dark:bg-gray-800 rounded-xl p-6 shadow-sm border border-gray-200 dark:border-gray-700"> | |
| <div className="flex items-center justify-between mb-6"> | |
| <h2 className="text-lg font-semibold text-gray-900 dark:text-white">BTC/USD Price Chart</h2> | |
| <div className="flex items-center gap-2"> | |
| <span className="w-2 h-2 rounded-full bg-bitcoin animate-pulse"></span> | |
| <span className="text-xs text-gray-500 dark:text-gray-400">Live</span> | |
| </div> | |
| </div> | |
| <div className="h-72"> | |
| <ResponsiveContainer width="100%" height="100%"> | |
| <LineChart data={data?.prices} margin={{ top: 5, right: 20, bottom: 5, left: 0 }}> | |
| <CartesianGrid strokeDasharray="3 3" stroke="#374151" opacity={0.3} /> | |
| <XAxis | |
| dataKey="timestamp" | |
| tickFormatter={formatTime} | |
| stroke="#9ca3af" | |
| fontSize={12} | |
| tick={{ fill: '#9ca3af' }} | |
| /> | |
| <YAxis | |
| domain={['dataMin - 500', 'dataMax + 500']} | |
| tickFormatter={(v) => `$${(v/1000).toFixed(1)}k`} | |
| stroke="#9ca3af" | |
| fontSize={12} | |
| tick={{ fill: '#9ca3af' }} | |
| /> | |
| <Tooltip | |
| contentStyle={{ | |
| backgroundColor: 'rgba(31, 41, 55, 0.95)', | |
| border: 'none', | |
| borderRadius: '8px', | |
| color: '#fff' | |
| }} | |
| labelFormatter={(label) => formatDate(label as number)} | |
| formatter={(value) => [formatPrice(value as number), 'Price']} | |
| /> | |
| <Line | |
| type="monotone" | |
| dataKey="price" | |
| stroke="#f7931a" | |
| strokeWidth={2} | |
| dot={false} | |
| activeDot={{ r: 6, fill: '#f7931a' }} | |
| /> | |
| </LineChart> | |
| </ResponsiveContainer> | |
| </div> | |
| </div> | |
| {/* Footer Info */} | |
| <div className="text-center text-xs text-gray-400"> | |
| Data provided by CoinGecko API • Auto-refreshes every 60 seconds | |
| </div> | |
| </div> | |
| ); | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import js from '@eslint/js' | |
| import globals from 'globals' | |
| import reactHooks from 'eslint-plugin-react-hooks' | |
| import reactRefresh from 'eslint-plugin-react-refresh' | |
| import tseslint from 'typescript-eslint' | |
| import { defineConfig, globalIgnores } from 'eslint/config' | |
| export default defineConfig([ | |
| globalIgnores(['dist']), | |
| { | |
| files: ['**/*.{ts,tsx}'], | |
| extends: [ | |
| js.configs.recommended, | |
| tseslint.configs.recommended, | |
| reactHooks.configs.flat.recommended, | |
| reactRefresh.configs.vite, | |
| ], | |
| languageOptions: { | |
| ecmaVersion: 2020, | |
| globals: globals.browser, | |
| }, | |
| }, | |
| ]) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| @import "tailwindcss"; | |
| @theme { | |
| --color-bitcoin: #f7931a; | |
| --color-bitcoin-dark: #e68a00; | |
| } | |
| html { | |
| transition: background-color 0.3s, color 0.3s; | |
| } | |
| body { | |
| font-family: 'Inter', system-ui, -apple-system, sans-serif; | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!doctype html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8" /> | |
| <link rel="icon" type="image/svg+xml" href="/favicon.svg" /> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | |
| <title>demand-competency-test</title> | |
| </head> | |
| <body> | |
| <div id="root"></div> | |
| <script type="module" src="/src/main.tsx"></script> | |
| </body> | |
| </html> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import { StrictMode } from 'react' | |
| import { createRoot } from 'react-dom/client' | |
| import './index.css' | |
| import App from './App.tsx' | |
| createRoot(document.getElementById('root')!).render( | |
| <StrictMode> | |
| <App /> | |
| </StrictMode>, | |
| ) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| { | |
| "name": "demand-competency-test", | |
| "private": true, | |
| "version": "0.0.0", | |
| "type": "module", | |
| "scripts": { | |
| "dev": "vite", | |
| "build": "tsc -b && vite build", | |
| "lint": "eslint .", | |
| "preview": "vite preview" | |
| }, | |
| "dependencies": { | |
| "@tanstack/react-query": "^5.96.2", | |
| "react": "^19.2.4", | |
| "react-dom": "^19.2.4", | |
| "react-router-dom": "^7.14.0", | |
| "recharts": "^3.8.1" | |
| }, | |
| "devDependencies": { | |
| "@eslint/js": "^9.39.4", | |
| "@tailwindcss/vite": "^4.2.2", | |
| "@types/node": "^24.12.2", | |
| "@types/react": "^19.2.14", | |
| "@types/react-dom": "^19.2.3", | |
| "@vitejs/plugin-react": "^6.0.1", | |
| "eslint": "^9.39.4", | |
| "eslint-plugin-react-hooks": "^7.0.1", | |
| "eslint-plugin-react-refresh": "^0.5.2", | |
| "globals": "^17.4.0", | |
| "tailwindcss": "^4.2.2", | |
| "typescript": "~6.0.2", | |
| "typescript-eslint": "^8.58.0", | |
| "vite": "^8.0.4" | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import { createContext, useContext, useEffect, useState } from 'react'; | |
| type Theme = 'dark' | 'light'; | |
| interface ThemeContextType { | |
| theme: Theme; | |
| toggleTheme: () => void; | |
| } | |
| const ThemeContext = createContext<ThemeContextType>({ | |
| theme: 'dark', | |
| toggleTheme: () => {}, | |
| }); | |
| export function ThemeProvider({ children }: { children: React.ReactNode }) { | |
| const [theme, setTheme] = useState<Theme>(() => { | |
| const stored = localStorage.getItem('theme') as Theme | null; | |
| return stored || 'dark'; | |
| }); | |
| useEffect(() => { | |
| localStorage.setItem('theme', theme); | |
| document.documentElement.classList.toggle('dark', theme === 'dark'); | |
| document.documentElement.classList.toggle('light', theme === 'light'); | |
| }, [theme]); | |
| const toggleTheme = () => { | |
| setTheme(t => (t === 'dark' ? 'light' : 'dark')); | |
| }; | |
| return ( | |
| <ThemeContext.Provider value={{ theme, toggleTheme }}> | |
| {children} | |
| </ThemeContext.Provider> | |
| ); | |
| } | |
| export const useTheme = () => useContext(ThemeContext); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| { | |
| "compilerOptions": { | |
| "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", | |
| "target": "es2023", | |
| "lib": ["ES2023", "DOM", "DOM.Iterable"], | |
| "module": "esnext", | |
| "types": ["vite/client"], | |
| "skipLibCheck": true, | |
| /* Bundler mode */ | |
| "moduleResolution": "bundler", | |
| "allowImportingTsExtensions": true, | |
| "verbatimModuleSyntax": true, | |
| "moduleDetection": "force", | |
| "noEmit": true, | |
| "jsx": "react-jsx", | |
| /* Linting */ | |
| "noUnusedLocals": true, | |
| "noUnusedParameters": true, | |
| "erasableSyntaxOnly": true, | |
| "noFallthroughCasesInSwitch": true | |
| }, | |
| "include": ["src"] | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Show hidden characters
| { | |
| "files": [], | |
| "references": [ | |
| { "path": "./tsconfig.app.json" }, | |
| { "path": "./tsconfig.node.json" } | |
| ] | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import { defineConfig } from 'vite' | |
| import react from '@vitejs/plugin-react' | |
| import tailwindcss from '@tailwindcss/vite' | |
| export default defineConfig({ | |
| plugins: [react(), tailwindcss()], | |
| }) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment