let cryptoList = [];
let alarms = [];
let notifications = [];
let lastPrices = {};
let userOptions = JSON.parse(localStorage.getItem("userOptions")) || {
soundFile: "ping.mp3",
darkMode: true,
enableDesktopNotifications: false,
okxApiKey: "",
okxSecretKey: "",
okxPassphrase: "",
};
let apiPreference = JSON.parse(localStorage.getItem("apiPreference")) || {};
let editMode = false;
let currentApiSelectSymbol = null;
let exchangeUsedMap = {};
async function loadCryptosFromServer() {
try {
const resp = await fetch("/api/cryptos");
cryptoList = await resp.json();
renderCryptoGrid();
} catch (err) {
console.error("Fehler beim Laden der Kryptoliste:", err);
}
}
async function addNewCrypto() {
const newCrypto = document.getElementById("newCryptoSymbol").value.trim().toUpperCase();
if (!newCrypto) return;
if (!(await isBinanceSupported(newCrypto)) && !(await isOkxSupported(newCrypto))) {
showErrorMessage("This cryptocurrency is not supported on Binance or OKX (USDT).");
closeAddCryptoModal();
return;
}
try {
const resp = await fetch("/api/cryptos", {
method: "POST",
headers: {"Content-Type": "application/json"},
body: JSON.stringify({symbol: newCrypto}),
});
const updatedList = await resp.json();
cryptoList = updatedList;
renderCryptoGrid();
} catch (err) {
showErrorMessage("Error adding new crypto: " + err.message);
}
closeAddCryptoModal();
}
async function deleteCrypto(index) {
const symbol = cryptoList[index];
if (!symbol) return;
try {
await fetch(`/api/cryptos/${symbol}`, {method: "DELETE"});
loadCryptosFromServer();
} catch (err) {
showErrorMessage("Error deleting crypto: " + err.message);
}
}
async function loadAlarmsFromServer() {
try {
const resp = await fetch("/api/alarms");
alarms = await resp.json();
renderAlarmList();
} catch (err) {
console.error("Fehler beim Laden der Alarme:", err);
}
}
async function addAlarm() {
const symbol = document.getElementById("alarmSymbol").value;
const price = parseFloat(document.getElementById("alarmPrice").value);
const frequency = document.getElementById("alarmFrequency").value;
const direction = document.getElementById("alarmDirection").value;
if (!symbol || isNaN(price)) return;
try {
await fetch("/api/alarms", {
method: "POST",
headers: {"Content-Type": "application/json"},
body: JSON.stringify({symbol, price, frequency, direction}),
});
loadAlarmsFromServer();
} catch (err) {
showErrorMessage("Error adding alarm: " + err.message);
}
}
async function deleteAlarm(alarmId) {
try {
await fetch(`/api/alarms/${alarmId}`, {method: "DELETE"});
loadAlarmsFromServer();
} catch (err) {
showErrorMessage("Error deleting alarm: " + err.message);
}
}
async function loadNotificationsFromServer() {
try {
const resp = await fetch("/api/notifications");
notifications = await resp.json();
renderNotifications();
} catch (err) {
console.error("Fehler beim Laden der Notifications:", err);
}
}
async function addNotification(msg) {
try {
await fetch("/api/notifications", {
method: "POST",
headers: {"Content-Type": "application/json"},
body: JSON.stringify({message: msg}),
});
loadNotificationsFromServer();
} catch (err) {
console.error("Fehler beim Hinzufügen einer Notification:", err);
}
}
async function clearNotifications() {
try {
await fetch("/api/notifications", {method: "DELETE"});
loadNotificationsFromServer();
} catch (err) {
console.error("Fehler beim Löschen der Notifications:", err);
}
}
async function init() {
document.getElementById("alarmSound").src = "sound/" + userOptions.soundFile;
updateTheme(userOptions.darkMode);
await loadCryptosFromServer();
await loadAlarmsFromServer();
await loadNotificationsFromServer();
setInterval(() => {
cryptoList.forEach((symbol, index) => {
const elementId = "crypto-" + index;
fetchCryptoData(symbol, elementId).catch(() => setNotSupported(elementId));
});
}, 1000);
renderNotifications();
}
function renderCryptoGrid() {
const grid = document.getElementById("cryptoGrid");
grid.innerHTML = "";
cryptoList.forEach((symbol, index) => {
const boxId = "crypto-" + index;
const box = document.createElement("div");
box.className = "crypto-box";
box.id = boxId;
const heading = document.createElement("h2");
heading.textContent = `${symbol}/USDT`;
box.appendChild(heading);
const dailyP = document.createElement("p");
dailyP.id = "daily-" + boxId;
dailyP.textContent = "Daily Price: -";
box.appendChild(dailyP);
const hourlyP = document.createElement("p");
hourlyP.id = "hourly-" + boxId;
hourlyP.textContent = "Price H: -";
box.appendChild(hourlyP);
const priceP = document.createElement("p");
priceP.id = "price-" + boxId;
priceP.innerHTML = "Current Price: -";
box.appendChild(priceP);
const change24 = document.createElement("p");
change24.id = "change24-" + boxId;
change24.textContent = "24h Change: -";
box.appendChild(change24);
const change1h = document.createElement("p");
change1h.id = "change1h-" + boxId;
change1h.textContent = "1h Change: -";
box.appendChild(change1h);
const apiLabel = document.createElement("div");
apiLabel.id = "api-" + boxId;
apiLabel.className = "api-label";
apiLabel.textContent = "API: ?";
apiLabel.addEventListener("click", () => {
openApiSelectModal(symbol);
});
box.appendChild(apiLabel);
if (editMode) {
box.draggable = true;
box.setAttribute("data-index", index);
box.addEventListener("dragstart", handleDragStart);
box.addEventListener("dragend", handleDragEnd);
box.addEventListener("dragover", handleDragOver);
box.addEventListener("dragleave", handleDragLeave);
box.addEventListener("drop", handleDrop);
const deleteBtn = document.createElement("button");
deleteBtn.textContent = "X";
deleteBtn.className = "delete-btn";
deleteBtn.addEventListener("click", (e) => {
e.stopPropagation();
deleteCrypto(index);
});
box.appendChild(deleteBtn);
} else {
box.addEventListener("click", (event) => {
if (event.target.classList.contains("api-label") || event.target.closest(".api-label")) {
return;
}
const usedExchange = exchangeUsedMap[symbol] || "BINANCE";
openTradingViewModal(usedExchange, symbol);
});
}
grid.appendChild(box);
fetchCryptoData(symbol, boxId).catch(() => setNotSupported(boxId));
});
}
function renderAlarmList() {
const container = document.getElementById("alarmListContainer");
container.innerHTML = "";
alarms.forEach((alarm) => {
const item = document.createElement("div");
item.className = "alarm-item";
const textSpan = document.createElement("span");
textSpan.innerText = `${alarm.symbol}: ${alarm.price} (${alarm.frequency}, ${alarm.direction})`;
item.appendChild(textSpan);
const delBtn = document.createElement("button");
delBtn.innerHTML = "🗑";
delBtn.className = "alarm-delete-btn";
delBtn.onclick = () => deleteAlarm(alarm.id);
item.appendChild(delBtn);
container.appendChild(item);
});
}
function renderNotifications() {
const notifyList = document.getElementById("notifyList");
notifyList.innerHTML = "";
notifications.forEach((item) => {
const li = document.createElement("li");
li.className = "notify-item";
li.innerHTML = `
${item.message}
${item.timestamp}
`;
notifyList.appendChild(li);
});
}
function openAddCryptoModal() {
document.getElementById("addCryptoModal").style.display = "block";
document.getElementById("newCryptoSymbol").value = "";
}
function closeAddCryptoModal() {
document.getElementById("addCryptoModal").style.display = "none";
}
function openAlarmModal() {
document.getElementById("alarmModal").style.display = "block";
const dropdown = document.getElementById("alarmSymbol");
dropdown.innerHTML = "";
cryptoList.forEach((symbol) => {
const option = document.createElement("option");
option.value = symbol;
option.textContent = symbol;
dropdown.appendChild(option);
});
renderAlarmList();
}
function closeAlarmModal() {
document.getElementById("alarmModal").style.display = "none";
}
function openOptionsModal() {
document.getElementById("optionsModal").style.display = "block";
document.getElementById("soundSelect").value = userOptions.soundFile;
document.getElementById("darkModeToggle").checked = userOptions.darkMode;
document.getElementById("desktopNotifyToggle").checked = userOptions.enableDesktopNotifications;
}
function closeOptionsModal() {
document.getElementById("optionsModal").style.display = "none";
}
function openApiModal() {
document.getElementById("apiModal").style.display = "block";
document.getElementById("okxApiKey").value = userOptions.okxApiKey || "";
document.getElementById("okxSecretKey").value = userOptions.okxSecretKey || "";
document.getElementById("okxPassphrase").value = userOptions.okxPassphrase || "";
}
function closeApiModal() {
document.getElementById("apiModal").style.display = "none";
}
function openApiSelectModal(symbol) {
currentApiSelectSymbol = symbol;
document.getElementById("apiSelectSymbol").textContent = symbol;
const currentVal = apiPreference[symbol] || "auto";
document.getElementById("apiSelectDropdown").value = currentVal;
document.getElementById("apiSelectModal").style.display = "block";
}
function closeApiSelectModal() {
document.getElementById("apiSelectModal").style.display = "none";
}
function openBuyMeModal() {
document.getElementById("buyMeModal").style.display = "block";
}
function closeBuyMeModal() {
document.getElementById("buyMeModal").style.display = "none";
}
function openCryptoNews() {
document.getElementById("cryptoNewsModal").style.display = "block";
}
function closeCryptoNewsModal() {
document.getElementById("cryptoNewsModal").style.display = "none";
}
function openEconomicCalendar() {
document.getElementById("economicCalendarModal").style.display = "block";
}
function closeEconomicCalendarModal() {
document.getElementById("economicCalendarModal").style.display = "none";
}
function saveOptions() {
userOptions.soundFile = document.getElementById("soundSelect").value;
const alarmSound = document.getElementById("alarmSound");
alarmSound.src = "sound/" + userOptions.soundFile;
alarmSound.load();
userOptions.darkMode = document.getElementById("darkModeToggle").checked;
updateTheme(userOptions.darkMode);
userOptions.enableDesktopNotifications = document.getElementById("desktopNotifyToggle").checked;
localStorage.setItem("userOptions", JSON.stringify(userOptions));
renderCryptoGrid();
closeOptionsModal();
}
function updateTheme(darkMode) {
if (darkMode) {
document.body.classList.remove("light");
document.body.classList.add("dark");
} else {
document.body.classList.remove("dark");
document.body.classList.add("light");
}
}
function saveApiSettings() {
userOptions.okxApiKey = document.getElementById("okxApiKey").value;
userOptions.okxSecretKey = document.getElementById("okxSecretKey").value;
userOptions.okxPassphrase = document.getElementById("okxPassphrase").value;
localStorage.setItem("userOptions", JSON.stringify(userOptions));
closeApiModal();
}
function saveApiSelection() {
const val = document.getElementById("apiSelectDropdown").value;
if (currentApiSelectSymbol) {
apiPreference[currentApiSelectSymbol] = val;
localStorage.setItem("apiPreference", JSON.stringify(apiPreference));
renderCryptoGrid();
}
closeApiSelectModal();
}
function toggleEditMode() {
editMode = !editMode;
document.getElementById("editButton").textContent = editMode ? "Done" : "Edit List";
renderCryptoGrid();
}
function handleDragStart(event) {
event.currentTarget.classList.add("dragging");
const index = event.currentTarget.getAttribute("data-index");
event.dataTransfer.setData("text/plain", index);
}
function handleDragEnd(event) {
event.currentTarget.classList.remove("dragging");
}
function handleDragOver(event) {
event.preventDefault();
event.currentTarget.classList.add("drag-over");
}
function handleDragLeave(event) {
event.currentTarget.classList.remove("drag-over");
}
function handleDrop(event) {
event.preventDefault();
event.currentTarget.classList.remove("drag-over");
const fromIndex = parseInt(event.dataTransfer.getData("text/plain"), 10);
const toIndex = parseInt(event.currentTarget.getAttribute("data-index"), 10);
reorderCryptoList(fromIndex, toIndex);
}
async function reorderCryptoList(fromIndex, toIndex) {
if (fromIndex === toIndex) return;
const item = cryptoList.splice(fromIndex, 1)[0];
cryptoList.splice(toIndex, 0, item);
renderCryptoGrid();
await saveCryptoList();
}
async function saveCryptoList() {
try {
await fetch("/api/cryptos", {
method: "PUT",
headers: {"Content-Type": "application/json"},
body: JSON.stringify({cryptoList: cryptoList}),
});
} catch (err) {
console.error("Fehler beim Speichern der Kryptoliste:", err);
}
}
async function fetchCryptoData(symbol, elementId) {
const preferredApi = apiPreference[symbol] || "auto";
if (preferredApi === "binance") {
if (await isBinanceSupported(symbol)) {
return fetchFromBinance(symbol, elementId);
} else {
return setNotSupported(elementId);
}
}
if (preferredApi === "okx") {
if (await isOkxSupported(symbol)) {
return fetchFromOkx(symbol, elementId);
} else {
return setNotSupported(elementId);
}
}
if (await isBinanceSupported(symbol)) {
return fetchFromBinance(symbol, elementId);
} else if (await isOkxSupported(symbol)) {
return fetchFromOkx(symbol, elementId);
} else {
return setNotSupported(elementId);
}
}
function setNotSupported(elementId) {
document.getElementById("daily-" + elementId).textContent = "Daily Price: ❌";
document.getElementById("hourly-" + elementId).textContent = "Price H: ❌";
document.getElementById("price-" + elementId).innerHTML = "Current Price: ❌ Not supported";
document.getElementById("change24-" + elementId).textContent = "24h Change: -";
document.getElementById("change1h-" + elementId).textContent = "1h Change: -";
document.getElementById("api-" + elementId).textContent = "API: ?";
}
async function fetchFromBinance(symbol, elementId) {
const tickerUrl = `https://api.binance.com/api/v3/ticker/24hr?symbol=${symbol}USDT`;
let resp = await fetch(tickerUrl);
if (!resp.ok) throw new Error("Binance request failed");
let data = await resp.json();
if (data.code) throw new Error("Binance error code");
const dailyOpen = parseFloat(data.openPrice);
const lastPrice = parseFloat(data.lastPrice);
const priceChange24h = parseFloat(data.priceChangePercent);
const klineUrl = `https://api.binance.com/api/v3/klines?symbol=${symbol}USDT&interval=1h&limit=1`;
let klResp = await fetch(klineUrl);
if (!klResp.ok) throw new Error("Binance 1h kline request failed");
let kData = await klResp.json();
if (!kData[0]) throw new Error("No 1h data from Binance");
const hourlyOpen = parseFloat(kData[0][1]);
const closeNow = parseFloat(kData[0][4]);
let pct1h = 0;
if (hourlyOpen !== 0) {
pct1h = ((closeNow - hourlyOpen) / hourlyOpen) * 100;
}
updateCryptoBox({
symbol,
elementId,
apiUsed: "BINANCE",
dailyOpen,
hourlyOpen,
lastPrice,
change24h: priceChange24h,
change1h: pct1h,
});
}
async function fetchFromOkx(symbol, elementId) {
const instId = `${symbol}-USDT`;
const tickerUrl = `https://www.okx.com/api/v5/market/ticker?instId=${instId}`;
let resp = await fetch(tickerUrl);
if (!resp.ok) throw new Error("OKX request failed");
let data = await resp.json();
if (!data.data || !data.data[0]) throw new Error("OKX no ticker data");
const ticker = data.data[0];
const dailyOpen = parseFloat(ticker.open24h);
const lastPrice = parseFloat(ticker.last);
let pct24 = 0;
if (dailyOpen !== 0) {
pct24 = ((lastPrice - dailyOpen) / dailyOpen) * 100;
}
const cUrl = `https://www.okx.com/api/v5/market/candles?instId=${instId}&bar=1H&limit=1`;
let cResp = await fetch(cUrl);
if (!cResp.ok) throw new Error("OKX 1h candle request failed");
let cData = await cResp.json();
if (!cData.data || !cData.data[0]) throw new Error("OKX no 1h data");
const hourlyOpen = parseFloat(cData.data[0][1]);
const closeNow = parseFloat(cData.data[0][4]);
let pct1h = 0;
if (hourlyOpen !== 0) {
pct1h = ((closeNow - hourlyOpen) / hourlyOpen) * 100;
}
updateCryptoBox({
symbol,
elementId,
apiUsed: "OKX",
dailyOpen,
hourlyOpen,
lastPrice,
change24h: pct24,
change1h: pct1h,
});
}
function formatPrice(value) {
if (isNaN(value)) {
return "-";
}
if (value < 0.01) {
return value.toFixed(6);
} else {
return value.toFixed(4);
}
}
function updateCryptoBox({symbol, elementId, apiUsed, dailyOpen, hourlyOpen, lastPrice, change24h, change1h}) {
exchangeUsedMap[symbol] = apiUsed;
const dOpenStr = formatPrice(dailyOpen);
const hOpenStr = formatPrice(hourlyOpen);
const lastStr = formatPrice(lastPrice);
const pct24Str = isNaN(change24h) ? "-" : change24h.toFixed(2) + "%";
const pct1hStr = isNaN(change1h) ? "-" : change1h.toFixed(2) + "%";
document.getElementById("daily-" + elementId).innerHTML = `Daily Price:
${dOpenStr} USDT
`;
document.getElementById("hourly-" + elementId).innerHTML = `Price H:
${hOpenStr} USDT
`;
document.getElementById("price-" + elementId).innerHTML = `Current Price:
${lastStr} USDT
`;
const c24 = document.getElementById("change24-" + elementId);
c24.innerHTML = `24h Change: ${pct24Str}`;
c24.className = "change " + (change24h >= 0 ? "up" : "down");
const c1h = document.getElementById("change1h-" + elementId);
c1h.innerHTML = `1h Change: ${pct1hStr}
`;
c1h.className = "change " + (change1h >= 0 ? "up" : "down");
document.getElementById("api-" + elementId).innerHTML = `API: ${apiUsed}`;
checkAlarms(symbol, lastPrice);
lastPrices[symbol] = lastPrice;
}
function checkAlarms(symbol, currentPrice) {
alarms.forEach((alarm) => {
if (alarm.symbol !== symbol) return;
if (alarm.triggered) return;
const alarmPrice = parseFloat(alarm.price);
const prevPrice = lastPrices[symbol] || null;
if (prevPrice === null || isNaN(prevPrice)) return;
let conditionMet = false;
if (alarm.direction === "Rising") {
conditionMet = prevPrice < alarmPrice && currentPrice >= alarmPrice;
} else if (alarm.direction === "Falling") {
conditionMet = prevPrice > alarmPrice && currentPrice <= alarmPrice;
} else if (alarm.direction === "Both") {
const crossingUp = prevPrice < alarmPrice && currentPrice >= alarmPrice;
const crossingDown = prevPrice > alarmPrice && currentPrice <= alarmPrice;
conditionMet = crossingUp || crossingDown;
}
if (conditionMet) {
const msg = `⚠️ ALARM (${alarm.frequency}, ${alarm.direction}): ${symbol} reached ${alarmPrice}!`;
showAlarmPopup(msg);
if (alarm.frequency === "Once") {
alarm.triggered = true;
}
}
});
}
function showErrorMessage(msg) {
document.getElementById("errorMessage").textContent = msg;
document.getElementById("errorOverlay").style.display = "block";
}
function closeErrorPopup() {
document.getElementById("errorOverlay").style.display = "none";
}
function showAlarmPopup(message) {
document.getElementById("alarmMessage").textContent = message;
document.getElementById("alarmOverlay").style.display = "block";
document.getElementById("alarmSound").play();
addNotification(message);
if (userOptions.enableDesktopNotifications && "Notification" in window) {
if (Notification.permission === "granted") {
new Notification("Crypto Price Alarm", {body: message});
} else if (Notification.permission !== "denied") {
Notification.requestPermission().then((permission) => {
if (permission === "granted") {
new Notification("Crypto Price Alarm", {body: message});
}
});
}
}
}
function closeAlarmPopup() {
document.getElementById("alarmOverlay").style.display = "none";
}
function copyToClipboard(address) {
if (navigator.clipboard && window.isSecureContext) {
navigator.clipboard
.writeText(address)
.then(() => {
alert("Address is copied to the clipboard.");
})
.catch((err) => {
console.error("Clipboard API Fehler:", err);
alert("Fehler beim Kopieren der Adresse.");
});
} else {
let textArea = document.createElement("textarea");
textArea.value = address;
textArea.style.position = "fixed";
textArea.style.top = "0";
textArea.style.left = "0";
textArea.style.width = "2em";
textArea.style.height = "2em";
textArea.style.padding = "0";
textArea.style.border = "none";
textArea.style.outline = "none";
textArea.style.boxShadow = "none";
textArea.style.background = "transparent";
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
try {
document.execCommand("copy");
alert("Address is copied to the clipboard.");
} catch (err) {
console.error("Fallback Copy Fehler:", err);
alert("Fehler beim Kopieren der Adresse.");
}
document.body.removeChild(textArea);
}
}
window.addEventListener("DOMContentLoaded", init);
async function isBinanceSupported(symbol) {
const url = `https://api.binance.com/api/v3/ticker/24hr?symbol=${symbol}USDT`;
try {
const response = await fetch(url);
if (!response.ok) return false;
const data = await response.json();
if (data.code) return false;
return true;
} catch {
return false;
}
}
async function isOkxSupported(symbol) {
const instId = `${symbol}-USDT`;
const okxUrl = `https://www.okx.com/api/v5/market/ticker?instId=${instId}`;
try {
const response = await fetch(okxUrl);
if (!response.ok) return false;
const data = await response.json();
if (!data.data || !data.data[0]) return false;
return true;
} catch {
return false;
}
}
window.addEventListener("DOMContentLoaded", init);
function copyToClipboard(address) {
if (navigator.clipboard && window.isSecureContext) {
navigator.clipboard
.writeText(address)
.then(() => {
alert("Address is copied to the clipboard.");
})
.catch((err) => {
console.error("Clipboard API Fehler:", err);
alert("Fehler beim Kopieren der Adresse.");
});
} else {
let textArea = document.createElement("textarea");
textArea.value = address;
textArea.style.position = "fixed";
textArea.style.top = "0";
textArea.style.left = "0";
textArea.style.width = "2em";
textArea.style.height = "2em";
textArea.style.padding = "0";
textArea.style.border = "none";
textArea.style.outline = "none";
textArea.style.boxShadow = "none";
textArea.style.background = "transparent";
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
try {
document.execCommand("copy");
alert("Address is copied to the clipboard.");
} catch (err) {
console.error("Fallback Copy Fehler:", err);
alert("Fehler beim Kopieren der Adresse.");
}
document.body.removeChild(textArea);
}
}