Compare commits

...

10 Commits

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

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

View File

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

210
README.md
View File

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

8
data/data.json Normal file
View File

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

View File

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

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

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

After

Width:  |  Height:  |  Size: 963 B

View File

@ -1,258 +1,295 @@
<!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" />
</head> <link rel="icon" type="image/png" href="images/favicon.png" />
<body class="dark"> </head>
<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 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="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>
<div class="main-container"> <div class="main-container">
<div class="left-column">
<div class="left-column"> <div class="grid-container" id="cryptoGrid"></div>
<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>
<div class="buttons-and-version">
<div class="right-column"> <button class="coffee-btn" onclick="openBuyMeModal()">
<div class="notify-heading"> Buy me a Coffee
<span>Notify</span> <img src="images/coffee.svg" alt="Coffee" class="coffee-icon" />
<button onclick="clearNotifications()">Clear List</button> </button>
</div>
<ul class="notify-list" id="notifyList">
</ul>
<button
<button class="coffee-btn" onclick="openBuyMeModal()"> class="my-button"
Buy me a Coffee <img src="images/coffee.svg" alt="Coffee" class="coffee-icon"> onclick="window.open('https://github.com/Gerald-Ha/HodlEye-Crypto-Price-Tracker', '_blank')"
</button> >
Github
<img
src="images/github-mark.svg"
alt="Git Logo"
class="coffee-icon"
/>
</button>
<div class="version-info">
<button onclick="window.open('https://www.gerald-hasani.com', '_blank')">Developer</button> <span id="currentVersion">Version 1.0.6</span>
<span
id="updateAvailable"
style="display: none; color: red; cursor: pointer"
>
Update Available
</span>
</div>
</div> </div>
</div>
</div> </div>
<div id="alarmModal" class="modal"> <div id="alarmModal" class="modal">
<div class="modal-content"> <div class="modal-content">
<span class="close" onclick="closeAlarmModal()">&times;</span> <span class="close" onclick="closeAlarmModal()">&times;</span>
<h2>Price Alarms</h2> <h2>Price Alarms</h2>
<div class="alarm-list-container" id="alarmListContainer"> <div class="alarm-list-container" id="alarmListContainer"></div>
</div>
<label for="alarmSymbol">Symbol:</label> <label for="alarmSymbol">Symbol:</label>
<select id="alarmSymbol"></select><br> <select id="alarmSymbol"></select
><br />
<label for="alarmPrice">Alarm Price (USDT):</label> <label for="alarmPrice">Alarm Price (USDT):</label>
<input type="number" id="alarmPrice" placeholder="Price"><br> <input type="number" id="alarmPrice" placeholder="Price" /><br />
<label for="alarmFrequency">Frequency:</label> <label for="alarmFrequency">Frequency:</label>
<select id="alarmFrequency"> <select id="alarmFrequency">
<option value="Once">Once</option> <option value="Once">Once</option>
<option value="Recurring">Recurring</option> <option value="Recurring">Recurring</option></select
</select><br> ><br />
<label for="alarmDirection">Direction:</label> <label for="alarmDirection">Direction:</label>
<select id="alarmDirection"> <select id="alarmDirection">
<option value="Rising">Rising</option> <option value="Rising">Rising</option>
<option value="Falling">Falling</option> <option value="Falling">Falling</option>
<option value="Both">Both</option> <option value="Both">Both</option></select
</select><br> ><br />
<button onclick="addAlarm()">Add Alarm</button> <button onclick="addAlarm()">Add Alarm</button>
</div> </div>
</div> </div>
<div id="optionsModal" class="modal"> <div id="optionsModal" class="modal">
<div class="modal-content"> <div class="modal-content">
<span class="close" onclick="closeOptionsModal()">&times;</span> <span class="close" onclick="closeOptionsModal()">&times;</span>
<h2>Options</h2> <h2>Options</h2>
<label for="soundSelect">Alarm Sound:</label> <label for="soundSelect">Alarm Sound:</label>
<select id="soundSelect"> <select id="soundSelect">
<option value="ping.mp3">ping.mp3</option> <option value="ping.mp3">ping.mp3</option>
<option value="cashing.mp3">cashing.mp3</option> <option value="cashing.mp3">cashing.mp3</option>
</select> </select>
<br><br> <br /><br />
<input type="checkbox" id="darkModeToggle"> <input type="checkbox" id="darkModeToggle" />
<label for="darkModeToggle">Enable Dark Mode</label> <label for="darkModeToggle">Enable Dark Mode</label>
<br><br> <br /><br />
<input type="checkbox" id="desktopNotifyToggle"> <input type="checkbox" id="desktopNotifyToggle" />
<label for="desktopNotifyToggle">Enable Desktop Notifications</label> <label for="desktopNotifyToggle">Enable Desktop Notifications</label>
<br><br> <br /><br />
<button onclick="openApiModal()">ADD/Edit OKX API</button> <button onclick="openApiModal()">ADD/Edit OKX API</button>
<br><br> <br /><br />
<button onclick="saveOptions()">Save</button> <button onclick="saveOptions()">Save</button>
</div> </div>
</div> </div>
<div id="apiModal" class="modal"> <div id="apiModal" class="modal">
<div class="modal-content"> <div class="modal-content">
<span class="close" onclick="closeApiModal()">&times;</span> <span class="close" onclick="closeApiModal()">&times;</span>
<h2>OKX API Settings</h2> <h2>OKX API Settings</h2>
<p>Enter your OKX API data here (optional). Primary is Binance. <p>
If Binance fails, the site tries OKX (or if you force OKX).</p> Enter your OKX API data here (optional). Primary is Binance. If
Binance fails, the site tries OKX (or if you force OKX).
</p>
<label for="okxApiKey">OKX API-Key:</label><br> <label for="okxApiKey">OKX API-Key:</label><br />
<input type="text" id="okxApiKey" placeholder="e.g. c42166aa-..."> <input type="text" id="okxApiKey" placeholder="e.g. c42166aa-..." />
<br> <br />
<label for="okxSecretKey">OKX Secret-Key:</label><br> <label for="okxSecretKey">OKX Secret-Key:</label><br />
<input type="text" id="okxSecretKey" placeholder="e.g. ACD0B07F..."> <input type="text" id="okxSecretKey" placeholder="e.g. ACD0B07F..." />
<br> <br />
<label for="okxPassphrase">OKX Passphrase (if needed):</label><br> <label for="okxPassphrase">OKX Passphrase (if needed):</label><br />
<input type="text" id="okxPassphrase" placeholder="(optional)"> <input type="text" id="okxPassphrase" placeholder="(optional)" />
<br><br> <br /><br />
<button onclick="saveApiSettings()">Save</button> <button onclick="saveApiSettings()">Save</button>
</div> </div>
</div> </div>
<div id="addCryptoModal" class="modal"> <div id="addCryptoModal" class="modal">
<div class="modal-content"> <div class="modal-content">
<span class="close" onclick="closeAddCryptoModal()">&times;</span> <span class="close" onclick="closeAddCryptoModal()">&times;</span>
<h2>Add a New Cryptocurrency</h2> <h2>Add a New Cryptocurrency</h2>
<label for="newCryptoSymbol">Symbol (e.g. ETH):</label> <label for="newCryptoSymbol">Symbol (e.g. ETH):</label>
<input type="text" id="newCryptoSymbol" placeholder="BTC, ETH, ADA..."><br> <input
<br><br> type="text"
<button onclick="addNewCrypto()">Add</button> id="newCryptoSymbol"
</div> placeholder="BTC, ETH, ADA..."
/><br />
<br /><br />
<button onclick="addNewCrypto()">Add</button>
</div>
</div> </div>
<div id="apiSelectModal" class="modal"> <div id="apiSelectModal" class="modal">
<div class="modal-content"> <div class="modal-content">
<span class="close" onclick="closeApiSelectModal()">&times;</span> <span class="close" onclick="closeApiSelectModal()">&times;</span>
<h2>API Selection for <span id="apiSelectSymbol"></span></h2> <h2>API Selection for <span id="apiSelectSymbol"></span></h2>
<label for="apiSelectDropdown">Please choose:</label> <label for="apiSelectDropdown">Please choose:</label>
<select id="apiSelectDropdown"> <select id="apiSelectDropdown">
<option value="auto">Auto (Binance -> OKX fallback)</option> <option value="auto">Auto (Binance -> OKX fallback)</option>
<option value="binance">Force Binance</option> <option value="binance">Force Binance</option>
<option value="okx">Force OKX</option> <option value="okx">Force OKX</option>
</select> </select>
<br><br> <br /><br />
<button onclick="saveApiSelection()">Save</button> <button onclick="saveApiSelection()">Save</button>
</div> </div>
</div> </div>
<div id="buyMeModal" class="modal"> <div id="buyMeModal" class="modal">
<div class="modal-content"> <div class="modal-content">
<span class="close" onclick="closeBuyMeModal()">&times;</span> <span class="close" onclick="closeBuyMeModal()">&times;</span>
<h2>Buy me a Coffee</h2> <h2>Buy me a Coffee</h2>
<p><strong>Send me:</strong> USDT / Ethereum</p> <p><strong>Send me:</strong> USDT / Ethereum</p>
<p><strong>Chain:</strong> <p>
<span style="text-decoration: underline; cursor: pointer;" <strong>Chain:</strong>
onclick="copyToClipboard('0x26c2E3F6C854Af006520ec2ce433982866bB7632')"> <span
ETH style="text-decoration: underline; cursor: pointer"
</span> / onclick="copyToClipboard('0x26c2E3F6C854Af006520ec2ce433982866bB7632')"
<span style="text-decoration: underline; cursor: pointer;" >
onclick="copyToClipboard('0x26c2E3F6C854Af006520ec2ce433982866bB7632')"> ETH
BSC </span>
</span> /
</p> <span
<p> style="text-decoration: underline; cursor: pointer"
<span style="text-decoration: underline; cursor: pointer;" onclick="copyToClipboard('0x26c2E3F6C854Af006520ec2ce433982866bB7632')"
onclick="copyToClipboard('0x26c2E3F6C854Af006520ec2ce433982866bB7632')"> >
0x26c2E3F6C854Af006520ec2ce433982866bB7632 BSC
</span> </span>
</p> </p>
<p>(When clicked, the address is copied to the clipboard.)</p> <p>
</div> <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>
<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>
<button onclick="closeAlarmPopup()">Close</button> <button onclick="closeAlarmPopup()">Close</button>
</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>
<button onclick="closeErrorPopup()">Ok</button> <button onclick="closeErrorPopup()">Ok</button>
</div> </div>
</div> </div>
<audio id="alarmSound"> <audio id="alarmSound">
<source src="sound/ping.mp3" type="audio/mpeg"> <source src="sound/ping.mp3" type="audio/mpeg" />
</audio> </audio>
<div id="cryptoNewsModal" class="modal"> <div id="cryptoNewsModal" class="modal">
<div class="modal-content"> <div class="modal-content">
<span class="close" onclick="closeCryptoNewsModal()">&times;</span> <span class="close" onclick="closeCryptoNewsModal()">&times;</span>
<h2>Crypto News</h2> <h2>Crypto News</h2>
<input type="text" id="search" placeholder="Suche nach Artikeln..."> <input type="text" id="search" placeholder="Suche nach Artikeln..." />
<div id="news-feed">Lade News...</div> <div id="news-feed">Lade News...</div>
</div> </div>
</div> </div>
<div id="economicCalendarModal" class="modal"> <div id="economicCalendarModal" class="modal">
<div class="modal-content"> <div class="modal-content">
<span class="close" onclick="closeEconomicCalendarModal()">&times;</span> <span class="close" onclick="closeEconomicCalendarModal()"
>&times;</span
>
<h2>Economic Calendar</h2> <h2>Economic Calendar</h2>
<div class="iframe-container"> <div class="iframe-container">
<div class="iframe-wrapper"> <div class="iframe-wrapper">
<iframe <iframe
src="https://sslecal2.investing.com?columns=exc_flags,exc_currency,exc_importance,exc_actual,exc_forecast,exc_previous&features=datepicker,timezone&countries=25,32,6,37,72,22,17,39,14,10,35,43,56,36,110,11,26,12,4,5&calType=week&timeZone=8&lang=1" src="https://sslecal2.investing.com?columns=exc_flags,exc_currency,exc_importance,exc_actual,exc_forecast,exc_previous&features=datepicker,timezone&countries=25,32,6,37,72,22,17,39,14,10,35,43,56,36,110,11,26,12,4,5&calType=week&timeZone=8&lang=1"
allowtransparency="true"> allowtransparency="true"
>
</iframe> </iframe>
</div> </div>
<div class="poweredBy"> <div class="poweredBy">
<span>Real Time Economic Calendar provided by <span
<a href="https://www.investing.com/" rel="nofollow" target="_blank" class="underline_link">Investing.com</a>. >Real Time Economic Calendar provided by
</span> <a
</div> href="https://www.investing.com/"
rel="nofollow"
target="_blank"
class="underline_link"
>Investing.com</a
>.
</span>
</div>
</div> </div>
</div> </div>
</div> </div>
<div id="updateModal" class="modal">
<div class="modal-content">
<span class="close" onclick="closeUpdateModal()">&times;</span>
<h2 id="updateTitle">Update auf <span id="updateVersion"></span></h2>
<p id="updateChanges"></p>
<button onclick="performUpdate()">Update</button>
</div>
</div>
<script src="magic.js"></script> <script src="magic.js"></script>
<script src="news.js"></script> <script src="news.js"></script>
</body> <script src="update.js"></script>
</body>
</html> </html>

File diff suppressed because it is too large Load Diff

View File

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

74
public/responsive.css Normal file
View File

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

View File

@ -1,12 +1,13 @@
body { body {
font-family: Arial, sans-serif; font-family: Arial, sans-serif;
margin: 0 auto; /* Zentrierung */ margin: 0 auto;
padding: 0; padding: 0;
transition: background-color 0.4s, color 0.4s; transition: background-color 0.4s, color 0.4s;
min-width: 1400px;
max-width: 1978px; max-width: 1778px;
min-width: 808px;
} }
h1 { h1 {
font-size: 28px; font-size: 28px;
} }
@ -16,6 +17,7 @@ body.dark {
background-color: #121212; background-color: #121212;
color: #ffffff; color: #ffffff;
} }
body.light { body.light {
background-color: #f2f2f2; background-color: #f2f2f2;
color: #000000; color: #000000;
@ -26,9 +28,11 @@ body.light {
text-align: center; text-align: center;
padding: 20px; padding: 20px;
} }
.button-container { .button-container {
margin-bottom: 10px; margin-bottom: 10px;
} }
button { button {
padding: 10px; padding: 10px;
margin: 5px; margin: 5px;
@ -38,31 +42,68 @@ button {
border: none; border: none;
cursor: pointer; cursor: pointer;
border-radius: 5px; border-radius: 5px;
} }
button:hover { button:hover {
background-color: #005f73; background-color: #005f73;
} }
.alarm-btn { background-color: #ff8b00; }
.alarm-btn:hover { background-color: #e38806; } .alarm-btn {
.options-btn { background-color: #555; } background-color: #ff8b00;
.options-btn:hover { background-color: #777; } }
.alarm-btn:hover {
background-color: #e38806;
}
.options-btn {
background-color: #555;
}
.options-btn:hover {
background-color: #777;
}
.main-container { .main-container {
display: flex; display: flex;
} }
.left-column { .left-column {
width: 75%; width: 75%;
padding: 20px; padding: 10px;
} }
.right-column { .right-column {
width: 320px;
display: grid;
grid-template-rows: 300px auto;
border-left: 1px solid #444; border-left: 1px solid #444;
width: 320px;
padding: 20px; padding: 20px;
}
.notify-area {
min-height: 400px;
max-width: 280px;
border: 1px solid #444;
overflow-y: auto;
padding: 10px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
.buttons-and-version {
display: flex;
flex-direction: column;
max-width: 260px;
padding-left: 6%;
}
.notify-heading { .notify-heading {
display: flex; display: flex;
@ -71,6 +112,7 @@ button:hover {
font-size: 20px; font-size: 20px;
margin-bottom: 10px; margin-bottom: 10px;
} }
.notify-list { .notify-list {
list-style: none; list-style: none;
padding-left: 0; padding-left: 0;
@ -83,12 +125,14 @@ button:hover {
.grid-container { .grid-container {
display: grid; display: grid;
grid-template-columns: repeat(3, 1fr); grid-template-columns: repeat(5, 1fr);
gap: 10px; gap: 10px;
width: 90%;
width: 80%;
margin: auto; margin: auto;
padding: 10px; padding: 10px;
} }
.crypto-box { .crypto-box {
background-color: #1e1e1e; background-color: #1e1e1e;
border: 2px solid #444; border: 2px solid #444;
@ -98,15 +142,22 @@ button:hover {
font-size: 16px; font-size: 16px;
position: relative; position: relative;
user-select: none; user-select: none;
min-width: 177px;
} }
body.light .crypto-box { body.light .crypto-box {
background-color: #ffffff; background-color: #ffffff;
border: 2px solid #ccc; border: 2px solid #ccc;
color: #000; color: #000;
} }
.change.up { color: #00ff00; } .change.up {
.change.down { color: #ff0000; } color: #00ff00;
}
.change.down {
color: #ff0000;
}
.delete-btn { .delete-btn {
@ -125,6 +176,7 @@ body.light .crypto-box {
line-height: 24px; line-height: 24px;
padding: 0; padding: 0;
} }
.delete-btn:hover { .delete-btn:hover {
background-color: #999; background-color: #999;
color: #000; color: #000;
@ -135,6 +187,7 @@ body.light .crypto-box {
opacity: 0.5; opacity: 0.5;
transform: scale(0.97); transform: scale(0.97);
} }
.crypto-box.drag-over { .crypto-box.drag-over {
border: 2px dashed #ccc !important; border: 2px dashed #ccc !important;
} }
@ -144,49 +197,52 @@ 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;
overflow-y: auto; overflow-y: auto;
overflow-x: hidden; overflow-x: hidden;
position: relative; position: relative;
box-shadow: 0px 4px 10px rgba(0, 0, 0, 0.3); box-shadow: 0px 4px 10px rgba(0, 0, 0, 0.3);
} }
#cryptoNewsModal .modal-content { #cryptoNewsModal .modal-content {
width: 40%; width: 40%;
overflow-x: hidden; overflow-x: hidden;
overflow-y: auto; overflow-y: auto;
max-height: 80vh; max-height: 80vh;
} }
#buyMeModal .modal-content { #buyMeModal .modal-content {
width: 30%; width: 30%;
overflow-x: hidden; overflow-x: hidden;
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%, -50%);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
} }
@ -199,28 +255,28 @@ body.light .crypto-box {
} }
#optionsModal .modal-content { #optionsModal .modal-content {
width: 20%; width: 30%;
overflow-x: hidden; overflow-x: hidden;
} }
#addCryptoModal .modal-content { #addCryptoModal .modal-content {
width: 20%; width: 20%;
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%, -50%);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
} }
#addCryptoModal .modal-content label { #addCryptoModal .modal-content label {
display: block; display: block;
margin-bottom: 6px; margin-bottom: 6px;
} }
#addCryptoModal .close { #addCryptoModal .close {
@ -235,43 +291,44 @@ body.light .crypto-box {
#addCryptoModal .modal-content select, #addCryptoModal .modal-content select,
#addCryptoModal .modal-content input { #addCryptoModal .modal-content input {
width: 100%; width: 100%;
height: 20px; height: 20px;
font-size: 16px; font-size: 16px;
padding: 8px; padding: 8px;
border: 1px solid #ccc; border: 1px solid #ccc;
box-sizing: border-box; box-sizing: border-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;
justify-content: center; justify-content: center;
} }
#alarmModal .modal-content label { #alarmModal .modal-content label {
display: block; display: block;
margin-bottom: 3px; margin-bottom: 3px;
} }
#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;
} }
@ -412,17 +490,19 @@ body.light .alarm-item {
.coffee-btn { .coffee-btn {
margin-top: 60px; margin-top: 60px;
background-color: #614484; background-color: #614484;
font-family: 'Bree Serif', serif; font-family: 'Bree Serif', serif;
font-size: 16px; font-size: 16px;
color: white; color: white;
padding: 10px 20px; padding: 10px 20px;
border: none; border: none;
cursor: pointer; cursor: pointer;
} }
.coffee-btn:hover { .coffee-btn:hover {
background-color: #711fe9; background-color: #711fe9;
} }
.coffee-icon { .coffee-icon {
width: 25px; width: 25px;
height: 25px; height: 25px;
@ -443,21 +523,26 @@ body.light .alarm-item {
color: white; color: white;
text-align: center; text-align: center;
} }
#news-feed { #news-feed {
text-align: left; text-align: left;
} }
.news-item { .news-item {
padding: 15px; padding: 15px;
border-bottom: 1px solid #333; border-bottom: 1px solid #333;
transition: background 0.3s; transition: background 0.3s;
} }
.news-item:last-child { .news-item:last-child {
border-bottom: none; border-bottom: none;
} }
.news-item:hover { .news-item:hover {
background: rgba(255, 255, 255, 0.1); background: rgba(255, 255, 255, 0.1);
cursor: pointer; cursor: pointer;
} }
.news-title { .news-title {
font-size: 18px; font-size: 18px;
color: #1db954; color: #1db954;
@ -465,6 +550,7 @@ body.light .alarm-item {
display: block; display: block;
margin-bottom: 5px; margin-bottom: 5px;
} }
.news-meta { .news-meta {
font-size: 14px; font-size: 14px;
color: gray; color: gray;
@ -472,10 +558,12 @@ body.light .alarm-item {
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
} }
.news-source { .news-source {
font-weight: bold; font-weight: bold;
color: #ffffff; color: #ffffff;
} }
.time { .time {
font-size: 14px; font-size: 14px;
color: gray; color: gray;
@ -484,18 +572,14 @@ body.light .alarm-item {
.button-grid-container { .button-grid-container {
display: grid; display: grid;
grid-template-columns: 1fr 2fr 1fr; grid-template-columns: 1fr 1fr 1fr;
/* alle gleich breit */
align-items: center; align-items: center;
justify-items: center; justify-items: center;
gap: 10px; column-gap: 10px;
/* oder kleiner */
margin-bottom: 15px; margin-bottom: 15px;
} }
.grid-left, .grid-middle, .grid-right {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 10px;
}
#economicCalendarModal .iframe-container { #economicCalendarModal .iframe-container {
@ -510,6 +594,7 @@ body.light .alarm-item {
align-items: center; align-items: center;
justify-content: center; justify-content: center;
} }
#economicCalendarModal .iframe-wrapper { #economicCalendarModal .iframe-wrapper {
position: relative; position: relative;
width: 100%; width: 100%;
@ -520,12 +605,14 @@ body.light .alarm-item {
background: white; background: white;
box-shadow: 0px 4px 10px rgba(0, 0, 0, 0.2); box-shadow: 0px 4px 10px rgba(0, 0, 0, 0.2);
} }
#economicCalendarModal iframe { #economicCalendarModal iframe {
width: calc(100% - 20px); width: calc(100% - 20px);
height: calc(100% - 20px); height: calc(100% - 20px);
border: none; border: none;
display: block; display: block;
} }
#economicCalendarModal .poweredBy { #economicCalendarModal .poweredBy {
margin-top: 10px; margin-top: 10px;
font-family: Arial, Helvetica, sans-serif; font-family: Arial, Helvetica, sans-serif;
@ -533,6 +620,7 @@ body.light .alarm-item {
color: #ffffff; color: #ffffff;
text-align: center; text-align: center;
} }
#economicCalendarModal .poweredBy a { #economicCalendarModal .poweredBy a {
color: #06529D; color: #06529D;
font-weight: bold; font-weight: bold;
@ -540,3 +628,26 @@ body.light .alarm-item {
} }
#updateModal .modal-content {}
.version-info {
text-align: center;
margin-top: 10px;
}
.change {
margin: 5px;
}
.my-button {
background-color: white;
color: black;
border: 1px solid black;
padding: 8px 16px;
cursor: pointer;
font-weight: 700;
}

112
public/update.js Normal file
View File

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

View File

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

14
update.json Normal file
View File

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