Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
name: CI

on:
push:
branches: [main]
pull_request:

jobs:
server:
name: Server (Typecheck + Tests)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
cache-dependency-path: server/package-lock.json
- run: npm ci
working-directory: server
- run: npm run typecheck
working-directory: server
- run: npm test
working-directory: server

client:
name: Client (Lint + Build)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
cache-dependency-path: client/package-lock.json
- run: npm ci
working-directory: client
- run: npm run lint
working-directory: client
- run: npm run build
working-directory: client
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ server/.env
*.log
.DS_Store
client/dist/
server/.cache/
17 changes: 16 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,22 @@ COINGECKO_API_KEY=CG-dein_coingecko_key
| `CMC_API_KEY` | CoinMarketCap | Alle Coins (`#/coins`) |
| `COINGECKO_API_KEY` | CoinGecko | Markets, Charts, F&G |

Status: `GET http://localhost:3001/api/status`
Status: `GET http://localhost:3001/api/status` · Health: `GET http://localhost:3001/api/health`

## Ausfallsicherheit

Fällt eine API aus, springt automatisch eine andere Quelle ein:

| Daten | Primär | Fallback |
|-------|--------|----------|
| Fear & Greed | Alternative.me | CoinMarketCap F&G → eigener Composite aus Marktdaten |
| Alle Coins | CoinMarketCap | CoinGecko (gleiche Datenform) |
| Markets | CoinGecko | Aufbereitung aus gecachten CMC-Daten |

Zusätzlich: Die letzten guten Daten bleiben im Cache (statt 503) und werden
als Snapshot unter `server/.cache/` gespeichert — nach einem Neustart sind
sofort Daten da. `GET /api/health` zeigt pro Datensatz Quelle, Alter und
letzten Fehler.

## Features

Expand Down
42 changes: 0 additions & 42 deletions client/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

22 changes: 12 additions & 10 deletions client/src/hooks/usePolling.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,18 @@ export function usePolling<T>(fetcher: () => Promise<T>, intervalMs: number) {
const [error, setError] = useState<string | null>(null);
const [loading, setLoading] = useState(true);

const load = useCallback(async () => {
try {
const result = await fetcher();
setData(result);
setError(null);
} catch (e) {
setError(e instanceof Error ? e.message : 'Fehler beim Laden');
} finally {
setLoading(false);
}
const load = useCallback(() => {
return fetcher()
.then((result) => {
setData(result);
setError(null);
})
.catch((e: unknown) => {
setError(e instanceof Error ? e.message : 'Fehler beim Laden');
})
.finally(() => {
setLoading(false);
});
}, [fetcher]);

useEffect(() => {
Expand Down
27 changes: 10 additions & 17 deletions client/src/main.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,15 @@
import { StrictMode, useEffect } from 'react';
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import './index.css';
import App from './App.tsx';

function Root() {
useEffect(() => {
document.documentElement.classList.add('dark');
document.documentElement.setAttribute('data-theme', 'dark');
// Remove the boot skeleton once React has mounted
const boot = document.getElementById('ts-boot');
if (boot) boot.remove();
}, []);
document.documentElement.classList.add('dark');
document.documentElement.setAttribute('data-theme', 'dark');
// Boot-Skeleton entfernen, sobald das React-Bundle ausgeführt wird
document.getElementById('ts-boot')?.remove();

return (
<StrictMode>
<App />
</StrictMode>
);
}

createRoot(document.getElementById('root')!).render(<Root />);
createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
</StrictMode>,
);
7 changes: 4 additions & 3 deletions client/src/pages/CoinDetail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,13 @@ export function CoinDetail() {
const [live, setLive] = useState<LivePriceResponse | null>(null);
const [chart, setChart] = useState<CoinChartResponse | null>(null);
const [range, setRange] = useState<Range>('24h');
const [loading, setLoading] = useState(true);
// Abgeleiteter Ladezustand: lädt, solange die Daten nicht zum aktuellen Slug gehören
const [loadedSlug, setLoadedSlug] = useState<string | null>(null);
const loading = Boolean(slug) && loadedSlug !== slug;
const liveTimer = useRef<ReturnType<typeof setInterval> | null>(null);

useEffect(() => {
if (!slug) return;
setLoading(true);
void Promise.allSettled([
fetchCoinDetail(slug),
fetchCoinLive(slug),
Expand All @@ -42,7 +43,7 @@ export function CoinDetail() {
if (d.status === 'fulfilled') setDetail(d.value);
if (l.status === 'fulfilled') setLive(l.value);
if (c.status === 'fulfilled') setChart(c.value);
setLoading(false);
setLoadedSlug(slug);
});
}, [slug]);

Expand Down
4 changes: 4 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading