boerse
All checks were successful
Deploy / deploy-staging (push) Successful in 6s
Deploy / deploy-production (push) Has been skipped

This commit is contained in:
2026-04-22 01:31:18 +02:00
parent a1bab34bd3
commit 91dc84d027
16 changed files with 1697 additions and 86 deletions

View File

@@ -0,0 +1,135 @@
.bc-hero {
background:
radial-gradient(circle at top right, rgba(71, 169, 255, 0.22), transparent 32%),
radial-gradient(circle at bottom left, rgba(81, 214, 141, 0.18), transparent 28%),
linear-gradient(145deg, rgba(255,255,255,0.04), rgba(255,255,255,0.01));
}
.bc-toolbar,
.bc-card-grid {
display: grid;
gap: 14px;
}
.bc-toolbar {
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
}
.bc-card-grid {
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
}
.bc-stat {
padding: 16px;
border-radius: 18px;
background: linear-gradient(180deg, rgba(255,255,255,0.06), rgba(255,255,255,0.02));
border: 1px solid rgba(255,255,255,0.08);
}
.bc-stat-value {
margin-top: 6px;
font-size: 1.35rem;
font-weight: 700;
}
.bc-surface {
padding: 18px;
border-radius: 22px;
background: linear-gradient(180deg, rgba(255,255,255,0.05), rgba(255,255,255,0.02));
border: 1px solid rgba(255,255,255,0.08);
}
.bc-chart-shell {
position: relative;
min-height: 360px;
overflow: hidden;
}
.bc-chart-svg {
width: 100%;
height: 340px;
display: block;
}
.bc-chart-path {
fill: none;
stroke: #5eead4;
stroke-width: 3;
stroke-linecap: round;
stroke-linejoin: round;
filter: drop-shadow(0 12px 24px rgba(94, 234, 212, 0.25));
}
.bc-chart-area {
fill: url(#bc-chart-fill);
}
.bc-chart-grid line {
stroke: rgba(255,255,255,0.08);
stroke-dasharray: 4 6;
}
.bc-range-list,
.bc-inline-actions {
display: flex;
gap: 10px;
flex-wrap: wrap;
}
.bc-range-button {
border: 1px solid rgba(255,255,255,0.12);
background: rgba(255,255,255,0.04);
color: var(--text);
padding: 8px 12px;
border-radius: 999px;
cursor: pointer;
transition: transform .18s ease, background .18s ease, border-color .18s ease;
}
.bc-range-button:hover,
.bc-range-button[aria-pressed="true"] {
transform: translateY(-1px);
background: rgba(94, 234, 212, 0.12);
border-color: rgba(94, 234, 212, 0.45);
}
.bc-panel-fade {
animation: bcPanelFade .35s ease;
}
@keyframes bcPanelFade {
from { opacity: 0; transform: translateY(8px) scale(.99); }
to { opacity: 1; transform: translateY(0) scale(1); }
}
.bc-position-list {
display: grid;
gap: 12px;
}
.bc-position-row {
display: grid;
grid-template-columns: minmax(0, 1.6fr) repeat(3, minmax(100px, .8fr));
gap: 12px;
align-items: center;
padding: 14px 16px;
border-radius: 16px;
background: rgba(255,255,255,0.03);
border: 1px solid rgba(255,255,255,0.06);
}
.bc-pill-soft {
display: inline-flex;
align-items: center;
padding: 6px 10px;
border-radius: 999px;
background: rgba(255,255,255,0.06);
color: var(--muted);
font-size: .85rem;
}
@media (max-width: 900px) {
.bc-position-row {
grid-template-columns: 1fr;
}
}

View File

@@ -0,0 +1,146 @@
(function () {
const app = document.querySelector('[data-bc-home]');
if (!app) return;
const chartShell = app.querySelector('[data-bc-chart]');
const instrumentSelect = app.querySelector('[data-bc-instrument]');
const instrumentNameNode = app.querySelector('[data-bc-instrument-name]');
const instrumentMetaNode = app.querySelector('[data-bc-instrument-meta]');
const rangeButtons = Array.from(app.querySelectorAll('[data-range]'));
const statusNode = app.querySelector('[data-bc-chart-status]');
const summaryNode = app.querySelector('[data-bc-chart-summary]');
const endpoint = app.getAttribute('data-chart-endpoint') || '';
const instrumentsScript = app.querySelector('[data-bc-instruments-json]');
const instrumentMap = new Map();
if (instrumentsScript?.textContent) {
try {
const items = JSON.parse(instrumentsScript.textContent);
if (Array.isArray(items)) {
items.forEach((item) => instrumentMap.set(String(item.instrument_id), item));
}
} catch (_error) {}
}
let activeRange = '1m';
let currentPayload = null;
function pointsForRange(payload, range) {
if (!payload) return [];
const daily = payload.daily || [];
const weekly = payload.weekly || [];
const monthly = payload.monthly || [];
switch (range) {
case '1d': return daily.slice(-2);
case '5d': return daily.slice(-5);
case '1m': return daily.slice(-22);
case '3m': return daily.slice(-66);
case '6m': return weekly.slice(-26);
case '1y': return weekly.slice(-52);
case '5y': return monthly.slice(-60);
default: return daily.slice(-22);
}
}
function renderChart(points) {
if (!chartShell) return;
chartShell.classList.remove('bc-panel-fade');
void chartShell.offsetWidth;
chartShell.classList.add('bc-panel-fade');
if (!points || points.length === 0) {
chartShell.innerHTML = '<div class="muted">Keine Chartdaten verfuegbar.</div>';
return;
}
const values = points.map((point) => Number(point.close || 0));
const min = Math.min(...values);
const max = Math.max(...values);
const width = 920;
const height = 340;
const paddingX = 24;
const paddingY = 28;
const usableWidth = width - paddingX * 2;
const usableHeight = height - paddingY * 2;
const spread = max - min || 1;
const coords = points.map((point, index) => {
const x = paddingX + (usableWidth * index / Math.max(points.length - 1, 1));
const y = paddingY + usableHeight - ((Number(point.close || 0) - min) / spread) * usableHeight;
return { x, y };
});
const path = coords.map((coord, index) => `${index === 0 ? 'M' : 'L'}${coord.x.toFixed(2)},${coord.y.toFixed(2)}`).join(' ');
const area = `${path} L${coords[coords.length - 1].x.toFixed(2)},${height - paddingY} L${coords[0].x.toFixed(2)},${height - paddingY} Z`;
const grid = [0, 1, 2, 3].map((step) => {
const y = paddingY + (usableHeight * step / 3);
return `<line x1="${paddingX}" y1="${y}" x2="${width - paddingX}" y2="${y}"></line>`;
}).join('');
chartShell.innerHTML = `
<svg class="bc-chart-svg" viewBox="0 0 ${width} ${height}" preserveAspectRatio="none">
<defs>
<linearGradient id="bc-chart-fill" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="rgba(94,234,212,0.32)"></stop>
<stop offset="100%" stop-color="rgba(94,234,212,0.02)"></stop>
</linearGradient>
</defs>
<g class="bc-chart-grid">${grid}</g>
<path class="bc-chart-area" d="${area}"></path>
<path class="bc-chart-path" d="${path}"></path>
</svg>
`;
const first = values[0];
const last = values[values.length - 1];
const delta = last - first;
const percent = first !== 0 ? (delta / first) * 100 : 0;
if (summaryNode) {
summaryNode.textContent = `${last.toFixed(2)} | ${delta >= 0 ? '+' : ''}${delta.toFixed(2)} (${percent.toFixed(2)}%)`;
}
}
async function loadChart() {
const instrumentId = instrumentSelect ? instrumentSelect.value : '';
if (!instrumentId || !endpoint) {
if (statusNode) statusNode.textContent = 'Keine Aktie fuer den Chart ausgewaehlt.';
if (summaryNode) summaryNode.textContent = '-';
if (chartShell) chartShell.innerHTML = '<div class="muted">Keine Chartdaten verfuegbar.</div>';
return;
}
if (statusNode) statusNode.textContent = 'Chartdaten werden geladen...';
try {
const response = await fetch(`${endpoint}${endpoint.includes('?') ? '&' : '?'}instrument_id=${encodeURIComponent(instrumentId)}`, { headers: { Accept: 'application/json' } });
const payload = await response.json();
if (!payload.ok) {
throw new Error(payload.message || 'Chartdaten konnten nicht geladen werden.');
}
currentPayload = payload;
renderChart(pointsForRange(payload, activeRange));
if (statusNode) statusNode.textContent = `Quelle: Alpha Vantage | Symbol ${payload.symbol || ''}`;
} catch (error) {
currentPayload = null;
chartShell.innerHTML = `<div class="muted">${error.message}</div>`;
if (statusNode) statusNode.textContent = 'Fehler beim Laden der Chartdaten.';
}
}
rangeButtons.forEach((button) => {
button.addEventListener('click', () => {
activeRange = button.getAttribute('data-range') || '1m';
rangeButtons.forEach((item) => item.setAttribute('aria-pressed', item === button ? 'true' : 'false'));
renderChart(pointsForRange(currentPayload, activeRange));
});
});
if (instrumentSelect) {
instrumentSelect.addEventListener('change', () => {
const meta = instrumentMap.get(String(instrumentSelect.value));
if (meta) {
if (instrumentNameNode) instrumentNameNode.textContent = meta.instrument_name || 'Keine Aktie ausgewaehlt';
if (instrumentMetaNode) instrumentMetaNode.textContent = `${meta.symbol || ''} · ${meta.isin || '-'}`;
}
loadChart();
});
}
loadChart();
})();