boerse
This commit is contained in:
135
modules/boersenchecker/assets/boersenchecker.css
Normal file
135
modules/boersenchecker/assets/boersenchecker.css
Normal 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;
|
||||
}
|
||||
}
|
||||
146
modules/boersenchecker/assets/boersenchecker.js
Normal file
146
modules/boersenchecker/assets/boersenchecker.js
Normal 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();
|
||||
})();
|
||||
Reference in New Issue
Block a user