Compare commits

...

10 Commits

Author SHA1 Message Date
Gerald-H
2f29f21519
Update README.md 2025-03-09 03:44:07 +01:00
92917dbeaa Update Fix 2025-03-09 03:18:00 +01:00
55d15cf147 fix 2025-03-09 02:35:50 +01:00
5554024131 fix 2025-03-09 02:28:42 +01:00
e297acdcdc fix 2025-03-09 02:25:28 +01:00
9dd80f704c fix 2025-03-09 02:17:34 +01:00
06cfc8244a Merge remote-tracking branch 'origin/main' 2025-03-09 02:13:33 +01:00
f5c7e607df url fix 2025-03-09 02:13:23 +01:00
Gerald-H
23500f0c20
Update update.json 2025-03-09 02:09:23 +01:00
f751559878 Release 1.0.6
- Responsive design addet
- Crypto News Auto Update without Browser Refresh every 3 minutes
- Crypto Price Refresh Reduce from 5 Second to 1 Second
- Docker command updated to saved alarms and registered cryptocurrencies that are still there after a Docker image update
--> docker run -p 3099:3099 -p 5001:5001 -v hodleye_data:/app/data --name hodleye-container hodleye-crypto-tracker

- Bugfix "Edit List" works now
- Addet Update Notification every 5 Days
- re-Design from Crypto-box
- improving the logic of the price display for small cryptocurrencies
2025-03-09 02:07:54 +01:00
13 changed files with 1360 additions and 926 deletions

View File

@ -1,4 +1,4 @@
MIT License
Copyright (c) 2025 Gerald-H Copyright (c) 2025 Gerald-H

View File

@ -4,8 +4,6 @@ A lightweight Docker-based web tool to monitor cryptocurrency prices (via Binanc
<img src="https://github.com/user-attachments/assets/d87ca663-97be-4c22-a0ab-46505fe9c99f" width="800" height="auto"> <img src="https://github.com/user-attachments/assets/d87ca663-97be-4c22-a0ab-46505fe9c99f" width="800" height="auto">
--- ---
## Table of Contents ## Table of Contents
@ -37,28 +35,35 @@ A lightweight Docker-based web tool to monitor cryptocurrency prices (via Binanc
- **Unlimited Alarms**: No cap on the number of alarms you can set. - **Unlimited Alarms**: No cap on the number of alarms you can set.
- **Unlimited Crypto Tracking**: Easily add as many coins as you want. - **Unlimited Crypto Tracking**: Easily add as many coins as you want.
- **Real-Time Price Updates (every 5 seconds)**: Uses Binance and OKX data. - **Real-Time Price Updates (every 1 seconds)**: Uses Binance and OKX data.
- **Crypto News & Economic Calendar**: Stay updated on the latest events affecting the market. - **Crypto News & Economic Calendar**: Stay updated on the latest events affecting the market.
The tool refreshes prices every **5 seconds**, which may introduce a slight delay in alarm triggers if the price quickly touches the threshold in between intervals. The tool refreshes prices every **1 seconds**, which may introduce a slight delay in alarm triggers if the price quickly touches the threshold in between intervals.
--- ---
&nbsp;
## Features ## Features
### Unlimited Alarms & Tracking ### Unlimited Alarms & Tracking
- You can set **as many alarms as you like** — no daily or total limit. - You can set **as many alarms as you like** — no daily or total limit.
- Track **any number of cryptocurrencies** in the list simultaneously. - Track **any number of cryptocurrencies** in the list simultaneously.
<img src="https://github.com/user-attachments/assets/276de04d-dcf6-411a-b550-cbb5104c1579" width="auto" height="400"> <img src="https://github.com/user-attachments/assets/276de04d-dcf6-411a-b550-cbb5104c1579" width="auto" height="400">
&nbsp;
### Price Updates ### Price Updates
- **Binance** and **OKX** are integrated as the primary data sources. - **Binance** and **OKX** are integrated as the primary data sources.
- By default, HodlEye tries Binance first; if that fails or is forced off, it falls back to OKX. - By default, HodlEye tries Binance first; if that fails or is forced off, it falls back to OKX.
- It automatically fetches the current price, 24h open, 1h open, and calculates the 24h and 1h percentage changes every **5 seconds**. - It automatically fetches the current price, 24h open, 1h open, and calculates the 24h and 1h percentage changes every **1 seconds**.
&nbsp;
### Alarm Functionality ### Alarm Functionality
- Set alarms for each coin (e.g., `BTC/USDT`), choosing: - Set alarms for each coin (e.g., `BTC/USDT`), choosing:
- **Alarm Price** (threshold) - **Alarm Price** (threshold)
- **Direction** (Rising, Falling, or Both) - **Direction** (Rising, Falling, or Both)
@ -66,55 +71,62 @@ The tool refreshes prices every **5 seconds**, which may introduce a slight dela
- When triggered, a popup and sound notification appear, with optional desktop notifications. - When triggered, a popup and sound notification appear, with optional desktop notifications.
- **Once** alarms are marked locally in the browser (not removed from the server) so they do not trigger again unless reloaded or manually reset. - **Once** alarms are marked locally in the browser (not removed from the server) so they do not trigger again unless reloaded or manually reset.
&nbsp;
### Crypto News ### Crypto News
- News from multiple RSS sources: - News from multiple RSS sources:
- `https://crypto.news/feed/` - `https://crypto.news/feed/`
- `https://cointelegraph.com/rss` - `https://cointelegraph.com/rss`
- `https://thedefiant.io/api/feed` - `https://thedefiant.io/api/feed`
- `https://newsbtc.com/feed` - `https://newsbtc.com/feed`
- `https://news.bitcoin.com/feed` *(may be inaccessible in certain regions)* - `https://news.bitcoin.com/feed` _(may be inaccessible in certain regions)_
- `https://bitcoinmagazine.com/feed` - `https://bitcoinmagazine.com/feed`
- `https://cryptopanic.com/news/rss/` - `https://cryptopanic.com/news/rss/`
- `https://decrypt.co/feed` - `https://decrypt.co/feed`
- Quickly view and filter recent articles within the built-in News modal. - Quickly view and filter recent articles within the built-in News modal.
<img src="https://github.com/user-attachments/assets/f0727b39-a075-4d50-9600-f53c803d4a1b" width="auto" height="400"> <img src="https://github.com/user-attachments/assets/f0727b39-a075-4d50-9600-f53c803d4a1b" width="auto" height="400">
&nbsp;
### Economic Calendar ### Economic Calendar
- The **Economic Calendar** button opens a modal with an [Investing.com](https://www.investing.com/) iframe, showing major economic events such as central bank decisions and market-impacting announcements. - The **Economic Calendar** button opens a modal with an [Investing.com](https://www.investing.com/) iframe, showing major economic events such as central bank decisions and market-impacting announcements.
<img src="https://github.com/user-attachments/assets/e254301e-9aaa-48d8-84e7-6faa598ca8be" width="600" height="auto"> <img src="https://github.com/user-attachments/assets/e254301e-9aaa-48d8-84e7-6faa598ca8be" width="600" height="auto">
--- ---
&nbsp;
## Installation & Usage ## Installation & Usage
### Requirements ### Requirements
- [Docker](https://www.docker.com/) installed. - [Docker](https://www.docker.com/) installed.
- (Optional) [Docker-Compose](https://docs.docker.com/compose/) if you want a more complex or multi-container setup. - (Optional) [Docker-Compose](https://docs.docker.com/compose/) if you want a more complex or multi-container setup.
### Docker Build & Run ### Docker Build & Run
1. **Clone this repository** 1. **Clone this repository**
```bash ```bash
git clone https://github.com/YourGitHubName/HodlEye.git git clone https://github.com/Gerald-Ha/HodlEye-Crypto-Price-Tracker.git
cd HodlEye cd HodlEye
``` ```
2. **Build the Docker image** 2. **Build the Docker image**
```bash ```bash
docker buildx build -t hodleye-crypto-tracker . docker buildx build -t hodleye-crypto-tracker .
``` ```
*(Make sure youre in the same directory as the Dockerfile.)*
_(Make sure youre in the same directory as the Dockerfile.)_
3. **Run the container** 3. **Run the container**
```bash ```bash
docker run -d -p 3099:3099 -p 5001:5001 --name hodleye-container hodleye-crypto-tracker docker run -p 3099:3099 -p 5001:5001 -v hodleye_data:/app/data --name hodleye-container hodleye-crypto-tracker
``` ```
- Port `3099` serves the main web interface. - Port `3099` serves the main web interface.
- Port `5001` is used by the Node.js server that fetches news RSS feeds. - Port `5001` is used by the Node.js server that fetches news RSS feeds.
@ -123,37 +135,48 @@ The tool refreshes prices every **5 seconds**, which may introduce a slight dela
- **News Feed Endpoint**: [http://localhost:5001/api/news](http://localhost:5001/api/news) - **News Feed Endpoint**: [http://localhost:5001/api/news](http://localhost:5001/api/news)
--- ---
&nbsp;
## Windows Notification App: HodlEye_Notify ## Windows Notification App: HodlEye_Notify
<img src="https://github.com/user-attachments/assets/a3356708-1b3a-4fb8-9a71-d1af88f29c5f" width="auto" height="150"> <img src="https://github.com/user-attachments/assets/a3356708-1b3a-4fb8-9a71-d1af88f29c5f" width="auto" height="150">
<img src="https://github.com/user-attachments/assets/cb0eb2a8-45fb-4a80-bb81-56957e838153" width="auto" height="150"> <img src="https://github.com/user-attachments/assets/cb0eb2a8-45fb-4a80-bb81-56957e838153" width="auto" height="150">
If you prefer not to keep the HodlEye Crypto Price Tracker web interface open in your browser all the time, you can use a lightweight Windows application called **HodlEye_Notify**. This tool connects directly to the same endpoint as the Docker container and will display notifications on your desktop whenever an alarm is triggered. If you prefer not to keep the HodlEye Crypto Price Tracker web interface open in your browser all the time, you can use a lightweight Windows application called **HodlEye_Notify**. This tool connects directly to the same endpoint as the Docker container and will display notifications on your desktop whenever an alarm is triggered.
&nbsp;
1. **Setup** 1. **Setup**
- Enter the IP address and port of your HodlEye Docker container (for example, `http://192.168.1.112:3099/`) in the HodlEye_Notify window. - Enter the IP address and port of your HodlEye Docker container (for example, `http://192.168.1.112:3099/`) in the HodlEye_Notify window.
- Click **Connect** to establish a WebSocket connection. - Click **Connect** to establish a WebSocket connection.
- Once connected, youll see the status change to “Connected.” - Once connected, youll see the status change to “Connected.”
2. **Autostart** 2. **Autostart**
- Add HodlEye_Notify to your Windows **Startup** folder so it automatically launches when Windows starts. This way, youll continuously receive notifications without needing to reopen the program manually. - Add HodlEye_Notify to your Windows **Startup** folder so it automatically launches when Windows starts. This way, youll continuously receive notifications without needing to reopen the program manually.
3. **Testing Notifications** 3. **Testing Notifications**
- From the machine running the Docker container, you can trigger a test notification using the following `curl` command: - From the machine running the Docker container, you can trigger a test notification using the following `curl` command:
**Ubuntu**
```bash ```bash
curl -X POST http://192.168.1.112:3099/api/notifications \ curl -X POST http://192.168.1.112:3099/api/notifications \
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
-d "{\"message\": \"⚠️ ALARM (Recurring, Both): BTC reached 92250\", \"timestamp\": \"2025-03-06T06:19:58.584Z\"}" -d "{\"message\": \"⚠️ ALARM (Recurring, Both): BTC reached 92250\", \"timestamp\": \"2025-03-06T06:19:58.584Z\"}"
``` ```
**Windows CMD**
```bash
curl -X POST http://192.168.1.112:3099/api/notifications -H "Content-Type: application/json" -d "{\"message\": \"⚠️ ALARM (Recurring, Both): BTC reached 92250\", \"timestamp\": \"2025-03-06T06:19:58.584Z\"}"
```
- If everything is configured correctly, you should receive a desktop notification from HodlEye_Notify indicating the alarm has triggered. - If everything is configured correctly, you should receive a desktop notification from HodlEye_Notify indicating the alarm has triggered.
This application simplifies the process of staying informed about your alarms, letting you work on other tasks without leaving the HodlEye web interface open. This application simplifies the process of staying informed about your alarms, letting you work on other tasks without leaving the HodlEye web interface open.
--- ---
&nbsp;
## Project Structure ## Project Structure
Below is an example directory tree (based on your structure). Yours may vary slightly: Below is an example directory tree (based on your structure). Yours may vary slightly:
@ -191,7 +214,9 @@ HodlEye-Crypto-Price-Tracker
└── ping.mp3 └── ping.mp3
``` ```
&nbsp;
### Frontend (`index.html` & `magic.js`) ### Frontend (`index.html` & `magic.js`)
- **`index.html`** - **`index.html`**
- Main interface containing modals and buttons (Add Crypto, Edit List, Alarms, Options, etc.). - Main interface containing modals and buttons (Add Crypto, Edit List, Alarms, Options, etc.).
- Includes buttons for: - Includes buttons for:
@ -204,11 +229,13 @@ HodlEye-Crypto-Price-Tracker
- Core logic: - Core logic:
- Fetches cryptos (`/api/cryptos`) - Fetches cryptos (`/api/cryptos`)
- Loads alarms (`/api/alarms`) and notifications (`/api/notifications`) - Loads alarms (`/api/alarms`) and notifications (`/api/notifications`)
- Pulls prices from Binance/OKX every 5 seconds - Pulls prices from Binance/OKX every 1 seconds
- Checks and triggers alarms - Checks and triggers alarms
- Handles UI rendering (prices, alarms, notifications, drag & drop reorder) - Handles UI rendering (prices, alarms, notifications, drag & drop reorder)
&nbsp;
### News Feed Server (Node.js) ### News Feed Server (Node.js)
- A minimal Node.js Express server (in `server.js` or similar) which: - A minimal Node.js Express server (in `server.js` or similar) which:
- Retrieves the listed RSS feeds and parses them via `xml2js` - Retrieves the listed RSS feeds and parses them via `xml2js`
- Serves them in JSON format at `/api/news` - Serves them in JSON format at `/api/news`
@ -221,20 +248,24 @@ HodlEye-Crypto-Price-Tracker
``` ```
- Accessible at [http://localhost:5001/api/news](http://localhost:5001/api/news). - Accessible at [http://localhost:5001/api/news](http://localhost:5001/api/news).
*(Within Docker, its already bundled, so just expose `5001`.)* _(Within Docker, its already bundled, so just expose `5001`.)_
--- ---
&nbsp;
## Important Notes / Limitations ## Important Notes / Limitations
1. **5-second polling** 1. **1-second polling**
- Theres a potential delay in alarms because price thresholds are only checked every 5 seconds. If a price briefly touches and moves away between polls, you might miss that exact trigger moment.
- Theres a potential delay in alarms because price thresholds are only checked every 1 seconds. If a price briefly touches and moves away between polls, you might miss that exact trigger moment.
2. **API availability** 2. **API availability**
- Binance/OKX may be temporarily down or might not support certain symbols. - Binance/OKX may be temporarily down or might not support certain symbols.
- HodlEye tries Binance → fallback to OKX if needed. - HodlEye tries Binance → fallback to OKX if needed.
3. **Unlimited Alarms (Once vs. Recurring)** 3. **Unlimited Alarms (Once vs. Recurring)**
- **Once** alarms become locally “triggered” to avoid repeated alerts but are not server-side deactivated. - **Once** alarms become locally “triggered” to avoid repeated alerts but are not server-side deactivated.
- **Recurring** triggers repeatedly every time the threshold is crossed. - **Recurring** triggers repeatedly every time the threshold is crossed.
@ -244,6 +275,7 @@ HodlEye-Crypto-Price-Tracker
--- ---
&nbsp;
## Coming Soon ## Coming Soon
Exciting new features and improvements are on the way! Here are some planned updates: Exciting new features and improvements are on the way! Here are some planned updates:
@ -252,20 +284,24 @@ Exciting new features and improvements are on the way! Here are some planned upd
- **More Integrations**: Expanding support for additional exchanges and data sources. - **More Integrations**: Expanding support for additional exchanges and data sources.
- **Automatic alarm in the event of a sharp price drop of 10% within 1 hour** - **Automatic alarm in the event of a sharp price drop of 10% within 1 hour**
- **HodlEye Notify Alarm with various sound selections and HodlEye Alarms** - **HodlEye Notify Alarm with various sound selections and HodlEye Alarms**
- **Adding more Responsive design**
Stay tuned for updates! Stay tuned for updates!
---
--- ---
&nbsp;
## Privacy & Data Disclaimer ## Privacy & Data Disclaimer
- **No Data Collection by This Application**: HodlEye itself does not collect, store, or process any personal data or usage analytics. - **No Data Collection by This Application**: HodlEye itself does not collect, store, or process any personal data or usage analytics.
- **External Services**: Certain features (e.g., news feeds, iframes) rely on third-party websites or APIs. We do not control and are not responsible for the data-collection practices or privacy policies of these external providers. Please refer to the privacy policies of those services for details. - **External Services**: Certain features (e.g., news feeds, iframes) rely on third-party websites or APIs. We do not control and are not responsible for the data-collection practices or privacy policies of these external providers. Please refer to the privacy policies of those services for details.
--- ---
&nbsp;

8
data/data.json Normal file
View File

@ -0,0 +1,8 @@
{
"cryptos": [
"BTC",
"ETH"
],
"alarms": [],
"notifications": []
}

View File

@ -0,0 +1 @@
<svg version="1.1" id="main_outline" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" style="enable-background:new 0 0 640 640;" xml:space="preserve" viewBox="5.67 143.05 628.65 387.55"> <g> <path id="teabag" style="fill:#FFFFFF" d="M395.9,484.2l-126.9-61c-12.5-6-17.9-21.2-11.8-33.8l61-126.9c6-12.5,21.2-17.9,33.8-11.8 c17.2,8.3,27.1,13,27.1,13l-0.1-109.2l16.7-0.1l0.1,117.1c0,0,57.4,24.2,83.1,40.1c3.7,2.3,10.2,6.8,12.9,14.4 c2.1,6.1,2,13.1-1,19.3l-61,126.9C423.6,484.9,408.4,490.3,395.9,484.2z"></path> <g> <g> <path style="fill:#609926" d="M622.7,149.8c-4.1-4.1-9.6-4-9.6-4s-117.2,6.6-177.9,8c-13.3,0.3-26.5,0.6-39.6,0.7c0,39.1,0,78.2,0,117.2 c-5.5-2.6-11.1-5.3-16.6-7.9c0-36.4-0.1-109.2-0.1-109.2c-29,0.4-89.2-2.2-89.2-2.2s-141.4-7.1-156.8-8.5 c-9.8-0.6-22.5-2.1-39,1.5c-8.7,1.8-33.5,7.4-53.8,26.9C-4.9,212.4,6.6,276.2,8,285.8c1.7,11.7,6.9,44.2,31.7,72.5 c45.8,56.1,144.4,54.8,144.4,54.8s12.1,28.9,30.6,55.5c25,33.1,50.7,58.9,75.7,62c63,0,188.9-0.1,188.9-0.1s12,0.1,28.3-10.3 c14-8.5,26.5-23.4,26.5-23.4s12.9-13.8,30.9-45.3c5.5-9.7,10.1-19.1,14.1-28c0,0,55.2-117.1,55.2-231.1 C633.2,157.9,624.7,151.8,622.7,149.8z M125.6,353.9c-25.9-8.5-36.9-18.7-36.9-18.7S69.6,321.8,60,295.4 c-16.5-44.2-1.4-71.2-1.4-71.2s8.4-22.5,38.5-30c13.8-3.7,31-3.1,31-3.1s7.1,59.4,15.7,94.2c7.2,29.2,24.8,77.7,24.8,77.7 S142.5,359.9,125.6,353.9z M425.9,461.5c0,0-6.1,14.5-19.6,15.4c-5.8,0.4-10.3-1.2-10.3-1.2s-0.3-0.1-5.3-2.1l-112.9-55 c0,0-10.9-5.7-12.8-15.6c-2.2-8.1,2.7-18.1,2.7-18.1L322,273c0,0,4.8-9.7,12.2-13c0.6-0.3,2.3-1,4.5-1.5c8.1-2.1,18,2.8,18,2.8 l110.7,53.7c0,0,12.6,5.7,15.3,16.2c1.9,7.4-0.5,14-1.8,17.2C474.6,363.8,425.9,461.5,425.9,461.5z"></path> <path style="fill:#609926" d="M326.8,380.1c-8.2,0.1-15.4,5.8-17.3,13.8c-1.9,8,2,16.3,9.1,20c7.7,4,17.5,1.8,22.7-5.4 c5.1-7.1,4.3-16.9-1.8-23.1l24-49.1c1.5,0.1,3.7,0.2,6.2-0.5c4.1-0.9,7.1-3.6,7.1-3.6c4.2,1.8,8.6,3.8,13.2,6.1 c4.8,2.4,9.3,4.9,13.4,7.3c0.9,0.5,1.8,1.1,2.8,1.9c1.6,1.3,3.4,3.1,4.7,5.5c1.9,5.5-1.9,14.9-1.9,14.9 c-2.3,7.6-18.4,40.6-18.4,40.6c-8.1-0.2-15.3,5-17.7,12.5c-2.6,8.1,1.1,17.3,8.9,21.3c7.8,4,17.4,1.7,22.5-5.3 c5-6.8,4.6-16.3-1.1-22.6c1.9-3.7,3.7-7.4,5.6-11.3c5-10.4,13.5-30.4,13.5-30.4c0.9-1.7,5.7-10.3,2.7-21.3 c-2.5-11.4-12.6-16.7-12.6-16.7c-12.2-7.9-29.2-15.2-29.2-15.2s0-4.1-1.1-7.1c-1.1-3.1-2.8-5.1-3.9-6.3c4.7-9.7,9.4-19.3,14.1-29 c-4.1-2-8.1-4-12.2-6.1c-4.8,9.8-9.7,19.7-14.5,29.5c-6.7-0.1-12.9,3.5-16.1,9.4c-3.4,6.3-2.7,14.1,1.9,19.8 C343.2,346.5,335,363.3,326.8,380.1z"></path> </g> </g> </g> </svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@ -0,0 +1 @@
<svg width="98" height="96" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M48.854 0C21.839 0 0 22 0 49.217c0 21.756 13.993 40.172 33.405 46.69 2.427.49 3.316-1.059 3.316-2.362 0-1.141-.08-5.052-.08-9.127-13.59 2.934-16.42-5.867-16.42-5.867-2.184-5.704-5.42-7.17-5.42-7.17-4.448-3.015.324-3.015.324-3.015 4.934.326 7.523 5.052 7.523 5.052 4.367 7.496 11.404 5.378 14.235 4.074.404-3.178 1.699-5.378 3.074-6.6-10.839-1.141-22.243-5.378-22.243-24.283 0-5.378 1.94-9.778 5.014-13.2-.485-1.222-2.184-6.275.486-13.038 0 0 4.125-1.304 13.426 5.052a46.97 46.97 0 0 1 12.214-1.63c4.125 0 8.33.571 12.213 1.63 9.302-6.356 13.427-5.052 13.427-5.052 2.67 6.763.97 11.816.485 13.038 3.155 3.422 5.015 7.822 5.015 13.2 0 18.905-11.404 23.06-22.324 24.283 1.78 1.548 3.316 4.481 3.316 9.126 0 6.6-.08 11.897-.08 13.526 0 1.304.89 2.853 3.316 2.364 19.412-6.52 33.405-24.935 33.405-46.691C97.707 22 75.788 0 48.854 0z" fill="#24292f"/></svg>

After

Width:  |  Height:  |  Size: 963 B

View File

@ -1,97 +1,110 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>HodlEye Crypto Price Tracker</title> <title>HodlEye Crypto Price Tracker</title>
<link rel="stylesheet" href="style.css"> <link rel="stylesheet" href="style.css" />
<link rel="icon" type="image/png" href="images/favicon.png"> <link rel="stylesheet" href="responsive.css" />
<link rel="icon" type="image/png" href="images/favicon.png" />
</head> </head>
<body class="dark"> <body class="dark">
<div class="header"> <div class="header">
<h1>HodlEye Crypto Price Tracker</h1> <h1>HodlEye Crypto Price Tracker</h1>
<div class="button-grid-container"> <div class="button-grid-container">
<div class="grid-left"> <div class="grid-left">
<button onclick="openCryptoNews()">Crypto News</button> <button onclick="openCryptoNews()">Crypto News</button>
<button onclick="openEconomicCalendar()">Economic Calendar</button> <button onclick="openEconomicCalendar()">Economic Calendar</button>
</div> </div>
<div class="grid-middle"> <div class="grid-middle">
<button onclick="openAddCryptoModal()">Add Crypto</button> <button onclick="openAddCryptoModal()">Add Crypto</button>
<button onclick="toggleEditMode()" id="editButton">Edit List</button> <button onclick="toggleEditMode()" id="editButton">Edit List</button>
<button class="alarm-btn" onclick="openAlarmModal()">Alarms⏰</button> <button class="alarm-btn" onclick="openAlarmModal()">Alarms</button>
<button class="options-btn" onclick="openOptionsModal()">Options</button> <button class="options-btn" onclick="openOptionsModal()">
</div> Options
<div class="grid-right"> </button>
</div> </div>
<div class="grid-right"></div>
</div> </div>
</div> </div>
<div class="main-container"> <div class="main-container">
<div class="left-column"> <div class="left-column">
<div class="grid-container" id="cryptoGrid"> <div class="grid-container" id="cryptoGrid"></div>
</div> </div>
</div>
<div class="right-column"> <div class="right-column">
<div class="notify-area">
<div class="notify-heading"> <div class="notify-heading">
<span>Notify</span> <span>Notify</span>
<button onclick="clearNotifications()">Clear List</button> <button onclick="clearNotifications()">Clear List</button>
</div> </div>
<ul class="notify-list" id="notifyList"> <ul class="notify-list" id="notifyList"></ul>
</div>
</ul>
<div class="buttons-and-version">
<button class="coffee-btn" onclick="openBuyMeModal()"> <button class="coffee-btn" onclick="openBuyMeModal()">
Buy me a Coffee <img src="images/coffee.svg" alt="Coffee" class="coffee-icon"> Buy me a Coffee
<img src="images/coffee.svg" alt="Coffee" class="coffee-icon" />
</button> </button>
<button
class="my-button"
onclick="window.open('https://github.com/Gerald-Ha/HodlEye-Crypto-Price-Tracker', '_blank')"
>
Github
<img
src="images/github-mark.svg"
alt="Git Logo"
class="coffee-icon"
/>
</button>
<button onclick="window.open('https://www.gerald-hasani.com', '_blank')">Developer</button> <div class="version-info">
<span id="currentVersion">Version 1.0.6</span>
<span
id="updateAvailable"
style="display: none; color: red; cursor: pointer"
>
Update Available
</span>
</div>
</div>
</div> </div>
</div> </div>
<div id="alarmModal" class="modal"> <div id="alarmModal" class="modal">
<div class="modal-content"> <div class="modal-content">
<span class="close" onclick="closeAlarmModal()">&times;</span> <span class="close" onclick="closeAlarmModal()">&times;</span>
<h2>Price Alarms</h2> <h2>Price Alarms</h2>
<div class="alarm-list-container" id="alarmListContainer"> <div class="alarm-list-container" id="alarmListContainer"></div>
</div>
<label for="alarmSymbol">Symbol:</label> <label for="alarmSymbol">Symbol:</label>
<select id="alarmSymbol"></select><br> <select id="alarmSymbol"></select
><br />
<label for="alarmPrice">Alarm Price (USDT):</label> <label for="alarmPrice">Alarm Price (USDT):</label>
<input type="number" id="alarmPrice" placeholder="Price"><br> <input type="number" id="alarmPrice" placeholder="Price" /><br />
<label for="alarmFrequency">Frequency:</label> <label for="alarmFrequency">Frequency:</label>
<select id="alarmFrequency"> <select id="alarmFrequency">
<option value="Once">Once</option> <option value="Once">Once</option>
<option value="Recurring">Recurring</option> <option value="Recurring">Recurring</option></select
</select><br> ><br />
<label for="alarmDirection">Direction:</label> <label for="alarmDirection">Direction:</label>
<select id="alarmDirection"> <select id="alarmDirection">
<option value="Rising">Rising</option> <option value="Rising">Rising</option>
<option value="Falling">Falling</option> <option value="Falling">Falling</option>
<option value="Both">Both</option> <option value="Both">Both</option></select
</select><br> ><br />
<button onclick="addAlarm()">Add Alarm</button> <button onclick="addAlarm()">Add Alarm</button>
</div> </div>
</div> </div>
<div id="optionsModal" class="modal"> <div id="optionsModal" class="modal">
<div class="modal-content"> <div class="modal-content">
<span class="close" onclick="closeOptionsModal()">&times;</span> <span class="close" onclick="closeOptionsModal()">&times;</span>
@ -102,60 +115,63 @@
<option value="ping.mp3">ping.mp3</option> <option value="ping.mp3">ping.mp3</option>
<option value="cashing.mp3">cashing.mp3</option> <option value="cashing.mp3">cashing.mp3</option>
</select> </select>
<br><br> <br /><br />
<input type="checkbox" id="darkModeToggle"> <input type="checkbox" id="darkModeToggle" />
<label for="darkModeToggle">Enable Dark Mode</label> <label for="darkModeToggle">Enable Dark Mode</label>
<br><br> <br /><br />
<input type="checkbox" id="desktopNotifyToggle"> <input type="checkbox" id="desktopNotifyToggle" />
<label for="desktopNotifyToggle">Enable Desktop Notifications</label> <label for="desktopNotifyToggle">Enable Desktop Notifications</label>
<br><br> <br /><br />
<button onclick="openApiModal()">ADD/Edit OKX API</button> <button onclick="openApiModal()">ADD/Edit OKX API</button>
<br><br> <br /><br />
<button onclick="saveOptions()">Save</button> <button onclick="saveOptions()">Save</button>
</div> </div>
</div> </div>
<div id="apiModal" class="modal"> <div id="apiModal" class="modal">
<div class="modal-content"> <div class="modal-content">
<span class="close" onclick="closeApiModal()">&times;</span> <span class="close" onclick="closeApiModal()">&times;</span>
<h2>OKX API Settings</h2> <h2>OKX API Settings</h2>
<p>Enter your OKX API data here (optional). Primary is Binance. <p>
If Binance fails, the site tries OKX (or if you force OKX).</p> Enter your OKX API data here (optional). Primary is Binance. If
Binance fails, the site tries OKX (or if you force OKX).
</p>
<label for="okxApiKey">OKX API-Key:</label><br> <label for="okxApiKey">OKX API-Key:</label><br />
<input type="text" id="okxApiKey" placeholder="e.g. c42166aa-..."> <input type="text" id="okxApiKey" placeholder="e.g. c42166aa-..." />
<br> <br />
<label for="okxSecretKey">OKX Secret-Key:</label><br> <label for="okxSecretKey">OKX Secret-Key:</label><br />
<input type="text" id="okxSecretKey" placeholder="e.g. ACD0B07F..."> <input type="text" id="okxSecretKey" placeholder="e.g. ACD0B07F..." />
<br> <br />
<label for="okxPassphrase">OKX Passphrase (if needed):</label><br> <label for="okxPassphrase">OKX Passphrase (if needed):</label><br />
<input type="text" id="okxPassphrase" placeholder="(optional)"> <input type="text" id="okxPassphrase" placeholder="(optional)" />
<br><br> <br /><br />
<button onclick="saveApiSettings()">Save</button> <button onclick="saveApiSettings()">Save</button>
</div> </div>
</div> </div>
<div id="addCryptoModal" class="modal"> <div id="addCryptoModal" class="modal">
<div class="modal-content"> <div class="modal-content">
<span class="close" onclick="closeAddCryptoModal()">&times;</span> <span class="close" onclick="closeAddCryptoModal()">&times;</span>
<h2>Add a New Cryptocurrency</h2> <h2>Add a New Cryptocurrency</h2>
<label for="newCryptoSymbol">Symbol (e.g. ETH):</label> <label for="newCryptoSymbol">Symbol (e.g. ETH):</label>
<input type="text" id="newCryptoSymbol" placeholder="BTC, ETH, ADA..."><br> <input
<br><br> type="text"
id="newCryptoSymbol"
placeholder="BTC, ETH, ADA..."
/><br />
<br /><br />
<button onclick="addNewCrypto()">Add</button> <button onclick="addNewCrypto()">Add</button>
</div> </div>
</div> </div>
<div id="apiSelectModal" class="modal"> <div id="apiSelectModal" class="modal">
<div class="modal-content"> <div class="modal-content">
<span class="close" onclick="closeApiSelectModal()">&times;</span> <span class="close" onclick="closeApiSelectModal()">&times;</span>
@ -166,30 +182,37 @@
<option value="binance">Force Binance</option> <option value="binance">Force Binance</option>
<option value="okx">Force OKX</option> <option value="okx">Force OKX</option>
</select> </select>
<br><br> <br /><br />
<button onclick="saveApiSelection()">Save</button> <button onclick="saveApiSelection()">Save</button>
</div> </div>
</div> </div>
<div id="buyMeModal" class="modal"> <div id="buyMeModal" class="modal">
<div class="modal-content"> <div class="modal-content">
<span class="close" onclick="closeBuyMeModal()">&times;</span> <span class="close" onclick="closeBuyMeModal()">&times;</span>
<h2>Buy me a Coffee</h2> <h2>Buy me a Coffee</h2>
<p><strong>Send me:</strong> USDT / Ethereum</p> <p><strong>Send me:</strong> USDT / Ethereum</p>
<p><strong>Chain:</strong> <p>
<span style="text-decoration: underline; cursor: pointer;" <strong>Chain:</strong>
onclick="copyToClipboard('0x26c2E3F6C854Af006520ec2ce433982866bB7632')"> <span
style="text-decoration: underline; cursor: pointer"
onclick="copyToClipboard('0x26c2E3F6C854Af006520ec2ce433982866bB7632')"
>
ETH ETH
</span> / </span>
<span style="text-decoration: underline; cursor: pointer;" /
onclick="copyToClipboard('0x26c2E3F6C854Af006520ec2ce433982866bB7632')"> <span
style="text-decoration: underline; cursor: pointer"
onclick="copyToClipboard('0x26c2E3F6C854Af006520ec2ce433982866bB7632')"
>
BSC BSC
</span> </span>
</p> </p>
<p> <p>
<span style="text-decoration: underline; cursor: pointer;" <span
onclick="copyToClipboard('0x26c2E3F6C854Af006520ec2ce433982866bB7632')"> style="text-decoration: underline; cursor: pointer"
onclick="copyToClipboard('0x26c2E3F6C854Af006520ec2ce433982866bB7632')"
>
0x26c2E3F6C854Af006520ec2ce433982866bB7632 0x26c2E3F6C854Af006520ec2ce433982866bB7632
</span> </span>
</p> </p>
@ -197,7 +220,6 @@
</div> </div>
</div> </div>
<div id="alarmOverlay" class="alert-overlay"> <div id="alarmOverlay" class="alert-overlay">
<div class="alert-box"> <div class="alert-box">
<div id="alarmMessage"></div> <div id="alarmMessage"></div>
@ -205,7 +227,6 @@
</div> </div>
</div> </div>
<div id="errorOverlay" class="error-overlay"> <div id="errorOverlay" class="error-overlay">
<div class="error-box"> <div class="error-box">
<p id="errorMessage"></p> <p id="errorMessage"></p>
@ -213,46 +234,62 @@
</div> </div>
</div> </div>
<audio id="alarmSound"> <audio id="alarmSound">
<source src="sound/ping.mp3" type="audio/mpeg"> <source src="sound/ping.mp3" type="audio/mpeg" />
</audio> </audio>
<div id="cryptoNewsModal" class="modal"> <div id="cryptoNewsModal" class="modal">
<div class="modal-content"> <div class="modal-content">
<span class="close" onclick="closeCryptoNewsModal()">&times;</span> <span class="close" onclick="closeCryptoNewsModal()">&times;</span>
<h2>Crypto News</h2> <h2>Crypto News</h2>
<input type="text" id="search" placeholder="Suche nach Artikeln..."> <input type="text" id="search" placeholder="Suche nach Artikeln..." />
<div id="news-feed">Lade News...</div> <div id="news-feed">Lade News...</div>
</div> </div>
</div> </div>
<div id="economicCalendarModal" class="modal"> <div id="economicCalendarModal" class="modal">
<div class="modal-content"> <div class="modal-content">
<span class="close" onclick="closeEconomicCalendarModal()">&times;</span> <span class="close" onclick="closeEconomicCalendarModal()"
>&times;</span
>
<h2>Economic Calendar</h2> <h2>Economic Calendar</h2>
<div class="iframe-container"> <div class="iframe-container">
<div class="iframe-wrapper"> <div class="iframe-wrapper">
<iframe <iframe
src="https://sslecal2.investing.com?columns=exc_flags,exc_currency,exc_importance,exc_actual,exc_forecast,exc_previous&features=datepicker,timezone&countries=25,32,6,37,72,22,17,39,14,10,35,43,56,36,110,11,26,12,4,5&calType=week&timeZone=8&lang=1" src="https://sslecal2.investing.com?columns=exc_flags,exc_currency,exc_importance,exc_actual,exc_forecast,exc_previous&features=datepicker,timezone&countries=25,32,6,37,72,22,17,39,14,10,35,43,56,36,110,11,26,12,4,5&calType=week&timeZone=8&lang=1"
allowtransparency="true"> allowtransparency="true"
>
</iframe> </iframe>
</div> </div>
<div class="poweredBy"> <div class="poweredBy">
<span>Real Time Economic Calendar provided by <span
<a href="https://www.investing.com/" rel="nofollow" target="_blank" class="underline_link">Investing.com</a>. >Real Time Economic Calendar provided by
<a
href="https://www.investing.com/"
rel="nofollow"
target="_blank"
class="underline_link"
>Investing.com</a
>.
</span> </span>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div id="updateModal" class="modal">
<div class="modal-content">
<span class="close" onclick="closeUpdateModal()">&times;</span>
<h2 id="updateTitle">Update auf <span id="updateVersion"></span></h2>
<p id="updateChanges"></p>
<button onclick="performUpdate()">Update</button>
</div>
</div>
<script src="magic.js"></script> <script src="magic.js"></script>
<script src="news.js"></script> <script src="news.js"></script>
<script src="update.js"></script>
</body> </body>
</html> </html>

View File

@ -1,30 +1,22 @@
let cryptoList = []; let cryptoList = [];
let alarms = []; let alarms = [];
let notifications = []; let notifications = [];
let lastPrices = {}; let lastPrices = {};
let userOptions = JSON.parse(localStorage.getItem("userOptions")) || { let userOptions = JSON.parse(localStorage.getItem("userOptions")) || {
soundFile: "ping.mp3", soundFile: "ping.mp3",
darkMode: true, darkMode: true,
enableDesktopNotifications: false, enableDesktopNotifications: false,
okxApiKey: "", okxApiKey: "",
okxSecretKey: "", okxSecretKey: "",
okxPassphrase: "" okxPassphrase: "",
}; };
let apiPreference = JSON.parse(localStorage.getItem("apiPreference")) || {}; let apiPreference = JSON.parse(localStorage.getItem("apiPreference")) || {};
let editMode = false; let editMode = false;
let currentApiSelectSymbol = null; let currentApiSelectSymbol = null;
async function loadCryptosFromServer() { async function loadCryptosFromServer() {
try { try {
const resp = await fetch("/api/cryptos"); const resp = await fetch("/api/cryptos");
@ -39,19 +31,17 @@
const newCrypto = document.getElementById("newCryptoSymbol").value.trim().toUpperCase(); const newCrypto = document.getElementById("newCryptoSymbol").value.trim().toUpperCase();
if (!newCrypto) return; if (!newCrypto) return;
if (!(await isBinanceSupported(newCrypto)) && !(await isOkxSupported(newCrypto))) { if (!(await isBinanceSupported(newCrypto)) && !(await isOkxSupported(newCrypto))) {
showErrorMessage("This cryptocurrency is not supported on Binance or OKX (USDT)."); showErrorMessage("This cryptocurrency is not supported on Binance or OKX (USDT).");
closeAddCryptoModal(); closeAddCryptoModal();
return; return;
} }
try { try {
const resp = await fetch("/api/cryptos", { const resp = await fetch("/api/cryptos", {
method: "POST", method: "POST",
headers: {"Content-Type": "application/json"}, headers: {"Content-Type": "application/json"},
body: JSON.stringify({ symbol: newCrypto }) body: JSON.stringify({symbol: newCrypto}),
}); });
const updatedList = await resp.json(); const updatedList = await resp.json();
cryptoList = updatedList; cryptoList = updatedList;
@ -67,14 +57,12 @@
if (!symbol) return; if (!symbol) return;
try { try {
await fetch(`/api/cryptos/${symbol}`, {method: "DELETE"}); await fetch(`/api/cryptos/${symbol}`, {method: "DELETE"});
loadCryptosFromServer(); loadCryptosFromServer();
} catch (err) { } catch (err) {
showErrorMessage("Error deleting crypto: " + err.message); showErrorMessage("Error deleting crypto: " + err.message);
} }
} }
async function loadAlarmsFromServer() { async function loadAlarmsFromServer() {
try { try {
const resp = await fetch("/api/alarms"); const resp = await fetch("/api/alarms");
@ -94,13 +82,11 @@
if (!symbol || isNaN(price)) return; if (!symbol || isNaN(price)) return;
try { try {
await fetch("/api/alarms", { await fetch("/api/alarms", {
method: "POST", method: "POST",
headers: {"Content-Type": "application/json"}, headers: {"Content-Type": "application/json"},
body: JSON.stringify({ symbol, price, frequency, direction }) body: JSON.stringify({symbol, price, frequency, direction}),
}); });
loadAlarmsFromServer(); loadAlarmsFromServer();
} catch (err) { } catch (err) {
showErrorMessage("Error adding alarm: " + err.message); showErrorMessage("Error adding alarm: " + err.message);
@ -116,7 +102,6 @@
} }
} }
async function loadNotificationsFromServer() { async function loadNotificationsFromServer() {
try { try {
const resp = await fetch("/api/notifications"); const resp = await fetch("/api/notifications");
@ -128,12 +113,11 @@
} }
async function addNotification(msg) { async function addNotification(msg) {
try { try {
await fetch("/api/notifications", { await fetch("/api/notifications", {
method: "POST", method: "POST",
headers: {"Content-Type": "application/json"}, headers: {"Content-Type": "application/json"},
body: JSON.stringify({ message: msg }) body: JSON.stringify({message: msg}),
}); });
loadNotificationsFromServer(); loadNotificationsFromServer();
} catch (err) { } catch (err) {
@ -142,7 +126,6 @@
} }
async function clearNotifications() { async function clearNotifications() {
try { try {
await fetch("/api/notifications", {method: "DELETE"}); await fetch("/api/notifications", {method: "DELETE"});
loadNotificationsFromServer(); loadNotificationsFromServer();
@ -151,38 +134,29 @@
} }
} }
async function init() { async function init() {
document.getElementById("alarmSound").src = "sound/" + userOptions.soundFile; document.getElementById("alarmSound").src = "sound/" + userOptions.soundFile;
updateTheme(userOptions.darkMode); updateTheme(userOptions.darkMode);
await loadCryptosFromServer(); await loadCryptosFromServer();
await loadAlarmsFromServer(); await loadAlarmsFromServer();
await loadNotificationsFromServer(); await loadNotificationsFromServer();
setInterval(() => { setInterval(() => {
cryptoList.forEach((symbol, index) => { cryptoList.forEach((symbol, index) => {
const elementId = "crypto-" + index; const elementId = "crypto-" + index;
fetchCryptoData(symbol, elementId) fetchCryptoData(symbol, elementId).catch(() => setNotSupported(elementId));
.catch(() => setNotSupported(elementId));
}); });
}, 5000); }, 1000);
renderNotifications(); renderNotifications();
} }
function renderCryptoGrid() { function renderCryptoGrid() {
const grid = document.getElementById("cryptoGrid"); const grid = document.getElementById("cryptoGrid");
grid.innerHTML = ""; grid.innerHTML = "";
cryptoList.forEach((symbol, index) => { cryptoList.forEach((symbol, index) => {
const boxId = "crypto-" + index; const boxId = "crypto-" + index;
const box = document.createElement("div"); const box = document.createElement("div");
box.className = "crypto-box"; box.className = "crypto-box";
box.id = boxId; box.id = boxId;
@ -216,7 +190,6 @@
change1h.textContent = "1h Change: -"; change1h.textContent = "1h Change: -";
box.appendChild(change1h); box.appendChild(change1h);
const apiLabel = document.createElement("div"); const apiLabel = document.createElement("div");
apiLabel.id = "api-" + boxId; apiLabel.id = "api-" + boxId;
apiLabel.className = "api-label"; apiLabel.className = "api-label";
@ -226,7 +199,6 @@
}); });
box.appendChild(apiLabel); box.appendChild(apiLabel);
if (editMode) { if (editMode) {
box.draggable = true; box.draggable = true;
box.setAttribute("data-index", index); box.setAttribute("data-index", index);
@ -248,8 +220,6 @@
} }
grid.appendChild(box); grid.appendChild(box);
fetchCryptoData(symbol, boxId).catch(() => setNotSupported(boxId)); fetchCryptoData(symbol, boxId).catch(() => setNotSupported(boxId));
}); });
} }
@ -258,7 +228,7 @@
const container = document.getElementById("alarmListContainer"); const container = document.getElementById("alarmListContainer");
container.innerHTML = ""; container.innerHTML = "";
alarms.forEach(alarm => { alarms.forEach((alarm) => {
const item = document.createElement("div"); const item = document.createElement("div");
item.className = "alarm-item"; item.className = "alarm-item";
@ -293,7 +263,6 @@
/* ------------------------------------------------------------------ /* ------------------------------------------------------------------
EINFACHE UI-Funktionen: Modal open/close EINFACHE UI-Funktionen: Modal open/close
------------------------------------------------------------------ */ ------------------------------------------------------------------ */
function openAddCryptoModal() { function openAddCryptoModal() {
document.getElementById("addCryptoModal").style.display = "block"; document.getElementById("addCryptoModal").style.display = "block";
document.getElementById("newCryptoSymbol").value = ""; document.getElementById("newCryptoSymbol").value = "";
@ -308,7 +277,7 @@ function openAlarmModal() {
const dropdown = document.getElementById("alarmSymbol"); const dropdown = document.getElementById("alarmSymbol");
dropdown.innerHTML = ""; dropdown.innerHTML = "";
cryptoList.forEach(symbol => { cryptoList.forEach((symbol) => {
const option = document.createElement("option"); const option = document.createElement("option");
option.value = symbol; option.value = symbol;
option.textContent = symbol; option.textContent = symbol;
@ -366,7 +335,6 @@ function closeBuyMeModal() {
/* ------------------------------------------------------------------ /* ------------------------------------------------------------------
NEU: Crypto News & Economic Calendar Modals NEU: Crypto News & Economic Calendar Modals
------------------------------------------------------------------ */ ------------------------------------------------------------------ */
function openCryptoNews() { function openCryptoNews() {
document.getElementById("cryptoNewsModal").style.display = "block"; document.getElementById("cryptoNewsModal").style.display = "block";
} }
@ -383,8 +351,6 @@ function closeEconomicCalendarModal() {
document.getElementById("economicCalendarModal").style.display = "none"; document.getElementById("economicCalendarModal").style.display = "none";
} }
function saveOptions() { function saveOptions() {
userOptions.soundFile = document.getElementById("soundSelect").value; userOptions.soundFile = document.getElementById("soundSelect").value;
const alarmSound = document.getElementById("alarmSound"); const alarmSound = document.getElementById("alarmSound");
@ -473,11 +439,22 @@ async function reorderCryptoList(fromIndex, toIndex) {
const item = cryptoList.splice(fromIndex, 1)[0]; const item = cryptoList.splice(fromIndex, 1)[0];
cryptoList.splice(toIndex, 0, item); cryptoList.splice(toIndex, 0, item);
renderCryptoGrid(); 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) { async function fetchCryptoData(symbol, elementId) {
const preferredApi = apiPreference[symbol] || "auto"; const preferredApi = apiPreference[symbol] || "auto";
@ -515,7 +492,6 @@ async function reorderCryptoList(fromIndex, toIndex) {
document.getElementById("api-" + elementId).textContent = "API: ?"; document.getElementById("api-" + elementId).textContent = "API: ?";
} }
async function fetchFromBinance(symbol, elementId) { async function fetchFromBinance(symbol, elementId) {
const tickerUrl = `https://api.binance.com/api/v3/ticker/24hr?symbol=${symbol}USDT`; const tickerUrl = `https://api.binance.com/api/v3/ticker/24hr?symbol=${symbol}USDT`;
let resp = await fetch(tickerUrl); let resp = await fetch(tickerUrl);
@ -527,7 +503,6 @@ async function reorderCryptoList(fromIndex, toIndex) {
const lastPrice = parseFloat(data.lastPrice); const lastPrice = parseFloat(data.lastPrice);
const priceChange24h = parseFloat(data.priceChangePercent); const priceChange24h = parseFloat(data.priceChangePercent);
const klineUrl = `https://api.binance.com/api/v3/klines?symbol=${symbol}USDT&interval=1h&limit=1`; const klineUrl = `https://api.binance.com/api/v3/klines?symbol=${symbol}USDT&interval=1h&limit=1`;
let klResp = await fetch(klineUrl); let klResp = await fetch(klineUrl);
if (!klResp.ok) throw new Error("Binance 1h kline request failed"); if (!klResp.ok) throw new Error("Binance 1h kline request failed");
@ -549,11 +524,10 @@ async function reorderCryptoList(fromIndex, toIndex) {
hourlyOpen, hourlyOpen,
lastPrice, lastPrice,
change24h: priceChange24h, change24h: priceChange24h,
change1h: pct1h change1h: pct1h,
}); });
} }
async function fetchFromOkx(symbol, elementId) { async function fetchFromOkx(symbol, elementId) {
const instId = `${symbol}-USDT`; const instId = `${symbol}-USDT`;
const tickerUrl = `https://www.okx.com/api/v5/market/ticker?instId=${instId}`; const tickerUrl = `https://www.okx.com/api/v5/market/ticker?instId=${instId}`;
@ -571,7 +545,6 @@ async function reorderCryptoList(fromIndex, toIndex) {
pct24 = ((lastPrice - dailyOpen) / dailyOpen) * 100; pct24 = ((lastPrice - dailyOpen) / dailyOpen) * 100;
} }
const cUrl = `https://www.okx.com/api/v5/market/candles?instId=${instId}&bar=1H&limit=1`; const cUrl = `https://www.okx.com/api/v5/market/candles?instId=${instId}&bar=1H&limit=1`;
let cResp = await fetch(cUrl); let cResp = await fetch(cUrl);
if (!cResp.ok) throw new Error("OKX 1h candle request failed"); if (!cResp.ok) throw new Error("OKX 1h candle request failed");
@ -593,52 +566,52 @@ async function reorderCryptoList(fromIndex, toIndex) {
hourlyOpen, hourlyOpen,
lastPrice, lastPrice,
change24h: pct24, change24h: pct24,
change1h: pct1h change1h: pct1h,
}); });
} }
function updateCryptoBox({ function formatPrice(value) {
symbol, if (isNaN(value)) {
elementId, return "-";
apiUsed, }
dailyOpen, if (value < 0.01) {
hourlyOpen, return value.toFixed(6);
lastPrice, } else {
change24h, return value.toFixed(4);
change1h }
}) { }
const dOpenStr = isNaN(dailyOpen) ? "-" : dailyOpen.toFixed(4);
const hOpenStr = isNaN(hourlyOpen) ? "-" : hourlyOpen.toFixed(4); function updateCryptoBox({symbol, elementId, apiUsed, dailyOpen, hourlyOpen, lastPrice, change24h, change1h}) {
const lastStr = isNaN(lastPrice) ? "-" : lastPrice.toFixed(4); const dOpenStr = formatPrice(dailyOpen);
const hOpenStr = formatPrice(hourlyOpen);
const lastStr = formatPrice(lastPrice);
const pct24Str = isNaN(change24h) ? "-" : change24h.toFixed(2) + "%"; const pct24Str = isNaN(change24h) ? "-" : change24h.toFixed(2) + "%";
const pct1hStr = isNaN(change1h) ? "-" : change1h.toFixed(2) + "%"; const pct1hStr = isNaN(change1h) ? "-" : change1h.toFixed(2) + "%";
document.getElementById("daily-" + elementId).textContent = "Daily Price: " + dOpenStr + " USDT"; document.getElementById("daily-" + elementId).innerHTML = `Daily Price:<br>${dOpenStr} USDT<br>`;
document.getElementById("hourly-" + elementId).textContent = "Price H: " + hOpenStr + " USDT";
document.getElementById("price-" + elementId).innerHTML = document.getElementById("hourly-" + elementId).innerHTML = `Price H:<br>${hOpenStr} USDT<br>`;
`<strong>Current Price:</strong> ${lastStr} USDT`;
document.getElementById("price-" + elementId).innerHTML = `<strong>Current Price:</strong><br>${lastStr} USDT<br>`;
const c24 = document.getElementById("change24-" + elementId); const c24 = document.getElementById("change24-" + elementId);
c24.textContent = "24h Change: " + pct24Str; c24.innerHTML = `24h Change: ${pct24Str}`;
c24.className = "change " + (change24h >= 0 ? "up" : "down"); c24.className = "change " + (change24h >= 0 ? "up" : "down");
const c1h = document.getElementById("change1h-" + elementId); const c1h = document.getElementById("change1h-" + elementId);
c1h.textContent = "1h Change: " + pct1hStr; c1h.innerHTML = `1h Change: ${pct1hStr}<br><br>`;
c1h.className = "change " + (change1h >= 0 ? "up" : "down"); c1h.className = "change " + (change1h >= 0 ? "up" : "down");
document.getElementById("api-" + elementId).textContent = "API: " + apiUsed; document.getElementById("api-" + elementId).innerHTML = `API: ${apiUsed}`;
checkAlarms(symbol, lastPrice); checkAlarms(symbol, lastPrice);
lastPrices[symbol] = lastPrice; lastPrices[symbol] = lastPrice;
} }
function checkAlarms(symbol, currentPrice) { function checkAlarms(symbol, currentPrice) {
alarms.forEach(alarm => { alarms.forEach((alarm) => {
if (alarm.symbol !== symbol) return; if (alarm.symbol !== symbol) return;
if (alarm.triggered) return; if (alarm.triggered) return;
const alarmPrice = parseFloat(alarm.price); const alarmPrice = parseFloat(alarm.price);
@ -647,33 +620,30 @@ async function reorderCryptoList(fromIndex, toIndex) {
let conditionMet = false; let conditionMet = false;
if (alarm.direction === "Rising") { if (alarm.direction === "Rising") {
conditionMet = (prevPrice < alarmPrice && currentPrice >= alarmPrice); conditionMet = prevPrice < alarmPrice && currentPrice >= alarmPrice;
} else if (alarm.direction === "Falling") { } else if (alarm.direction === "Falling") {
conditionMet = (prevPrice > alarmPrice && currentPrice <= alarmPrice); conditionMet = prevPrice > alarmPrice && currentPrice <= alarmPrice;
} else if (alarm.direction === "Both") { } else if (alarm.direction === "Both") {
const crossingUp = (prevPrice < alarmPrice && currentPrice >= alarmPrice); const crossingUp = prevPrice < alarmPrice && currentPrice >= alarmPrice;
const crossingDown = (prevPrice > alarmPrice && currentPrice <= alarmPrice); const crossingDown = prevPrice > alarmPrice && currentPrice <= alarmPrice;
conditionMet = crossingUp || crossingDown; conditionMet = crossingUp || crossingDown;
} }
if (conditionMet) { if (conditionMet) {
const msg = `⚠️ ALARM (${alarm.frequency}, ${alarm.direction}): ${symbol} reached ${alarmPrice}!`; const msg = `⚠️ ALARM (${alarm.frequency}, ${alarm.direction}): ${symbol} reached ${alarmPrice}!`;
showAlarmPopup(msg); showAlarmPopup(msg);
if (alarm.frequency === "Once") { if (alarm.frequency === "Once") {
alarm.triggered = true; alarm.triggered = true;
} }
} }
}); });
} }
function showErrorMessage(msg) { function showErrorMessage(msg) {
document.getElementById("errorMessage").textContent = msg; document.getElementById("errorMessage").textContent = msg;
document.getElementById("errorOverlay").style.display = "block"; document.getElementById("errorOverlay").style.display = "block";
} }
function closeErrorPopup() { function closeErrorPopup() {
document.getElementById("errorOverlay").style.display = "none"; document.getElementById("errorOverlay").style.display = "none";
} }
@ -687,7 +657,7 @@ async function reorderCryptoList(fromIndex, toIndex) {
if (Notification.permission === "granted") { if (Notification.permission === "granted") {
new Notification("Crypto Price Alarm", {body: message}); new Notification("Crypto Price Alarm", {body: message});
} else if (Notification.permission !== "denied") { } else if (Notification.permission !== "denied") {
Notification.requestPermission().then(permission => { Notification.requestPermission().then((permission) => {
if (permission === "granted") { if (permission === "granted") {
new Notification("Crypto Price Alarm", {body: message}); new Notification("Crypto Price Alarm", {body: message});
} }
@ -695,16 +665,48 @@ async function reorderCryptoList(fromIndex, toIndex) {
} }
} }
} }
function closeAlarmPopup() { function closeAlarmPopup() {
document.getElementById("alarmOverlay").style.display = "none"; document.getElementById("alarmOverlay").style.display = "none";
} }
function copyToClipboard(address) { function copyToClipboard(address) {
navigator.clipboard.writeText(address).then(() => { if (navigator.clipboard && window.isSecureContext) {
navigator.clipboard
.writeText(address)
.then(() => {
alert("Address is copied to the clipboard."); 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);
}
} }
async function isBinanceSupported(symbol) { async function isBinanceSupported(symbol) {
const url = `https://api.binance.com/api/v3/ticker/24hr?symbol=${symbol}USDT`; const url = `https://api.binance.com/api/v3/ticker/24hr?symbol=${symbol}USDT`;
@ -733,24 +735,22 @@ async function reorderCryptoList(fromIndex, toIndex) {
} }
} }
window.addEventListener("DOMContentLoaded", init); window.addEventListener("DOMContentLoaded", init);
function copyToClipboard(address) { function copyToClipboard(address) {
if (navigator.clipboard && window.isSecureContext) { if (navigator.clipboard && window.isSecureContext) {
navigator.clipboard
navigator.clipboard.writeText(address).then(() => { .writeText(address)
.then(() => {
alert("Address is copied to the clipboard."); alert("Address is copied to the clipboard.");
}).catch(err => { })
.catch((err) => {
console.error("Clipboard API Fehler:", err); console.error("Clipboard API Fehler:", err);
alert("Fehler beim Kopieren der Adresse."); alert("Fehler beim Kopieren der Adresse.");
}); });
} else { } else {
let textArea = document.createElement("textarea"); let textArea = document.createElement("textarea");
textArea.value = address; textArea.value = address;
textArea.style.position = "fixed"; textArea.style.position = "fixed";
textArea.style.top = "0"; textArea.style.top = "0";
textArea.style.left = "0"; textArea.style.left = "0";
@ -765,7 +765,7 @@ async function reorderCryptoList(fromIndex, toIndex) {
textArea.focus(); textArea.focus();
textArea.select(); textArea.select();
try { try {
document.execCommand('copy'); document.execCommand("copy");
alert("Address is copied to the clipboard."); alert("Address is copied to the clipboard.");
} catch (err) { } catch (err) {
console.error("Fallback Copy Fehler:", err); console.error("Fallback Copy Fehler:", err);

View File

@ -4,6 +4,8 @@ document.addEventListener("DOMContentLoaded", function () {
const searchInput = document.getElementById("search"); const searchInput = document.getElementById("search");
let allArticles = []; let allArticles = [];
function refreshNewsFeed() {
fetch(apiUrl) fetch(apiUrl)
.then(response => response.json()) .then(response => response.json())
.then(data => { .then(data => {
@ -14,6 +16,8 @@ document.addEventListener("DOMContentLoaded", function () {
newsFeed.innerHTML = "Error loading the news."; newsFeed.innerHTML = "Error loading the news.";
console.error("Error when retrieving the news:", error); console.error("Error when retrieving the news:", error);
}); });
}
function displayArticles(items) { function displayArticles(items) {
newsFeed.innerHTML = ""; newsFeed.innerHTML = "";
@ -30,11 +34,11 @@ document.addEventListener("DOMContentLoaded", function () {
</div> </div>
</div> </div>
`; `;
newsFeed.appendChild(newsItem); newsFeed.appendChild(newsItem);
}); });
} }
searchInput.addEventListener("input", function () { searchInput.addEventListener("input", function () {
const searchTerm = searchInput.value.toLowerCase(); const searchTerm = searchInput.value.toLowerCase();
const filteredArticles = allArticles.filter(item => const filteredArticles = allArticles.filter(item =>
@ -44,6 +48,7 @@ document.addEventListener("DOMContentLoaded", function () {
displayArticles(filteredArticles); displayArticles(filteredArticles);
}); });
function getTimeAgo(date) { function getTimeAgo(date) {
const now = new Date(); const now = new Date();
const seconds = Math.floor((now - new Date(date)) / 1000); const seconds = Math.floor((now - new Date(date)) / 1000);
@ -59,6 +64,7 @@ document.addEventListener("DOMContentLoaded", function () {
return `vor ${Math.floor(days / 365)} Jahren`; return `vor ${Math.floor(days / 365)} Jahren`;
} }
function formatSourceName(source) { function formatSourceName(source) {
const sourceMap = { const sourceMap = {
"crypto_news": "Crypto News", "crypto_news": "Crypto News",
@ -70,4 +76,8 @@ document.addEventListener("DOMContentLoaded", function () {
}; };
return sourceMap[source] || source; return sourceMap[source] || source;
} }
refreshNewsFeed();
setInterval(refreshNewsFeed, 180000);
}); });

74
public/responsive.css Normal file
View File

@ -0,0 +1,74 @@
@media (max-width: 1614px) {
.grid-container {
grid-template-columns: repeat(4, 1fr);
}
}
@media (max-width: 1353px) {
.grid-container {
grid-template-columns: repeat(3, 1fr);
}
}
@media (max-width: 1275px) {
.grid-container {
grid-template-columns: repeat(3, 1fr);
}
}
@media (max-width: 1200px) {
.grid-container {
grid-template-columns: repeat(2, 1fr);
}
}
@media (max-width: 992px) {
.grid-container {
grid-template-columns: repeat(2, 1fr);
}
}
@media (max-width: 768px) {
.grid-container {
grid-template-columns: repeat(2, 1fr);
}
}
@media (max-width: 480px) {
.grid-container {
grid-template-columns: 1fr;
}
}
.main-container {
display: flex;
}
.left-column {
width: 75%;
}
.right-column {
width: 320px;
border-left: 1px solid #444;
padding: 20px;
display: flex;
flex-direction: column;
}
@media (max-width: 1251px) {
.button-grid-container {
display: grid;
grid-template-columns: none;
align-items: center;
justify-items: center;
column-gap: 10px;
margin-bottom: 15px;
}
}

View File

@ -1,12 +1,13 @@
body { body {
font-family: Arial, sans-serif; font-family: Arial, sans-serif;
margin: 0 auto; /* Zentrierung */ margin: 0 auto;
padding: 0; padding: 0;
transition: background-color 0.4s, color 0.4s; transition: background-color 0.4s, color 0.4s;
min-width: 1400px;
max-width: 1978px; max-width: 1778px;
min-width: 808px;
} }
h1 { h1 {
font-size: 28px; font-size: 28px;
} }
@ -16,6 +17,7 @@ body.dark {
background-color: #121212; background-color: #121212;
color: #ffffff; color: #ffffff;
} }
body.light { body.light {
background-color: #f2f2f2; background-color: #f2f2f2;
color: #000000; color: #000000;
@ -26,9 +28,11 @@ body.light {
text-align: center; text-align: center;
padding: 20px; padding: 20px;
} }
.button-container { .button-container {
margin-bottom: 10px; margin-bottom: 10px;
} }
button { button {
padding: 10px; padding: 10px;
margin: 5px; margin: 5px;
@ -38,31 +42,68 @@ button {
border: none; border: none;
cursor: pointer; cursor: pointer;
border-radius: 5px; border-radius: 5px;
} }
button:hover { button:hover {
background-color: #005f73; background-color: #005f73;
} }
.alarm-btn { background-color: #ff8b00; }
.alarm-btn:hover { background-color: #e38806; } .alarm-btn {
.options-btn { background-color: #555; } background-color: #ff8b00;
.options-btn:hover { background-color: #777; } }
.alarm-btn:hover {
background-color: #e38806;
}
.options-btn {
background-color: #555;
}
.options-btn:hover {
background-color: #777;
}
.main-container { .main-container {
display: flex; display: flex;
} }
.left-column { .left-column {
width: 75%; width: 75%;
padding: 20px; padding: 10px;
} }
.right-column { .right-column {
width: 320px;
display: grid;
grid-template-rows: 300px auto;
border-left: 1px solid #444; border-left: 1px solid #444;
width: 320px;
padding: 20px; padding: 20px;
}
.notify-area {
min-height: 400px;
max-width: 280px;
border: 1px solid #444;
overflow-y: auto;
padding: 10px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
.buttons-and-version {
display: flex;
flex-direction: column;
max-width: 260px;
padding-left: 6%;
}
.notify-heading { .notify-heading {
display: flex; display: flex;
@ -71,6 +112,7 @@ button:hover {
font-size: 20px; font-size: 20px;
margin-bottom: 10px; margin-bottom: 10px;
} }
.notify-list { .notify-list {
list-style: none; list-style: none;
padding-left: 0; padding-left: 0;
@ -83,12 +125,14 @@ button:hover {
.grid-container { .grid-container {
display: grid; display: grid;
grid-template-columns: repeat(3, 1fr); grid-template-columns: repeat(5, 1fr);
gap: 10px; gap: 10px;
width: 90%;
width: 80%;
margin: auto; margin: auto;
padding: 10px; padding: 10px;
} }
.crypto-box { .crypto-box {
background-color: #1e1e1e; background-color: #1e1e1e;
border: 2px solid #444; border: 2px solid #444;
@ -98,15 +142,22 @@ button:hover {
font-size: 16px; font-size: 16px;
position: relative; position: relative;
user-select: none; user-select: none;
min-width: 177px;
} }
body.light .crypto-box { body.light .crypto-box {
background-color: #ffffff; background-color: #ffffff;
border: 2px solid #ccc; border: 2px solid #ccc;
color: #000; color: #000;
} }
.change.up { color: #00ff00; } .change.up {
.change.down { color: #ff0000; } color: #00ff00;
}
.change.down {
color: #ff0000;
}
.delete-btn { .delete-btn {
@ -125,6 +176,7 @@ body.light .crypto-box {
line-height: 24px; line-height: 24px;
padding: 0; padding: 0;
} }
.delete-btn:hover { .delete-btn:hover {
background-color: #999; background-color: #999;
color: #000; color: #000;
@ -135,6 +187,7 @@ body.light .crypto-box {
opacity: 0.5; opacity: 0.5;
transform: scale(0.97); transform: scale(0.97);
} }
.crypto-box.drag-over { .crypto-box.drag-over {
border: 2px dashed #ccc !important; border: 2px dashed #ccc !important;
} }
@ -144,16 +197,19 @@ body.light .crypto-box {
display: none; display: none;
position: fixed; position: fixed;
z-index: 2; z-index: 2;
left: 0; top: 0; left: 0;
width: 100%; height: 100%; top: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.7); background-color: rgba(0, 0, 0, 0.7);
} }
.modal-content { .modal-content {
background-color: #222; background-color: #222222f2;
margin: 5% auto; margin: 5% auto;
padding: 20px; padding: 20px;
border-radius: 8px; border-radius: 8px;
max-width: 40%;
text-align: center; text-align: center;
color: white; color: white;
@ -200,7 +256,7 @@ body.light .crypto-box {
#optionsModal .modal-content { #optionsModal .modal-content {
width: 20%; width: 30%;
overflow-x: hidden; overflow-x: hidden;
} }
@ -245,12 +301,12 @@ body.light .crypto-box {
#alarmModal .modal-content { #alarmModal .modal-content {
width: 30%; min-width: 406px;
text-align: center; text-align: center;
position: absolute; position: absolute;
top: 35%; top: 35%;
left: 50%; left: 50%;
transform: translate(-50%, -50%); transform: translate(-50%, -45%);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
@ -265,13 +321,14 @@ body.light .crypto-box {
#alarmModal .modal-content select, #alarmModal .modal-content select,
#alarmModal .modal-content input { #alarmModal .modal-content input {
margin-bottom: 8px; margin-bottom: 8px;
width: 100%; width: 30%;
padding: 8px;
} }
#alarmModal .modal-content select { #alarmModal .modal-content select {
padding: 8px; padding: 8px;
font-size: 13px; font-size: 13px;
width: 100%; width: 30%;
} }
#alarmModal .close { #alarmModal .close {
@ -289,17 +346,23 @@ body.light .modal-content {
background-color: #ddd; background-color: #ddd;
color: #000; color: #000;
} }
.close { .close {
float: right; float: right;
font-size: 24px; font-size: 24px;
cursor: pointer; cursor: pointer;
} }
label { label {
display: inline-block; display: inline-block;
margin-top: 10px; margin-top: 10px;
} }
select, input[type="number"], input[type="text"] {
select,
input[type="number"],
input[type="text"] {
margin: 5px 0 10px 5px; margin: 5px 0 10px 5px;
font-size: medium;
} }
@ -307,10 +370,13 @@ select, input[type="number"], input[type="text"] {
display: none; display: none;
position: fixed; position: fixed;
z-index: 3; z-index: 3;
left: 0; top: 0; left: 0;
width: 100%; height: 100%; top: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.7); background-color: rgba(0, 0, 0, 0.7);
} }
.alert-box { .alert-box {
background-color: #333; background-color: #333;
color: #fff; color: #fff;
@ -321,13 +387,16 @@ select, input[type="number"], input[type="text"] {
text-align: center; text-align: center;
position: relative; position: relative;
} }
.alert-box button { .alert-box button {
margin-top: 15px; margin-top: 15px;
background-color: #555; background-color: #555;
} }
.alert-box button:hover { .alert-box button:hover {
background-color: #777; background-color: #777;
} }
body.light .alert-box { body.light .alert-box {
background-color: #ccc; background-color: #ccc;
color: #000; color: #000;
@ -338,10 +407,13 @@ body.light .alert-box {
display: none; display: none;
position: fixed; position: fixed;
z-index: 4; z-index: 4;
left: 0; top: 0; left: 0;
width: 100%; height: 100%; top: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.7); background-color: rgba(0, 0, 0, 0.7);
} }
.error-box { .error-box {
background-color: #aa3333; background-color: #aa3333;
color: #fff; color: #fff;
@ -352,6 +424,7 @@ body.light .alert-box {
text-align: center; text-align: center;
position: relative; position: relative;
} }
body.light .error-box { body.light .error-box {
background-color: #f2aaaa; background-color: #f2aaaa;
color: #000; color: #000;
@ -365,6 +438,7 @@ body.light .error-box {
text-decoration: underline; text-decoration: underline;
cursor: pointer; cursor: pointer;
} }
body.light .api-label { body.light .api-label {
color: #333; color: #333;
} }
@ -378,6 +452,7 @@ body.light .api-label {
overflow-y: auto; overflow-y: auto;
margin: 1em 0; margin: 1em 0;
} }
.alarm-item { .alarm-item {
display: flex; display: flex;
align-items: center; align-items: center;
@ -386,10 +461,12 @@ body.light .api-label {
padding: 6px; padding: 6px;
border-radius: 3px; border-radius: 3px;
} }
body.light .alarm-item { body.light .alarm-item {
background-color: #eee; background-color: #eee;
color: #000; color: #000;
} }
.alarm-delete-btn { .alarm-delete-btn {
background-color: #008CBA; background-color: #008CBA;
border: none; border: none;
@ -398,6 +475,7 @@ body.light .alarm-item {
border-radius: 5px; border-radius: 5px;
padding: 5px 8px; padding: 5px 8px;
} }
.alarm-delete-btn:hover { .alarm-delete-btn:hover {
background-color: #005f73; background-color: #005f73;
} }
@ -420,9 +498,11 @@ body.light .alarm-item {
border: none; border: none;
cursor: pointer; cursor: pointer;
} }
.coffee-btn:hover { .coffee-btn:hover {
background-color: #711fe9; background-color: #711fe9;
} }
.coffee-icon { .coffee-icon {
width: 25px; width: 25px;
height: 25px; height: 25px;
@ -443,21 +523,26 @@ body.light .alarm-item {
color: white; color: white;
text-align: center; text-align: center;
} }
#news-feed { #news-feed {
text-align: left; text-align: left;
} }
.news-item { .news-item {
padding: 15px; padding: 15px;
border-bottom: 1px solid #333; border-bottom: 1px solid #333;
transition: background 0.3s; transition: background 0.3s;
} }
.news-item:last-child { .news-item:last-child {
border-bottom: none; border-bottom: none;
} }
.news-item:hover { .news-item:hover {
background: rgba(255, 255, 255, 0.1); background: rgba(255, 255, 255, 0.1);
cursor: pointer; cursor: pointer;
} }
.news-title { .news-title {
font-size: 18px; font-size: 18px;
color: #1db954; color: #1db954;
@ -465,6 +550,7 @@ body.light .alarm-item {
display: block; display: block;
margin-bottom: 5px; margin-bottom: 5px;
} }
.news-meta { .news-meta {
font-size: 14px; font-size: 14px;
color: gray; color: gray;
@ -472,10 +558,12 @@ body.light .alarm-item {
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
} }
.news-source { .news-source {
font-weight: bold; font-weight: bold;
color: #ffffff; color: #ffffff;
} }
.time { .time {
font-size: 14px; font-size: 14px;
color: gray; color: gray;
@ -484,18 +572,14 @@ body.light .alarm-item {
.button-grid-container { .button-grid-container {
display: grid; display: grid;
grid-template-columns: 1fr 2fr 1fr; grid-template-columns: 1fr 1fr 1fr;
/* alle gleich breit */
align-items: center; align-items: center;
justify-items: center; justify-items: center;
gap: 10px; column-gap: 10px;
/* oder kleiner */
margin-bottom: 15px; margin-bottom: 15px;
} }
.grid-left, .grid-middle, .grid-right {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 10px;
}
#economicCalendarModal .iframe-container { #economicCalendarModal .iframe-container {
@ -510,6 +594,7 @@ body.light .alarm-item {
align-items: center; align-items: center;
justify-content: center; justify-content: center;
} }
#economicCalendarModal .iframe-wrapper { #economicCalendarModal .iframe-wrapper {
position: relative; position: relative;
width: 100%; width: 100%;
@ -520,12 +605,14 @@ body.light .alarm-item {
background: white; background: white;
box-shadow: 0px 4px 10px rgba(0, 0, 0, 0.2); box-shadow: 0px 4px 10px rgba(0, 0, 0, 0.2);
} }
#economicCalendarModal iframe { #economicCalendarModal iframe {
width: calc(100% - 20px); width: calc(100% - 20px);
height: calc(100% - 20px); height: calc(100% - 20px);
border: none; border: none;
display: block; display: block;
} }
#economicCalendarModal .poweredBy { #economicCalendarModal .poweredBy {
margin-top: 10px; margin-top: 10px;
font-family: Arial, Helvetica, sans-serif; font-family: Arial, Helvetica, sans-serif;
@ -533,6 +620,7 @@ body.light .alarm-item {
color: #ffffff; color: #ffffff;
text-align: center; text-align: center;
} }
#economicCalendarModal .poweredBy a { #economicCalendarModal .poweredBy a {
color: #06529D; color: #06529D;
font-weight: bold; font-weight: bold;
@ -540,3 +628,26 @@ body.light .alarm-item {
} }
#updateModal .modal-content {}
.version-info {
text-align: center;
margin-top: 10px;
}
.change {
margin: 5px;
}
.my-button {
background-color: white;
color: black;
border: 1px solid black;
padding: 8px 16px;
cursor: pointer;
font-weight: 700;
}

112
public/update.js Normal file
View File

@ -0,0 +1,112 @@
const CURRENT_VERSION = "1.0.6";
function getUpdateUrl() {
return "/api/update?t=" + new Date().getTime();
}
document.addEventListener("DOMContentLoaded", () => {
document.getElementById("currentVersion").textContent = `Version ${CURRENT_VERSION}`;
checkForUpdates();
setInterval(checkForUpdates, 259200000);
});
function checkForUpdates() {
fetch(getUpdateUrl())
.then((response) => response.json())
.then((data) => {
let skippedVersion = localStorage.getItem("skippedVersion") || null;
if (skippedVersion && data.version !== skippedVersion) {
localStorage.removeItem("skippedVersion");
skippedVersion = null;
}
if (compareVersions(data.version, CURRENT_VERSION) > 0) {
const updateAvailableEl = document.getElementById("updateAvailable");
updateAvailableEl.style.display = "inline";
updateAvailableEl.onclick = () => {
fetch(getUpdateUrl())
.then((response) => response.json())
.then((data) => {
let skippedVersion = localStorage.getItem("skippedVersion") || null;
if (skippedVersion && data.version !== skippedVersion) {
localStorage.removeItem("skippedVersion");
skippedVersion = null;
}
openUpdateModal(data);
})
.catch((error) =>
console.error("Fehler beim erneuten Abrufen des Updates:", error)
);
};
}
})
.catch((error) =>
console.error("Fehler beim Abrufen des Updates:", error)
);
}
function compareVersions(v1, v2) {
const v1parts = v1.split(".").map(Number);
const v2parts = v2.split(".").map(Number);
for (let i = 0; i < Math.max(v1parts.length, v2parts.length); i++) {
const num1 = v1parts[i] || 0;
const num2 = v2parts[i] || 0;
if (num1 > num2) return 1;
if (num1 < num2) return -1;
}
return 0;
}
function openUpdateModal(data) {
document.getElementById("updateVersion").textContent = data.version;
let changelogContainer = document.getElementById("updateChanges");
changelogContainer.innerHTML = "";
if (Array.isArray(data.changelog)) {
let ul = document.createElement("ul");
data.changelog.forEach(item => {
let li = document.createElement("li");
li.textContent = item;
ul.appendChild(li);
});
changelogContainer.appendChild(ul);
} else {
changelogContainer.textContent = data.changelog || "Kein Changelog vorhanden.";
}
window._updateData = data;
document.getElementById("updateModal").style.display = "block";
}
function closeUpdateModal() {
document.getElementById("updateModal").style.display = "none";
}
function performUpdate() {
window.open("https://github.com/Gerald-Ha/HodlEye-Crypto-Price-Tracker", "_blank");
closeUpdateModal();
}
function skipUpdate() {
if (window._updateData && window._updateData.version) {
localStorage.setItem("skippedVersion", window._updateData.version);
}
closeUpdateModal();
}

View File

@ -5,6 +5,7 @@ const cors = require("cors");
const http = require("http"); const http = require("http");
const WebSocket = require("ws"); const WebSocket = require("ws");
const app = express(); const app = express();
app.use(cors()); app.use(cors());
app.use(express.json()); app.use(express.json());
@ -16,7 +17,7 @@ const clients = new Set();
/* /*
* Metadata * Metadata
* Version: 1.0.5 * Version: 1.0.6
* Author/Dev: Gerald Hasani * Author/Dev: Gerald Hasani
* Name: HodlEye Crypto Price Tracker * Name: HodlEye Crypto Price Tracker
* Email: contact@gerald-hasani.com * Email: contact@gerald-hasani.com
@ -25,12 +26,20 @@ const clients = new Set();
const DATA_FILE = path.join(__dirname, "..", "data", "data.json"); const DATA_FILE = path.join(__dirname, "..", "data", "data.json");
if (!fs.existsSync(DATA_FILE)) { if (!fs.existsSync(DATA_FILE)) {
fs.writeFileSync(DATA_FILE, JSON.stringify({ fs.writeFileSync(
DATA_FILE,
JSON.stringify(
{
cryptos: ["BTC"], cryptos: ["BTC"],
alarms: [], alarms: [],
notifications: [] notifications: []
}, null, 2)); },
null,
2
)
);
} }
function readData() { function readData() {
@ -41,6 +50,23 @@ function writeData(data) {
fs.writeFileSync(DATA_FILE, JSON.stringify(data, null, 2)); fs.writeFileSync(DATA_FILE, JSON.stringify(data, null, 2));
} }
app.get("/api/update", (req, res) => {
const remoteUpdateUrl = "https://raw.githubusercontent.com/Gerald-Ha/HodlEye-Crypto-Price-Tracker/refs/heads/main/update.json";
fetch(remoteUpdateUrl)
.then(response => response.json())
.then(data => {
res.header("Cache-Control", "no-cache, no-store, must-revalidate");
res.header("Pragma", "no-cache");
res.header("Expires", "0");
res.json(data);
})
.catch(error => {
console.error("Fehler beim Abrufen der remote update.json:", error);
res.status(500).json({ error: "Could not fetch update data" });
});
});
app.get("/api/cryptos", (req, res) => { app.get("/api/cryptos", (req, res) => {
const data = readData(); const data = readData();
res.json(data.cryptos); res.json(data.cryptos);
@ -51,10 +77,8 @@ app.post("/api/cryptos", (req, res) => {
if (!symbol) { if (!symbol) {
return res.status(400).json({ error: "Symbol is required." }); return res.status(400).json({ error: "Symbol is required." });
} }
const data = readData(); const data = readData();
const upperSymbol = symbol.toUpperCase(); const upperSymbol = symbol.toUpperCase();
if (!data.cryptos.includes(upperSymbol)) { if (!data.cryptos.includes(upperSymbol)) {
data.cryptos.push(upperSymbol); data.cryptos.push(upperSymbol);
writeData(data); writeData(data);
@ -70,6 +94,17 @@ app.delete("/api/cryptos/:symbol", (req, res) => {
res.json({ success: true, cryptos: data.cryptos }); res.json({ success: true, cryptos: data.cryptos });
}); });
app.put("/api/cryptos", (req, res) => {
const { cryptoList } = req.body;
if (!Array.isArray(cryptoList)) {
return res.status(400).json({ error: "cryptoList must be an array." });
}
const data = readData();
data.cryptos = cryptoList;
writeData(data);
res.json({ success: true, cryptos: data.cryptos });
});
app.get("/api/alarms", (req, res) => { app.get("/api/alarms", (req, res) => {
const data = readData(); const data = readData();
res.json(data.alarms); res.json(data.alarms);
@ -80,7 +115,6 @@ app.post("/api/alarms", (req, res) => {
if (!symbol || !price) { if (!symbol || !price) {
return res.status(400).json({ error: "symbol and price are required." }); return res.status(400).json({ error: "symbol and price are required." });
} }
const data = readData(); const data = readData();
const newAlarm = { const newAlarm = {
id: Date.now(), id: Date.now(),
@ -113,7 +147,6 @@ app.post("/api/notifications", (req, res) => {
if (!message) { if (!message) {
return res.status(400).json({ error: "message is required." }); return res.status(400).json({ error: "message is required." });
} }
const data = readData(); const data = readData();
const entry = { const entry = {
message, message,
@ -122,7 +155,7 @@ app.post("/api/notifications", (req, res) => {
data.notifications.unshift(entry); data.notifications.unshift(entry);
writeData(data); writeData(data);
// Nachricht an alle WebSocket-Clients senden
clients.forEach((client) => { clients.forEach((client) => {
if (client.readyState === WebSocket.OPEN) { if (client.readyState === WebSocket.OPEN) {
client.send(JSON.stringify(entry)); client.send(JSON.stringify(entry));
@ -141,18 +174,15 @@ app.delete("/api/notifications", (req, res) => {
app.use(express.static(path.join(__dirname, "..", "public"))); app.use(express.static(path.join(__dirname, "..", "public")));
// WebSocket-Server für Echtzeitbenachrichtigungen
wss.on("connection", (ws) => { wss.on("connection", (ws) => {
console.log("Client verbunden"); console.log("Client verbunden");
clients.add(ws); clients.add(ws);
ws.on("close", () => { ws.on("close", () => {
console.log("Client getrennt"); console.log("Client getrennt");
clients.delete(ws); clients.delete(ws);
}); });
}); });
// Server starten
const PORT = process.env.PORT || 3099; const PORT = process.env.PORT || 3099;
server.listen(PORT, () => { server.listen(PORT, () => {
console.log(`Server läuft auf Port ${PORT}`); console.log(`Server läuft auf Port ${PORT}`);

14
update.json Normal file
View File

@ -0,0 +1,14 @@
{
"version": "1.0.6",
"changelog": [
"Bug fixes and improvements",
"Responsive design added",
"Crypto News Auto Update without Browser Refresh every 3 minutes",
"Crypto Price Refresh Reduce from 5 Second to 1 Second",
"Docker command updated to save alarms and registered cryptocurrencies that are still there after a Docker image update (--> docker run -p 3099:3099 -p 5001:5001 -v hodleye_data:/app/data --name hodleye-container hodleye-crypto-tracker)",
"Bugfix 'Edit List' works now",
"Added Update Option and Notification every 5 Days",
"Re-Design from Crypto-box",
"Improving the logic of the price display for small cryptocurrencies"
]
}