Skip to content

Instantly share code, notes, and snippets.

@jayva1
Created April 12, 2026 10:16
Show Gist options
  • Select an option

  • Save jayva1/fa34bd8e5998066366cc1fcf80af0a83 to your computer and use it in GitHub Desktop.

Select an option

Save jayva1/fa34bd8e5998066366cc1fcf80af0a83 to your computer and use it in GitHub Desktop.
DMND Competency Test - BTC Dashboard by Jay Valiya
.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);
}
}
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;
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>
);
}
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,
},
},
])
@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;
}
<!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>
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>,
)
{
"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"
}
}
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);
{
"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"]
}
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
]
}
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