mirror of
https://github.com/Gerald-Ha/HodlEye-Crypto-Price-Tracker.git
synced 2025-07-01 03:49:44 +00:00
Compare commits
10 Commits
03876d3b67
...
2f29f21519
Author | SHA1 | Date | |
---|---|---|---|
![]() |
2f29f21519 | ||
92917dbeaa | |||
55d15cf147 | |||
5554024131 | |||
e297acdcdc | |||
9dd80f704c | |||
06cfc8244a | |||
f5c7e607df | |||
![]() |
23500f0c20 | ||
f751559878 |
@ -1,4 +1,4 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
Copyright (c) 2025 Gerald-H
|
Copyright (c) 2025 Gerald-H
|
||||||
|
|
||||||
|
86
README.md
86
README.md
@ -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.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
||||||
## 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">
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### 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**.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### 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.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### 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">
|
||||||
|
|
||||||
|
|
||||||
### 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">
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 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 you’re in the same directory as the Dockerfile.)*
|
|
||||||
|
_(Make sure you’re 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)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 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.
|
||||||
|
|
||||||
|
|
||||||
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, you’ll see the status change to “Connected.”
|
- Once connected, you’ll 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, you’ll 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, you’ll 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.
|
||||||
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 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
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
### 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)
|
||||||
|
|
||||||
|
|
||||||
### 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, it’s already bundled, so just expose `5001`.)*
|
_(Within Docker, it’s already bundled, so just expose `5001`.)_
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
||||||
## Important Notes / Limitations
|
## Important Notes / Limitations
|
||||||
|
|
||||||
1. **5-second polling**
|
1. **1-second polling**
|
||||||
- There’s 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.
|
|
||||||
|
- There’s 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
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
||||||
## 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!
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
||||||
## 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.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
8
data/data.json
Normal file
8
data/data.json
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"cryptos": [
|
||||||
|
"BTC",
|
||||||
|
"ETH"
|
||||||
|
],
|
||||||
|
"alarms": [],
|
||||||
|
"notifications": []
|
||||||
|
}
|
1
public/images/Gitea_Logo.svg
Normal file
1
public/images/Gitea_Logo.svg
Normal 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 |
1
public/images/github-mark.svg
Normal file
1
public/images/github-mark.svg
Normal 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 |
@ -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()">×</span>
|
<span class="close" onclick="closeAlarmModal()">×</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()">×</span>
|
<span class="close" onclick="closeOptionsModal()">×</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()">×</span>
|
<span class="close" onclick="closeApiModal()">×</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()">×</span>
|
<span class="close" onclick="closeAddCryptoModal()">×</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()">×</span>
|
<span class="close" onclick="closeApiSelectModal()">×</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()">×</span>
|
<span class="close" onclick="closeBuyMeModal()">×</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()">×</span>
|
<span class="close" onclick="closeCryptoNewsModal()">×</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()">×</span>
|
<span class="close" onclick="closeEconomicCalendarModal()"
|
||||||
|
>×</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()">×</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>
|
||||||
|
186
public/magic.js
186
public/magic.js
@ -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);
|
||||||
|
@ -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
74
public/responsive.css
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
183
public/style.css
183
public/style.css
@ -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
112
public/update.js
Normal 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();
|
||||||
|
}
|
@ -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
14
update.json
Normal 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"
|
||||||
|
]
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user