mirror of
https://github.com/Gerald-Ha/HodlEye-Crypto-Price-Tracker.git
synced 2025-04-08 21:13:18 +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
|
||||
|
||||
|
210
README.md
210
README.md
@ -4,29 +4,27 @@ 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">
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [Overview](#overview)
|
||||
2. [Features](#features)
|
||||
- [Unlimited Alarms & Tracking](#unlimited-alarms--tracking)
|
||||
- [Price Updates](#price-updates)
|
||||
- [Alarm Functionality](#alarm-functionality)
|
||||
- [Crypto News](#crypto-news)
|
||||
- [Economic Calendar](#economic-calendar)
|
||||
3. [Installation & Usage](#installation--usage)
|
||||
- [Requirements](#requirements)
|
||||
- [Docker Build & Run](#docker-build--run)
|
||||
4. [Windows Notification App: HodlEye_Notify](#windows-notification-app-hodleye_notify)
|
||||
5. [Project Structure](#project-structure)
|
||||
- [Frontend (index.html & magic.js)](#frontend-indexhtml--magicjs)
|
||||
- [News Feed Server (Node.js)](#news-feed-server-nodejs)
|
||||
6. [Important Notes / Limitations](#important-notes--limitations)
|
||||
7. [Coming Soon](#coming-soon)
|
||||
8. [Privacy & Data Disclaimer](#privacy--data-disclaimer)
|
||||
1. [Overview](#overview)
|
||||
2. [Features](#features)
|
||||
- [Unlimited Alarms & Tracking](#unlimited-alarms--tracking)
|
||||
- [Price Updates](#price-updates)
|
||||
- [Alarm Functionality](#alarm-functionality)
|
||||
- [Crypto News](#crypto-news)
|
||||
- [Economic Calendar](#economic-calendar)
|
||||
3. [Installation & Usage](#installation--usage)
|
||||
- [Requirements](#requirements)
|
||||
- [Docker Build & Run](#docker-build--run)
|
||||
4. [Windows Notification App: HodlEye_Notify](#windows-notification-app-hodleye_notify)
|
||||
5. [Project Structure](#project-structure)
|
||||
- [Frontend (index.html & magic.js)](#frontend-indexhtml--magicjs)
|
||||
- [News Feed Server (Node.js)](#news-feed-server-nodejs)
|
||||
6. [Important Notes / Limitations](#important-notes--limitations)
|
||||
7. [Coming Soon](#coming-soon)
|
||||
8. [Privacy & Data Disclaimer](#privacy--data-disclaimer)
|
||||
9. [License](#license)
|
||||
|
||||
---
|
||||
@ -35,30 +33,37 @@ A lightweight Docker-based web tool to monitor cryptocurrency prices (via Binanc
|
||||
|
||||
**HodlEye Crypto Price Tracker** is a Dockerized application that aims to surpass typical limitations of other tracking platforms (like TradingView), offering:
|
||||
|
||||
- **Unlimited Alarms**: No cap on the number of alarms you can set.
|
||||
- **Unlimited Crypto Tracking**: Easily add as many coins as you want.
|
||||
- **Real-Time Price Updates (every 5 seconds)**: Uses Binance and OKX data.
|
||||
- **Unlimited Alarms**: No cap on the number of alarms you can set.
|
||||
- **Unlimited Crypto Tracking**: Easily add as many coins as you want.
|
||||
- **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.
|
||||
|
||||
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
|
||||
|
||||
### Unlimited Alarms & Tracking
|
||||
|
||||
- You can set **as many alarms as you like** — no daily or total limit.
|
||||
- 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">
|
||||
|
||||
|
||||
|
||||
### Price Updates
|
||||
|
||||
- **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.
|
||||
- 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
|
||||
|
||||
- Set alarms for each coin (e.g., `BTC/USDT`), choosing:
|
||||
- **Alarm Price** (threshold)
|
||||
- **Direction** (Rising, Falling, or Both)
|
||||
@ -66,94 +71,112 @@ 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.
|
||||
- **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
|
||||
|
||||
- News from multiple RSS sources:
|
||||
- `https://crypto.news/feed/`
|
||||
- `https://cointelegraph.com/rss`
|
||||
- `https://thedefiant.io/api/feed`
|
||||
- `https://newsbtc.com/feed`
|
||||
- `https://news.bitcoin.com/feed` *(may be inaccessible in certain regions)*
|
||||
- `https://bitcoinmagazine.com/feed`
|
||||
- `https://cryptopanic.com/news/rss/`
|
||||
- `https://crypto.news/feed/`
|
||||
- `https://cointelegraph.com/rss`
|
||||
- `https://thedefiant.io/api/feed`
|
||||
- `https://newsbtc.com/feed`
|
||||
- `https://news.bitcoin.com/feed` _(may be inaccessible in certain regions)_
|
||||
- `https://bitcoinmagazine.com/feed`
|
||||
- `https://cryptopanic.com/news/rss/`
|
||||
- `https://decrypt.co/feed`
|
||||
- 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">
|
||||
|
||||
|
||||
|
||||
### 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.
|
||||
|
||||
<img src="https://github.com/user-attachments/assets/e254301e-9aaa-48d8-84e7-6faa598ca8be" width="600" height="auto">
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
|
||||
## Installation & Usage
|
||||
|
||||
### Requirements
|
||||
|
||||
- [Docker](https://www.docker.com/) installed.
|
||||
- (Optional) [Docker-Compose](https://docs.docker.com/compose/) if you want a more complex or multi-container setup.
|
||||
|
||||
### Docker Build & Run
|
||||
|
||||
1. **Clone this repository**
|
||||
1. **Clone this repository**
|
||||
|
||||
```bash
|
||||
git clone https://github.com/YourGitHubName/HodlEye.git
|
||||
git clone https://github.com/Gerald-Ha/HodlEye-Crypto-Price-Tracker.git
|
||||
cd HodlEye
|
||||
```
|
||||
|
||||
2. **Build the Docker image**
|
||||
2. **Build the Docker image**
|
||||
|
||||
```bash
|
||||
docker buildx build -t hodleye-crypto-tracker .
|
||||
```
|
||||
*(Make sure you’re in the same directory as the Dockerfile.)*
|
||||
|
||||
3. **Run the container**
|
||||
_(Make sure you’re in the same directory as the Dockerfile.)_
|
||||
|
||||
3. **Run the container**
|
||||
|
||||
```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.
|
||||
|
||||
4. **Access the application**
|
||||
- **Main UI**: [http://localhost:3099](http://localhost:3099)
|
||||
4. **Access the application**
|
||||
- **Main UI**: [http://localhost:3099](http://localhost:3099)
|
||||
- **News Feed Endpoint**: [http://localhost:5001/api/news](http://localhost:5001/api/news)
|
||||
|
||||
---
|
||||
|
||||
---
|
||||
|
||||
## 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/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.
|
||||
- Click **Connect** to establish a WebSocket connection.
|
||||
- Once connected, you’ll see the status change to “Connected.”
|
||||
- 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.
|
||||
- Once connected, you’ll see the status change to “Connected.”
|
||||
|
||||
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.
|
||||
2. **Autostart**
|
||||
|
||||
3. **Testing Notifications**
|
||||
- From the machine running the Docker container, you can trigger a test notification using the following `curl` command:
|
||||
```bash
|
||||
- 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**
|
||||
|
||||
- From the machine running the Docker container, you can trigger a test notification using the following `curl` command:
|
||||
|
||||
**Ubuntu**
|
||||
```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.
|
||||
```
|
||||
|
||||
**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.
|
||||
|
||||
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
|
||||
|
||||
Below is an example directory tree (based on your structure). Yours may vary slightly:
|
||||
@ -191,24 +214,28 @@ HodlEye-Crypto-Price-Tracker
|
||||
└── ping.mp3
|
||||
```
|
||||
|
||||
|
||||
### Frontend (`index.html` & `magic.js`)
|
||||
- **`index.html`**
|
||||
- Main interface containing modals and buttons (Add Crypto, Edit List, Alarms, Options, etc.).
|
||||
|
||||
- **`index.html`**
|
||||
- Main interface containing modals and buttons (Add Crypto, Edit List, Alarms, Options, etc.).
|
||||
- Includes buttons for:
|
||||
- **Crypto News** (opens a news modal)
|
||||
- **Economic Calendar** (Investing.com iframe)
|
||||
- **Unlimited Alarms** management
|
||||
- **Options** (Dark mode, alarm sounds, desktop notifications)
|
||||
- **Crypto News** (opens a news modal)
|
||||
- **Economic Calendar** (Investing.com iframe)
|
||||
- **Unlimited Alarms** management
|
||||
- **Options** (Dark mode, alarm sounds, desktop notifications)
|
||||
- **Buy me a Coffee** donation button
|
||||
- **`magic.js`**
|
||||
- **`magic.js`**
|
||||
- Core logic:
|
||||
- Fetches cryptos (`/api/cryptos`)
|
||||
- Loads alarms (`/api/alarms`) and notifications (`/api/notifications`)
|
||||
- Pulls prices from Binance/OKX every 5 seconds
|
||||
- Checks and triggers alarms
|
||||
- Fetches cryptos (`/api/cryptos`)
|
||||
- Loads alarms (`/api/alarms`) and notifications (`/api/notifications`)
|
||||
- Pulls prices from Binance/OKX every 1 seconds
|
||||
- Checks and triggers alarms
|
||||
- Handles UI rendering (prices, alarms, notifications, drag & drop reorder)
|
||||
|
||||
|
||||
### News Feed Server (Node.js)
|
||||
|
||||
- A minimal Node.js Express server (in `server.js` or similar) which:
|
||||
- Retrieves the listed RSS feeds and parses them via `xml2js`
|
||||
- Serves them in JSON format at `/api/news`
|
||||
@ -221,51 +248,60 @@ HodlEye-Crypto-Price-Tracker
|
||||
```
|
||||
- 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
|
||||
|
||||
1. **5-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.
|
||||
1. **1-second polling**
|
||||
|
||||
2. **API availability**
|
||||
- Binance/OKX may be temporarily down or might not support certain symbols.
|
||||
- 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**
|
||||
|
||||
- Binance/OKX may be temporarily down or might not support certain symbols.
|
||||
- HodlEye tries Binance → fallback to OKX if needed.
|
||||
|
||||
3. **Unlimited Alarms (Once vs. Recurring)**
|
||||
- **Once** alarms become locally “triggered” to avoid repeated alerts but are not server-side deactivated.
|
||||
3. **Unlimited Alarms (Once vs. Recurring)**
|
||||
|
||||
- **Once** alarms become locally “triggered” to avoid repeated alerts but are not server-side deactivated.
|
||||
- **Recurring** triggers repeatedly every time the threshold is crossed.
|
||||
|
||||
4. **RSS Feeds**
|
||||
- Some feeds might be inaccessible in certain regions (`bitcoin_news` in Germany, for instance).
|
||||
4. **RSS Feeds**
|
||||
- Some feeds might be inaccessible in certain regions (`bitcoin_news` in Germany, for instance).
|
||||
- Economic Calendar content is loaded from an `<iframe>` pointing to Investing.com.
|
||||
|
||||
---
|
||||
|
||||
|
||||
## Coming Soon
|
||||
|
||||
Exciting new features and improvements are on the way! Here are some planned updates:
|
||||
|
||||
- **Portfolio Management**: Track your crypto holdings in real-time with easy-to-read analytics.
|
||||
- **Portfolio Management**: Track your crypto holdings in real-time with easy-to-read analytics.
|
||||
- **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**
|
||||
- **HodlEye Notify Alarm with various sound selections and HodlEye Alarms**
|
||||
- **Adding more Responsive design**
|
||||
|
||||
|
||||
Stay tuned for updates!
|
||||
|
||||
---
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
|
||||
## Privacy & Data Disclaimer
|
||||
|
||||
- **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.
|
||||
- **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.
|
||||
|
||||
---
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
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 (image error) 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 (image error) Size: 963 B |
@ -1,258 +1,295 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>HodlEye Crypto Price Tracker</title>
|
||||
|
||||
<link rel="stylesheet" href="style.css">
|
||||
<link rel="icon" type="image/png" href="images/favicon.png">
|
||||
</head>
|
||||
<body class="dark">
|
||||
|
||||
|
||||
<link rel="stylesheet" href="style.css" />
|
||||
<link rel="stylesheet" href="responsive.css" />
|
||||
<link rel="icon" type="image/png" href="images/favicon.png" />
|
||||
</head>
|
||||
<body class="dark">
|
||||
<div class="header">
|
||||
<h1>HodlEye Crypto Price Tracker</h1>
|
||||
<div class="button-grid-container">
|
||||
<div class="grid-left">
|
||||
|
||||
<button onclick="openCryptoNews()">Crypto News</button>
|
||||
<button onclick="openEconomicCalendar()">Economic Calendar</button>
|
||||
</div>
|
||||
<div class="grid-middle">
|
||||
<button onclick="openAddCryptoModal()">Add Crypto</button>
|
||||
<button onclick="toggleEditMode()" id="editButton">Edit List</button>
|
||||
<button class="alarm-btn" onclick="openAlarmModal()">Alarms⏰</button>
|
||||
<button class="options-btn" onclick="openOptionsModal()">Options</button>
|
||||
</div>
|
||||
<div class="grid-right">
|
||||
|
||||
</div>
|
||||
<h1>HodlEye Crypto Price Tracker</h1>
|
||||
<div class="button-grid-container">
|
||||
<div class="grid-left">
|
||||
<button onclick="openCryptoNews()">Crypto News</button>
|
||||
<button onclick="openEconomicCalendar()">Economic Calendar</button>
|
||||
</div>
|
||||
<div class="grid-middle">
|
||||
<button onclick="openAddCryptoModal()">Add Crypto</button>
|
||||
<button onclick="toggleEditMode()" id="editButton">Edit List</button>
|
||||
<button class="alarm-btn" onclick="openAlarmModal()">Alarms</button>
|
||||
<button class="options-btn" onclick="openOptionsModal()">
|
||||
Options
|
||||
</button>
|
||||
</div>
|
||||
<div class="grid-right"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="main-container">
|
||||
|
||||
<div class="left-column">
|
||||
<div class="grid-container" id="cryptoGrid">
|
||||
|
||||
</div>
|
||||
<div class="left-column">
|
||||
<div class="grid-container" id="cryptoGrid"></div>
|
||||
</div>
|
||||
|
||||
<div class="right-column">
|
||||
<div class="notify-area">
|
||||
<div class="notify-heading">
|
||||
<span>Notify</span>
|
||||
<button onclick="clearNotifications()">Clear List</button>
|
||||
</div>
|
||||
<ul class="notify-list" id="notifyList"></ul>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="right-column">
|
||||
<div class="notify-heading">
|
||||
<span>Notify</span>
|
||||
<button onclick="clearNotifications()">Clear List</button>
|
||||
</div>
|
||||
<ul class="notify-list" id="notifyList">
|
||||
|
||||
</ul>
|
||||
<div class="buttons-and-version">
|
||||
<button class="coffee-btn" onclick="openBuyMeModal()">
|
||||
Buy me a Coffee
|
||||
<img src="images/coffee.svg" alt="Coffee" class="coffee-icon" />
|
||||
</button>
|
||||
|
||||
|
||||
<button class="coffee-btn" onclick="openBuyMeModal()">
|
||||
Buy me a Coffee <img src="images/coffee.svg" alt="Coffee" class="coffee-icon">
|
||||
</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 id="alarmModal" class="modal">
|
||||
<div class="modal-content">
|
||||
<span class="close" onclick="closeAlarmModal()">×</span>
|
||||
<h2>Price Alarms</h2>
|
||||
<div class="modal-content">
|
||||
<span class="close" onclick="closeAlarmModal()">×</span>
|
||||
<h2>Price Alarms</h2>
|
||||
|
||||
<div class="alarm-list-container" id="alarmListContainer">
|
||||
|
||||
</div>
|
||||
<div class="alarm-list-container" id="alarmListContainer"></div>
|
||||
|
||||
<label for="alarmSymbol">Symbol:</label>
|
||||
<select id="alarmSymbol"></select><br>
|
||||
<label for="alarmSymbol">Symbol:</label>
|
||||
<select id="alarmSymbol"></select
|
||||
><br />
|
||||
|
||||
<label for="alarmPrice">Alarm Price (USDT):</label>
|
||||
<input type="number" id="alarmPrice" placeholder="Price"><br>
|
||||
<label for="alarmPrice">Alarm Price (USDT):</label>
|
||||
<input type="number" id="alarmPrice" placeholder="Price" /><br />
|
||||
|
||||
<label for="alarmFrequency">Frequency:</label>
|
||||
<select id="alarmFrequency">
|
||||
<option value="Once">Once</option>
|
||||
<option value="Recurring">Recurring</option>
|
||||
</select><br>
|
||||
<label for="alarmFrequency">Frequency:</label>
|
||||
<select id="alarmFrequency">
|
||||
<option value="Once">Once</option>
|
||||
<option value="Recurring">Recurring</option></select
|
||||
><br />
|
||||
|
||||
<label for="alarmDirection">Direction:</label>
|
||||
<select id="alarmDirection">
|
||||
<option value="Rising">Rising</option>
|
||||
<option value="Falling">Falling</option>
|
||||
<option value="Both">Both</option>
|
||||
</select><br>
|
||||
|
||||
<button onclick="addAlarm()">Add Alarm</button>
|
||||
</div>
|
||||
<label for="alarmDirection">Direction:</label>
|
||||
<select id="alarmDirection">
|
||||
<option value="Rising">Rising</option>
|
||||
<option value="Falling">Falling</option>
|
||||
<option value="Both">Both</option></select
|
||||
><br />
|
||||
|
||||
<button onclick="addAlarm()">Add Alarm</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div id="optionsModal" class="modal">
|
||||
<div class="modal-content">
|
||||
<span class="close" onclick="closeOptionsModal()">×</span>
|
||||
<h2>Options</h2>
|
||||
<div class="modal-content">
|
||||
<span class="close" onclick="closeOptionsModal()">×</span>
|
||||
<h2>Options</h2>
|
||||
|
||||
<label for="soundSelect">Alarm Sound:</label>
|
||||
<select id="soundSelect">
|
||||
<option value="ping.mp3">ping.mp3</option>
|
||||
<option value="cashing.mp3">cashing.mp3</option>
|
||||
</select>
|
||||
<br><br>
|
||||
<label for="soundSelect">Alarm Sound:</label>
|
||||
<select id="soundSelect">
|
||||
<option value="ping.mp3">ping.mp3</option>
|
||||
<option value="cashing.mp3">cashing.mp3</option>
|
||||
</select>
|
||||
<br /><br />
|
||||
|
||||
<input type="checkbox" id="darkModeToggle">
|
||||
<label for="darkModeToggle">Enable Dark Mode</label>
|
||||
<br><br>
|
||||
<input type="checkbox" id="darkModeToggle" />
|
||||
<label for="darkModeToggle">Enable Dark Mode</label>
|
||||
<br /><br />
|
||||
|
||||
<input type="checkbox" id="desktopNotifyToggle">
|
||||
<label for="desktopNotifyToggle">Enable Desktop Notifications</label>
|
||||
<br><br>
|
||||
<input type="checkbox" id="desktopNotifyToggle" />
|
||||
<label for="desktopNotifyToggle">Enable Desktop Notifications</label>
|
||||
<br /><br />
|
||||
|
||||
<button onclick="openApiModal()">ADD/Edit OKX API</button>
|
||||
<br><br>
|
||||
<button onclick="openApiModal()">ADD/Edit OKX API</button>
|
||||
<br /><br />
|
||||
|
||||
<button onclick="saveOptions()">Save</button>
|
||||
</div>
|
||||
<button onclick="saveOptions()">Save</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div id="apiModal" class="modal">
|
||||
<div class="modal-content">
|
||||
<span class="close" onclick="closeApiModal()">×</span>
|
||||
<h2>OKX API Settings</h2>
|
||||
<p>Enter your OKX API data here (optional). Primary is Binance.
|
||||
If Binance fails, the site tries OKX (or if you force OKX).</p>
|
||||
<div class="modal-content">
|
||||
<span class="close" onclick="closeApiModal()">×</span>
|
||||
<h2>OKX API Settings</h2>
|
||||
<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>
|
||||
<input type="text" id="okxApiKey" placeholder="e.g. c42166aa-...">
|
||||
<br>
|
||||
<label for="okxApiKey">OKX API-Key:</label><br />
|
||||
<input type="text" id="okxApiKey" placeholder="e.g. c42166aa-..." />
|
||||
<br />
|
||||
|
||||
<label for="okxSecretKey">OKX Secret-Key:</label><br>
|
||||
<input type="text" id="okxSecretKey" placeholder="e.g. ACD0B07F...">
|
||||
<br>
|
||||
<label for="okxSecretKey">OKX Secret-Key:</label><br />
|
||||
<input type="text" id="okxSecretKey" placeholder="e.g. ACD0B07F..." />
|
||||
<br />
|
||||
|
||||
<label for="okxPassphrase">OKX Passphrase (if needed):</label><br>
|
||||
<input type="text" id="okxPassphrase" placeholder="(optional)">
|
||||
<br><br>
|
||||
<label for="okxPassphrase">OKX Passphrase (if needed):</label><br />
|
||||
<input type="text" id="okxPassphrase" placeholder="(optional)" />
|
||||
<br /><br />
|
||||
|
||||
<button onclick="saveApiSettings()">Save</button>
|
||||
</div>
|
||||
<button onclick="saveApiSettings()">Save</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div id="addCryptoModal" class="modal">
|
||||
<div class="modal-content">
|
||||
<span class="close" onclick="closeAddCryptoModal()">×</span>
|
||||
<h2>Add a New Cryptocurrency</h2>
|
||||
<label for="newCryptoSymbol">Symbol (e.g. ETH):</label>
|
||||
<input type="text" id="newCryptoSymbol" placeholder="BTC, ETH, ADA..."><br>
|
||||
<br><br>
|
||||
<button onclick="addNewCrypto()">Add</button>
|
||||
</div>
|
||||
<div class="modal-content">
|
||||
<span class="close" onclick="closeAddCryptoModal()">×</span>
|
||||
<h2>Add a New Cryptocurrency</h2>
|
||||
<label for="newCryptoSymbol">Symbol (e.g. ETH):</label>
|
||||
<input
|
||||
type="text"
|
||||
id="newCryptoSymbol"
|
||||
placeholder="BTC, ETH, ADA..."
|
||||
/><br />
|
||||
<br /><br />
|
||||
<button onclick="addNewCrypto()">Add</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div id="apiSelectModal" class="modal">
|
||||
<div class="modal-content">
|
||||
<span class="close" onclick="closeApiSelectModal()">×</span>
|
||||
<h2>API Selection for <span id="apiSelectSymbol"></span></h2>
|
||||
<label for="apiSelectDropdown">Please choose:</label>
|
||||
<select id="apiSelectDropdown">
|
||||
<option value="auto">Auto (Binance -> OKX fallback)</option>
|
||||
<option value="binance">Force Binance</option>
|
||||
<option value="okx">Force OKX</option>
|
||||
</select>
|
||||
<br><br>
|
||||
<button onclick="saveApiSelection()">Save</button>
|
||||
</div>
|
||||
<div class="modal-content">
|
||||
<span class="close" onclick="closeApiSelectModal()">×</span>
|
||||
<h2>API Selection for <span id="apiSelectSymbol"></span></h2>
|
||||
<label for="apiSelectDropdown">Please choose:</label>
|
||||
<select id="apiSelectDropdown">
|
||||
<option value="auto">Auto (Binance -> OKX fallback)</option>
|
||||
<option value="binance">Force Binance</option>
|
||||
<option value="okx">Force OKX</option>
|
||||
</select>
|
||||
<br /><br />
|
||||
<button onclick="saveApiSelection()">Save</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div id="buyMeModal" class="modal">
|
||||
<div class="modal-content">
|
||||
<span class="close" onclick="closeBuyMeModal()">×</span>
|
||||
<h2>Buy me a Coffee</h2>
|
||||
<p><strong>Send me:</strong> USDT / Ethereum</p>
|
||||
<p><strong>Chain:</strong>
|
||||
<span style="text-decoration: underline; cursor: pointer;"
|
||||
onclick="copyToClipboard('0x26c2E3F6C854Af006520ec2ce433982866bB7632')">
|
||||
ETH
|
||||
</span> /
|
||||
<span style="text-decoration: underline; cursor: pointer;"
|
||||
onclick="copyToClipboard('0x26c2E3F6C854Af006520ec2ce433982866bB7632')">
|
||||
BSC
|
||||
</span>
|
||||
</p>
|
||||
<p>
|
||||
<span style="text-decoration: underline; cursor: pointer;"
|
||||
onclick="copyToClipboard('0x26c2E3F6C854Af006520ec2ce433982866bB7632')">
|
||||
0x26c2E3F6C854Af006520ec2ce433982866bB7632
|
||||
</span>
|
||||
</p>
|
||||
<p>(When clicked, the address is copied to the clipboard.)</p>
|
||||
</div>
|
||||
<div class="modal-content">
|
||||
<span class="close" onclick="closeBuyMeModal()">×</span>
|
||||
<h2>Buy me a Coffee</h2>
|
||||
<p><strong>Send me:</strong> USDT / Ethereum</p>
|
||||
<p>
|
||||
<strong>Chain:</strong>
|
||||
<span
|
||||
style="text-decoration: underline; cursor: pointer"
|
||||
onclick="copyToClipboard('0x26c2E3F6C854Af006520ec2ce433982866bB7632')"
|
||||
>
|
||||
ETH
|
||||
</span>
|
||||
/
|
||||
<span
|
||||
style="text-decoration: underline; cursor: pointer"
|
||||
onclick="copyToClipboard('0x26c2E3F6C854Af006520ec2ce433982866bB7632')"
|
||||
>
|
||||
BSC
|
||||
</span>
|
||||
</p>
|
||||
<p>
|
||||
<span
|
||||
style="text-decoration: underline; cursor: pointer"
|
||||
onclick="copyToClipboard('0x26c2E3F6C854Af006520ec2ce433982866bB7632')"
|
||||
>
|
||||
0x26c2E3F6C854Af006520ec2ce433982866bB7632
|
||||
</span>
|
||||
</p>
|
||||
<p>(When clicked, the address is copied to the clipboard.)</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div id="alarmOverlay" class="alert-overlay">
|
||||
<div class="alert-box">
|
||||
<div id="alarmMessage"></div>
|
||||
<button onclick="closeAlarmPopup()">Close</button>
|
||||
</div>
|
||||
<div class="alert-box">
|
||||
<div id="alarmMessage"></div>
|
||||
<button onclick="closeAlarmPopup()">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div id="errorOverlay" class="error-overlay">
|
||||
<div class="error-box">
|
||||
<p id="errorMessage"></p>
|
||||
<button onclick="closeErrorPopup()">Ok</button>
|
||||
</div>
|
||||
<div class="error-box">
|
||||
<p id="errorMessage"></p>
|
||||
<button onclick="closeErrorPopup()">Ok</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<audio id="alarmSound">
|
||||
<source src="sound/ping.mp3" type="audio/mpeg">
|
||||
<source src="sound/ping.mp3" type="audio/mpeg" />
|
||||
</audio>
|
||||
|
||||
|
||||
<div id="cryptoNewsModal" class="modal">
|
||||
<div class="modal-content">
|
||||
<span class="close" onclick="closeCryptoNewsModal()">×</span>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
|
||||
<div id="economicCalendarModal" class="modal">
|
||||
<div class="modal-content">
|
||||
<span class="close" onclick="closeEconomicCalendarModal()">×</span>
|
||||
<span class="close" onclick="closeEconomicCalendarModal()"
|
||||
>×</span
|
||||
>
|
||||
<h2>Economic Calendar</h2>
|
||||
<div class="iframe-container">
|
||||
<div class="iframe-wrapper">
|
||||
<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"
|
||||
allowtransparency="true">
|
||||
<div class="iframe-wrapper">
|
||||
<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"
|
||||
allowtransparency="true"
|
||||
>
|
||||
</iframe>
|
||||
</div>
|
||||
<div class="poweredBy">
|
||||
<span>Real Time Economic Calendar provided by
|
||||
<a href="https://www.investing.com/" rel="nofollow" target="_blank" class="underline_link">Investing.com</a>.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="poweredBy">
|
||||
<span
|
||||
>Real Time Economic Calendar provided by
|
||||
<a
|
||||
href="https://www.investing.com/"
|
||||
rel="nofollow"
|
||||
target="_blank"
|
||||
class="underline_link"
|
||||
>Investing.com</a
|
||||
>.
|
||||
</span>
|
||||
</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="news.js"></script>
|
||||
</body>
|
||||
<script src="update.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
1110
public/magic.js
1110
public/magic.js
File diff suppressed because it is too large
Load Diff
@ -4,17 +4,21 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
const searchInput = document.getElementById("search");
|
||||
let allArticles = [];
|
||||
|
||||
fetch(apiUrl)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
allArticles = data;
|
||||
displayArticles(allArticles);
|
||||
})
|
||||
.catch(error => {
|
||||
newsFeed.innerHTML = "Error loading the news.";
|
||||
console.error("Error when retrieving the news:", error);
|
||||
});
|
||||
|
||||
function refreshNewsFeed() {
|
||||
fetch(apiUrl)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
allArticles = data;
|
||||
displayArticles(allArticles);
|
||||
})
|
||||
.catch(error => {
|
||||
newsFeed.innerHTML = "Error loading the news.";
|
||||
console.error("Error when retrieving the news:", error);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function displayArticles(items) {
|
||||
newsFeed.innerHTML = "";
|
||||
items.forEach(item => {
|
||||
@ -30,11 +34,11 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
newsFeed.appendChild(newsItem);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
searchInput.addEventListener("input", function () {
|
||||
const searchTerm = searchInput.value.toLowerCase();
|
||||
const filteredArticles = allArticles.filter(item =>
|
||||
@ -44,6 +48,7 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
displayArticles(filteredArticles);
|
||||
});
|
||||
|
||||
|
||||
function getTimeAgo(date) {
|
||||
const now = new Date();
|
||||
const seconds = Math.floor((now - new Date(date)) / 1000);
|
||||
@ -59,6 +64,7 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
return `vor ${Math.floor(days / 365)} Jahren`;
|
||||
}
|
||||
|
||||
|
||||
function formatSourceName(source) {
|
||||
const sourceMap = {
|
||||
"crypto_news": "Crypto News",
|
||||
@ -70,4 +76,8 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
};
|
||||
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;
|
||||
}
|
||||
}
|
251
public/style.css
251
public/style.css
@ -1,12 +1,13 @@
|
||||
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
margin: 0 auto; /* Zentrierung */
|
||||
margin: 0 auto;
|
||||
padding: 0;
|
||||
transition: background-color 0.4s, color 0.4s;
|
||||
min-width: 1400px;
|
||||
max-width: 1978px;
|
||||
|
||||
max-width: 1778px;
|
||||
min-width: 808px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 28px;
|
||||
}
|
||||
@ -16,6 +17,7 @@ body.dark {
|
||||
background-color: #121212;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
body.light {
|
||||
background-color: #f2f2f2;
|
||||
color: #000000;
|
||||
@ -26,9 +28,11 @@ body.light {
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.button-container {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 10px;
|
||||
margin: 5px;
|
||||
@ -38,31 +42,68 @@ button {
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
border-radius: 5px;
|
||||
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background-color: #005f73;
|
||||
}
|
||||
.alarm-btn { background-color: #ff8b00; }
|
||||
.alarm-btn:hover { background-color: #e38806; }
|
||||
.options-btn { background-color: #555; }
|
||||
.options-btn:hover { background-color: #777; }
|
||||
|
||||
.alarm-btn {
|
||||
background-color: #ff8b00;
|
||||
}
|
||||
|
||||
.alarm-btn:hover {
|
||||
background-color: #e38806;
|
||||
}
|
||||
|
||||
.options-btn {
|
||||
background-color: #555;
|
||||
}
|
||||
|
||||
.options-btn:hover {
|
||||
background-color: #777;
|
||||
}
|
||||
|
||||
|
||||
.main-container {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.left-column {
|
||||
width: 75%;
|
||||
padding: 20px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.right-column {
|
||||
width: 320px;
|
||||
|
||||
display: grid;
|
||||
|
||||
grid-template-rows: 300px auto;
|
||||
border-left: 1px solid #444;
|
||||
width: 320px;
|
||||
padding: 20px;
|
||||
|
||||
}
|
||||
|
||||
.notify-area {
|
||||
min-height: 400px;
|
||||
max-width: 280px;
|
||||
border: 1px solid #444;
|
||||
overflow-y: auto;
|
||||
padding: 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.buttons-and-version {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
max-width: 260px;
|
||||
padding-left: 6%;
|
||||
}
|
||||
|
||||
|
||||
.notify-heading {
|
||||
display: flex;
|
||||
@ -71,6 +112,7 @@ button:hover {
|
||||
font-size: 20px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.notify-list {
|
||||
list-style: none;
|
||||
padding-left: 0;
|
||||
@ -83,12 +125,14 @@ button:hover {
|
||||
|
||||
.grid-container {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
grid-template-columns: repeat(5, 1fr);
|
||||
gap: 10px;
|
||||
width: 90%;
|
||||
|
||||
width: 80%;
|
||||
margin: auto;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.crypto-box {
|
||||
background-color: #1e1e1e;
|
||||
border: 2px solid #444;
|
||||
@ -98,15 +142,22 @@ button:hover {
|
||||
font-size: 16px;
|
||||
position: relative;
|
||||
user-select: none;
|
||||
min-width: 177px;
|
||||
}
|
||||
|
||||
body.light .crypto-box {
|
||||
background-color: #ffffff;
|
||||
border: 2px solid #ccc;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.change.up { color: #00ff00; }
|
||||
.change.down { color: #ff0000; }
|
||||
.change.up {
|
||||
color: #00ff00;
|
||||
}
|
||||
|
||||
.change.down {
|
||||
color: #ff0000;
|
||||
}
|
||||
|
||||
|
||||
.delete-btn {
|
||||
@ -125,6 +176,7 @@ body.light .crypto-box {
|
||||
line-height: 24px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.delete-btn:hover {
|
||||
background-color: #999;
|
||||
color: #000;
|
||||
@ -135,6 +187,7 @@ body.light .crypto-box {
|
||||
opacity: 0.5;
|
||||
transform: scale(0.97);
|
||||
}
|
||||
|
||||
.crypto-box.drag-over {
|
||||
border: 2px dashed #ccc !important;
|
||||
}
|
||||
@ -144,49 +197,52 @@ body.light .crypto-box {
|
||||
display: none;
|
||||
position: fixed;
|
||||
z-index: 2;
|
||||
left: 0; top: 0;
|
||||
width: 100%; height: 100%;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0, 0, 0, 0.7);
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background-color: #222;
|
||||
background-color: #222222f2;
|
||||
margin: 5% auto;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
|
||||
text-align: center;
|
||||
border-radius: 8px;
|
||||
max-width: 40%;
|
||||
text-align: center;
|
||||
color: white;
|
||||
|
||||
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
position: relative;
|
||||
box-shadow: 0px 4px 10px rgba(0, 0, 0, 0.3);
|
||||
position: relative;
|
||||
box-shadow: 0px 4px 10px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
|
||||
#cryptoNewsModal .modal-content {
|
||||
width: 40%;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
max-height: 80vh;
|
||||
overflow-y: auto;
|
||||
max-height: 80vh;
|
||||
}
|
||||
|
||||
|
||||
|
||||
#buyMeModal .modal-content {
|
||||
|
||||
|
||||
width: 30%;
|
||||
overflow-x: hidden;
|
||||
|
||||
text-align: center;
|
||||
|
||||
text-align: center;
|
||||
position: absolute;
|
||||
top: 35%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
transform: translate(-50%, -50%);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
|
||||
@ -199,28 +255,28 @@ body.light .crypto-box {
|
||||
}
|
||||
|
||||
#optionsModal .modal-content {
|
||||
|
||||
width: 20%;
|
||||
|
||||
width: 30%;
|
||||
overflow-x: hidden;
|
||||
|
||||
|
||||
}
|
||||
|
||||
#addCryptoModal .modal-content {
|
||||
width: 20%;
|
||||
text-align: center;
|
||||
width: 20%;
|
||||
text-align: center;
|
||||
position: absolute;
|
||||
top: 35%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
transform: translate(-50%, -50%);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
#addCryptoModal .modal-content label {
|
||||
display: block;
|
||||
margin-bottom: 6px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
#addCryptoModal .close {
|
||||
@ -235,43 +291,44 @@ body.light .crypto-box {
|
||||
|
||||
#addCryptoModal .modal-content select,
|
||||
#addCryptoModal .modal-content input {
|
||||
width: 100%;
|
||||
height: 20px;
|
||||
font-size: 16px;
|
||||
width: 100%;
|
||||
height: 20px;
|
||||
font-size: 16px;
|
||||
padding: 8px;
|
||||
border: 1px solid #ccc;
|
||||
box-sizing: border-box;
|
||||
border: 1px solid #ccc;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
|
||||
#alarmModal .modal-content {
|
||||
width: 30%;
|
||||
text-align: center;
|
||||
min-width: 406px;
|
||||
text-align: center;
|
||||
position: absolute;
|
||||
top: 35%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
transform: translate(-50%, -45%);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
#alarmModal .modal-content label {
|
||||
display: block;
|
||||
margin-bottom: 3px;
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
|
||||
#alarmModal .modal-content select,
|
||||
#alarmModal .modal-content input {
|
||||
margin-bottom: 8px;
|
||||
width: 100%;
|
||||
margin-bottom: 8px;
|
||||
width: 30%;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
#alarmModal .modal-content select {
|
||||
padding: 8px;
|
||||
padding: 8px;
|
||||
font-size: 13px;
|
||||
width: 100%;
|
||||
width: 30%;
|
||||
}
|
||||
|
||||
#alarmModal .close {
|
||||
@ -289,17 +346,23 @@ body.light .modal-content {
|
||||
background-color: #ddd;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.close {
|
||||
float: right;
|
||||
font-size: 24px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
label {
|
||||
display: inline-block;
|
||||
margin-top: 10px;
|
||||
}
|
||||
select, input[type="number"], input[type="text"] {
|
||||
|
||||
select,
|
||||
input[type="number"],
|
||||
input[type="text"] {
|
||||
margin: 5px 0 10px 5px;
|
||||
font-size: medium;
|
||||
}
|
||||
|
||||
|
||||
@ -307,10 +370,13 @@ select, input[type="number"], input[type="text"] {
|
||||
display: none;
|
||||
position: fixed;
|
||||
z-index: 3;
|
||||
left: 0; top: 0;
|
||||
width: 100%; height: 100%;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0, 0, 0, 0.7);
|
||||
}
|
||||
|
||||
.alert-box {
|
||||
background-color: #333;
|
||||
color: #fff;
|
||||
@ -321,13 +387,16 @@ select, input[type="number"], input[type="text"] {
|
||||
text-align: center;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.alert-box button {
|
||||
margin-top: 15px;
|
||||
background-color: #555;
|
||||
}
|
||||
|
||||
.alert-box button:hover {
|
||||
background-color: #777;
|
||||
}
|
||||
|
||||
body.light .alert-box {
|
||||
background-color: #ccc;
|
||||
color: #000;
|
||||
@ -338,10 +407,13 @@ body.light .alert-box {
|
||||
display: none;
|
||||
position: fixed;
|
||||
z-index: 4;
|
||||
left: 0; top: 0;
|
||||
width: 100%; height: 100%;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0, 0, 0, 0.7);
|
||||
}
|
||||
|
||||
.error-box {
|
||||
background-color: #aa3333;
|
||||
color: #fff;
|
||||
@ -352,6 +424,7 @@ body.light .alert-box {
|
||||
text-align: center;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
body.light .error-box {
|
||||
background-color: #f2aaaa;
|
||||
color: #000;
|
||||
@ -365,6 +438,7 @@ body.light .error-box {
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
body.light .api-label {
|
||||
color: #333;
|
||||
}
|
||||
@ -378,6 +452,7 @@ body.light .api-label {
|
||||
overflow-y: auto;
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
.alarm-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@ -386,10 +461,12 @@ body.light .api-label {
|
||||
padding: 6px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
body.light .alarm-item {
|
||||
background-color: #eee;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.alarm-delete-btn {
|
||||
background-color: #008CBA;
|
||||
border: none;
|
||||
@ -398,6 +475,7 @@ body.light .alarm-item {
|
||||
border-radius: 5px;
|
||||
padding: 5px 8px;
|
||||
}
|
||||
|
||||
.alarm-delete-btn:hover {
|
||||
background-color: #005f73;
|
||||
}
|
||||
@ -412,17 +490,19 @@ body.light .alarm-item {
|
||||
|
||||
.coffee-btn {
|
||||
margin-top: 60px;
|
||||
background-color: #614484;
|
||||
font-family: 'Bree Serif', serif;
|
||||
background-color: #614484;
|
||||
font-family: 'Bree Serif', serif;
|
||||
font-size: 16px;
|
||||
color: white;
|
||||
padding: 10px 20px;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.coffee-btn:hover {
|
||||
background-color: #711fe9;
|
||||
}
|
||||
|
||||
.coffee-icon {
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
@ -443,21 +523,26 @@ body.light .alarm-item {
|
||||
color: white;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#news-feed {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.news-item {
|
||||
padding: 15px;
|
||||
border-bottom: 1px solid #333;
|
||||
transition: background 0.3s;
|
||||
}
|
||||
|
||||
.news-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.news-item:hover {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.news-title {
|
||||
font-size: 18px;
|
||||
color: #1db954;
|
||||
@ -465,6 +550,7 @@ body.light .alarm-item {
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.news-meta {
|
||||
font-size: 14px;
|
||||
color: gray;
|
||||
@ -472,10 +558,12 @@ body.light .alarm-item {
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.news-source {
|
||||
font-weight: bold;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.time {
|
||||
font-size: 14px;
|
||||
color: gray;
|
||||
@ -484,18 +572,14 @@ body.light .alarm-item {
|
||||
|
||||
.button-grid-container {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 2fr 1fr;
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
/* alle gleich breit */
|
||||
align-items: center;
|
||||
justify-items: center;
|
||||
gap: 10px;
|
||||
column-gap: 10px;
|
||||
/* oder kleiner */
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
.grid-left, .grid-middle, .grid-right {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
|
||||
#economicCalendarModal .iframe-container {
|
||||
@ -510,6 +594,7 @@ body.light .alarm-item {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
#economicCalendarModal .iframe-wrapper {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
@ -520,12 +605,14 @@ body.light .alarm-item {
|
||||
background: white;
|
||||
box-shadow: 0px 4px 10px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
#economicCalendarModal iframe {
|
||||
width: calc(100% - 20px);
|
||||
height: calc(100% - 20px);
|
||||
border: none;
|
||||
display: block;
|
||||
}
|
||||
|
||||
#economicCalendarModal .poweredBy {
|
||||
margin-top: 10px;
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
@ -533,6 +620,7 @@ body.light .alarm-item {
|
||||
color: #ffffff;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#economicCalendarModal .poweredBy a {
|
||||
color: #06529D;
|
||||
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 WebSocket = require("ws");
|
||||
|
||||
|
||||
const app = express();
|
||||
app.use(cors());
|
||||
app.use(express.json());
|
||||
@ -16,7 +17,7 @@ const clients = new Set();
|
||||
|
||||
/*
|
||||
* Metadata
|
||||
* Version: 1.0.5
|
||||
* Version: 1.0.6
|
||||
* Author/Dev: Gerald Hasani
|
||||
* Name: HodlEye Crypto Price Tracker
|
||||
* Email: contact@gerald-hasani.com
|
||||
@ -25,12 +26,20 @@ const clients = new Set();
|
||||
|
||||
const DATA_FILE = path.join(__dirname, "..", "data", "data.json");
|
||||
|
||||
|
||||
if (!fs.existsSync(DATA_FILE)) {
|
||||
fs.writeFileSync(DATA_FILE, JSON.stringify({
|
||||
cryptos: ["BTC"],
|
||||
alarms: [],
|
||||
notifications: []
|
||||
}, null, 2));
|
||||
fs.writeFileSync(
|
||||
DATA_FILE,
|
||||
JSON.stringify(
|
||||
{
|
||||
cryptos: ["BTC"],
|
||||
alarms: [],
|
||||
notifications: []
|
||||
},
|
||||
null,
|
||||
2
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function readData() {
|
||||
@ -41,6 +50,23 @@ function writeData(data) {
|
||||
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) => {
|
||||
const data = readData();
|
||||
res.json(data.cryptos);
|
||||
@ -51,10 +77,8 @@ app.post("/api/cryptos", (req, res) => {
|
||||
if (!symbol) {
|
||||
return res.status(400).json({ error: "Symbol is required." });
|
||||
}
|
||||
|
||||
const data = readData();
|
||||
const upperSymbol = symbol.toUpperCase();
|
||||
|
||||
if (!data.cryptos.includes(upperSymbol)) {
|
||||
data.cryptos.push(upperSymbol);
|
||||
writeData(data);
|
||||
@ -70,6 +94,17 @@ app.delete("/api/cryptos/:symbol", (req, res) => {
|
||||
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) => {
|
||||
const data = readData();
|
||||
res.json(data.alarms);
|
||||
@ -80,7 +115,6 @@ app.post("/api/alarms", (req, res) => {
|
||||
if (!symbol || !price) {
|
||||
return res.status(400).json({ error: "symbol and price are required." });
|
||||
}
|
||||
|
||||
const data = readData();
|
||||
const newAlarm = {
|
||||
id: Date.now(),
|
||||
@ -113,7 +147,6 @@ app.post("/api/notifications", (req, res) => {
|
||||
if (!message) {
|
||||
return res.status(400).json({ error: "message is required." });
|
||||
}
|
||||
|
||||
const data = readData();
|
||||
const entry = {
|
||||
message,
|
||||
@ -122,7 +155,7 @@ app.post("/api/notifications", (req, res) => {
|
||||
data.notifications.unshift(entry);
|
||||
writeData(data);
|
||||
|
||||
// Nachricht an alle WebSocket-Clients senden
|
||||
|
||||
clients.forEach((client) => {
|
||||
if (client.readyState === WebSocket.OPEN) {
|
||||
client.send(JSON.stringify(entry));
|
||||
@ -141,18 +174,15 @@ app.delete("/api/notifications", (req, res) => {
|
||||
|
||||
app.use(express.static(path.join(__dirname, "..", "public")));
|
||||
|
||||
// WebSocket-Server für Echtzeitbenachrichtigungen
|
||||
wss.on("connection", (ws) => {
|
||||
console.log("Client verbunden");
|
||||
clients.add(ws);
|
||||
|
||||
ws.on("close", () => {
|
||||
console.log("Client getrennt");
|
||||
clients.delete(ws);
|
||||
});
|
||||
console.log("Client verbunden");
|
||||
clients.add(ws);
|
||||
ws.on("close", () => {
|
||||
console.log("Client getrennt");
|
||||
clients.delete(ws);
|
||||
});
|
||||
});
|
||||
|
||||
// Server starten
|
||||
const PORT = process.env.PORT || 3099;
|
||||
server.listen(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