für Docker angepasst
This commit is contained in:
53
.dockerignore
Normal file
53
.dockerignore
Normal file
@@ -0,0 +1,53 @@
|
||||
# dependencies
|
||||
node_modules
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# testing
|
||||
coverage
|
||||
|
||||
# next.js
|
||||
.next/
|
||||
out/
|
||||
build
|
||||
dist
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
*.log
|
||||
|
||||
# local env files
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
|
||||
# git
|
||||
.git
|
||||
.gitignore
|
||||
|
||||
# editor
|
||||
.vscode
|
||||
.idea
|
||||
|
||||
# docker
|
||||
Dockerfile
|
||||
.dockerignore
|
||||
docker-compose.yml
|
||||
|
||||
# SQL scripts
|
||||
*.sql
|
||||
|
||||
# other
|
||||
README.md
|
||||
10
.env.example
Normal file
10
.env.example
Normal file
@@ -0,0 +1,10 @@
|
||||
# Docker Compose Environment Variables
|
||||
# Kopieren Sie diese Datei nach .env und passen Sie die Werte an
|
||||
|
||||
# MySQL Datenbankzugangsdaten
|
||||
DB_USER=root
|
||||
DB_PASS=IhrMySQLPasswort
|
||||
DB_NAME=RXF
|
||||
|
||||
# Build-Datum (wird automatisch beim Build gesetzt, Format: DD.MM.YYYY)
|
||||
BUILD_DATE=$(date +%d.%m.%Y)
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -32,6 +32,7 @@ yarn-error.log*
|
||||
|
||||
# env files (can opt-in for committing if needed)
|
||||
.env*
|
||||
!.env.example
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
317
DEPLOYMENT.md
Normal file
317
DEPLOYMENT.md
Normal file
@@ -0,0 +1,317 @@
|
||||
# Deployment Anleitung - Werte Next.js App
|
||||
|
||||
## Voraussetzungen
|
||||
|
||||
Auf dem Server benötigt:
|
||||
- Docker (Version 20.10+)
|
||||
- Docker Compose (Version 2.0+)
|
||||
- MySQL Server (bereits installiert und laufend)
|
||||
- RXF Datenbank mit Tabelle Werte_BZG muss bereits existieren
|
||||
|
||||
## Deployment Schritte
|
||||
|
||||
### 1. Projekt auf den Server übertragen
|
||||
|
||||
```bash
|
||||
# Auf dem Server ein Verzeichnis erstellen
|
||||
mkdir -p /opt/werte-next
|
||||
cd /opt/werte-next
|
||||
|
||||
# Projekt per Git oder rsync übertragen
|
||||
# Option A: Git
|
||||
git clone <repository-url> .
|
||||
|
||||
# Option B: rsync (von lokalem Rechner aus)
|
||||
rsync -av --exclude 'node_modules' --exclude '.next' \
|
||||
/Users/rxf/REXFUE_APPS/werte_next/ user@server:/opt/werte-next/
|
||||
```
|
||||
|
||||
### 2. Umgebungsvariablen konfigurieren
|
||||
|
||||
```bash
|
||||
# .env-Datei aus Beispiel erstellen
|
||||
cp .env.example .env
|
||||
|
||||
# .env-Datei mit Ihren Zugangsdaten bearbeiten
|
||||
nano .env
|
||||
```
|
||||
|
||||
Passen Sie die Werte in der `.env`-Datei an:
|
||||
```bash
|
||||
DB_USER=root
|
||||
DB_PASS=IhrMySQLPasswort
|
||||
DB_NAME=RXF
|
||||
```
|
||||
|
||||
**Wichtig**: Die `.env`-Datei enthält sensible Daten und wird NICHT ins Git-Repository committed!
|
||||
|
||||
### 3. Docker Images bauen und Container starten
|
||||
|
||||
```bash
|
||||
cd /opt/werte-next
|
||||
|
||||
# Images bauen und Container im Hintergrund starten
|
||||
docker-compose up -d --build
|
||||
```
|
||||
|
||||
### 4. Container-Status prüfen
|
||||
|
||||
```bash
|
||||
# Container-Status anzeigen
|
||||
docker-compose ps
|
||||
|
||||
# Logs anschauen
|
||||
docker-compose logs -f werte-app
|
||||
```
|
||||
|
||||
### 5. Datenbank prüfen
|
||||
|
||||
```bash
|
||||
# Direkt auf Host-MySQL verbinden
|
||||
mysql -uroot -p<IhrPasswort> RXF
|
||||
|
||||
# Tabelle prüfen
|
||||
SHOW TABLES;
|
||||
DESCRIBE Werte_BZG;
|
||||
SELECT COUNT(*) FROM Werte_BZG;
|
||||
|
||||
# Oder von außen (falls MySQL externen Zugriff erlaubt)
|
||||
mysql -h <server-ip> -uroot -p<IhrPasswort> RXF
|
||||
```
|
||||
|
||||
### 6. Anwendung im Browser öffnen
|
||||
|
||||
```
|
||||
http://<server-ip>:3000
|
||||
```
|
||||
|
||||
## Konfiguration anpassen
|
||||
|
||||
### Port ändern
|
||||
|
||||
In `docker-compose.yml` den Port für die App ändern:
|
||||
```yaml
|
||||
ports:
|
||||
- "8080:3000" # Zugriff über Port 8080
|
||||
```
|
||||
|
||||
### Datenbankverbindung anpassen
|
||||
|
||||
In `docker-compose.yml` die Umgebungsvariablen ändern:
|
||||
```yaml
|
||||
environment:
|
||||
- DB_HOST=host.docker.internal # Für Host-MySQL
|
||||
- DB_USER=root # MySQL-Benutzer
|
||||
- DB_PASS=IhrPasswort # MySQL-Passwort
|
||||
- DB_NAME=RXF # Datenbankname
|
||||
```
|
||||
|
||||
**Hinweis**: `host.docker.internal` zeigt auf den Docker-Host. Dies wird durch die `extra_hosts` Konfiguration ermöglicht.
|
||||
|
||||
## Verwaltung
|
||||
|
||||
### Container stoppen
|
||||
|
||||
```bash
|
||||
docker-compose stop
|
||||
```
|
||||
|
||||
### Container starten
|
||||
|
||||
```bash
|
||||
docker-compose start
|
||||
```
|
||||
|
||||
### Container neustarten
|
||||
|
||||
```bash
|
||||
docker-compose restart
|
||||
```
|
||||
|
||||
### Container entfernen (Daten bleiben erhalten)
|
||||
|
||||
```bash
|
||||
docker-compose down
|
||||
```
|
||||
|
||||
### Alles löschen inkl. Daten
|
||||
|
||||
```bash
|
||||
docker-compose down -v
|
||||
```
|
||||
|
||||
### Updates einspielen
|
||||
|
||||
```bash
|
||||
# Neue Version holen
|
||||
git pull # oder rsync
|
||||
|
||||
# Neu bauen und starten
|
||||
docker-compose up -d --build
|
||||
```
|
||||
|
||||
### Logs anschauen
|
||||
|
||||
```bash
|
||||
# App-Logs
|
||||
docker-compose logs -f werte-app
|
||||
|
||||
# Letzte 100 Zeilen
|
||||
docker-compose logs --tail=100 werte-app
|
||||
```
|
||||
|
||||
## Backup
|
||||
|
||||
### Datenbank-Backup erstellen
|
||||
|
||||
```bash
|
||||
# Direkt auf dem Host (empfohlen)
|
||||
# Passwort wird interaktiv abgefragt
|
||||
mysqldump -uroot -p RXF > backup_$(date +%Y%m%d).sql
|
||||
|
||||
# Oder mit gzip komprimieren
|
||||
mysqldump -uroot -p RXF | gzip > backup_$(date +%Y%m%d).sql.gz
|
||||
|
||||
# Nur die Werte_BZG Tabelle
|
||||
mysqldump -uroot -p RXF Werte_BZG > werte_backup_$(date +%Y%m%d).sql
|
||||
```
|
||||
|
||||
### Datenbank wiederherstellen
|
||||
|
||||
```bash
|
||||
# Aus Backup wiederherstellen
|
||||
mysql -uroot -p RXF < backup_20260222.sql
|
||||
|
||||
# Aus komprimiertem Backup
|
||||
gunzip < backup_20260222.sql.gz | mysql -uroot -p RXF
|
||||
```
|
||||
|
||||
## Reverse Proxy (Optional)
|
||||
|
||||
Für Produktionsumgebungen empfiehlt sich ein Reverse Proxy wie Nginx:
|
||||
|
||||
### Nginx-Konfiguration Beispiel
|
||||
|
||||
```nginx
|
||||
server {
|
||||
listen 80;
|
||||
server_name werte.example.com;
|
||||
|
||||
location / {
|
||||
proxy_pass http://localhost:3000;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection 'upgrade';
|
||||
proxy_set_header Host $host;
|
||||
proxy_cache_bypass $http_upgrade;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Mit SSL (Let's Encrypt)
|
||||
|
||||
```bash
|
||||
# Certbot installieren
|
||||
apt install certbot python3-certbot-nginx
|
||||
|
||||
# Zertifikat erstellen
|
||||
certbot --nginx -d werte.example.com
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Container startet nicht
|
||||
|
||||
```bash
|
||||
# Detaillierte Logs anschauen
|
||||
docker-compose logs werte-app
|
||||
|
||||
# Container neu bauen
|
||||
docker-compose build --no-cache werte-app
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
### Datenbankverbindung schlägt fehl
|
||||
|
||||
```bash
|
||||
# Prüfen ob MySQL auf dem Host läuft
|
||||
sudo systemctl status mysql
|
||||
# oder
|
||||
sudo service mysql status
|
||||
|
||||
# MySQL Error Log prüfen
|
||||
sudo tail -f /var/log/mysql/error.log
|
||||
|
||||
# Verbindung vom Docker-Container testen
|
||||
docker exec -it werte-next-app sh
|
||||
# Im Container:
|
||||
ping host.docker.internal
|
||||
```
|
||||
|
||||
**Hinweis**: Wenn `host.docker.internal` nicht funktioniert, können Sie stattdessen die Server-IP verwenden:
|
||||
```yaml
|
||||
environment:
|
||||
- DB_HOST=192.168.1.100 # Ersetzen mit tatsächlicher Server-IP
|
||||
```
|
||||
|
||||
### App ist langsam
|
||||
|
||||
```bash
|
||||
# Ressourcen-Nutzung prüfen
|
||||
docker stats
|
||||
|
||||
# Container neu starten
|
||||
docker-compose restart werte-app
|
||||
```
|
||||
|
||||
## Systemd Service (Optional)
|
||||
|
||||
Für automatischen Start beim Server-Reboot:
|
||||
|
||||
```bash
|
||||
# Service-Datei erstellen
|
||||
sudo nano /etc/systemd/system/werte-next.service
|
||||
```
|
||||
|
||||
Inhalt:
|
||||
```ini
|
||||
[Unit]
|
||||
Description=Werte Next.js Application
|
||||
After=docker.service
|
||||
Requires=docker.service
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
RemainAfterExit=yes
|
||||
WorkingDirectory=/opt/werte-next
|
||||
ExecStart=/usr/bin/docker-compose up -d
|
||||
ExecStop=/usr/bin/docker-compose down
|
||||
TimeoutStartSec=0
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
|
||||
Service aktivieren:
|
||||
```bash
|
||||
sudo systemctl enable werte-next
|
||||
sudo systemctl start werte-next
|
||||
sudo systemctl status werte-next
|
||||
```
|
||||
|
||||
## Monitoring
|
||||
|
||||
CPU und Memory-Nutzung überwachen:
|
||||
```bash
|
||||
docker stats werte-next-app
|
||||
```
|
||||
|
||||
## Support
|
||||
|
||||
Bei Problemen:
|
||||
- Logs prüfen: `docker-compose logs -f`
|
||||
- Container neu starten: `docker-compose restart`
|
||||
- Issues auf GitHub melden
|
||||
51
Dockerfile
Normal file
51
Dockerfile
Normal file
@@ -0,0 +1,51 @@
|
||||
# Multi-stage build for Next.js application
|
||||
FROM node:22-alpine AS base
|
||||
|
||||
# Install dependencies only when needed
|
||||
FROM base AS deps
|
||||
RUN apk add --no-cache libc6-compat
|
||||
WORKDIR /app
|
||||
|
||||
# Copy package files
|
||||
COPY package.json package-lock.json* ./
|
||||
RUN npm ci
|
||||
|
||||
# Rebuild the source code only when needed
|
||||
FROM base AS builder
|
||||
WORKDIR /app
|
||||
COPY --from=deps /app/node_modules ./node_modules
|
||||
COPY . .
|
||||
|
||||
# Set build date as build argument
|
||||
ARG BUILD_DATE
|
||||
ENV NEXT_PUBLIC_BUILD_DATE=${BUILD_DATE}
|
||||
|
||||
# Disable telemetry during build
|
||||
ENV NEXT_TELEMETRY_DISABLED=1
|
||||
|
||||
# Build the application
|
||||
RUN npm run build
|
||||
|
||||
# Production image, copy all the files and run next
|
||||
FROM base AS runner
|
||||
WORKDIR /app
|
||||
|
||||
ENV NODE_ENV=production
|
||||
ENV NEXT_TELEMETRY_DISABLED=1
|
||||
|
||||
RUN addgroup --system --gid 1001 nodejs
|
||||
RUN adduser --system --uid 1001 nextjs
|
||||
|
||||
# Copy necessary files
|
||||
COPY --from=builder /app/public ./public
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
|
||||
|
||||
USER nextjs
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
ENV PORT=3000
|
||||
ENV HOSTNAME="0.0.0.0"
|
||||
|
||||
CMD ["node", "server.js"]
|
||||
189
README.md
189
README.md
@@ -1,36 +1,185 @@
|
||||
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
|
||||
# Werte Next.js - Gesundheitsdaten Tracking
|
||||
|
||||
## Getting Started
|
||||
Eine moderne Next.js-Anwendung zur Erfassung und Verwaltung von Gesundheitsdaten, basierend auf dem ursprünglichen Werte-Projekt.
|
||||
|
||||
First, run the development server:
|
||||
## Features
|
||||
|
||||
- ✅ **Eingabeformular** für Gesundheitsdaten:
|
||||
- Datum und Zeit
|
||||
- Blutzucker (mg/dl)
|
||||
- Mahlzeiten-Informationen
|
||||
- Gewicht (kg)
|
||||
- Blutdruck (systolisch/diastolisch in mmHg)
|
||||
- Puls
|
||||
|
||||
- ✅ **Anzeige der letzten 10 Einträge** mit Löschfunktion
|
||||
- ✅ **Responsive Design** mit Tailwind CSS
|
||||
- ✅ **TypeScript** für Type-Safety
|
||||
- ✅ **MySQL-Datenbankanbindung** (Docker-Container)
|
||||
|
||||
## Technologie-Stack
|
||||
|
||||
- **Framework**: Next.js 16.x (App Router)
|
||||
- **Sprache**: TypeScript
|
||||
- **Styling**: Tailwind CSS
|
||||
- **Datenbank**: MySQL 5.6 (Docker)
|
||||
- **ORM**: mysql2
|
||||
|
||||
## Voraussetzungen
|
||||
|
||||
- Node.js 18.x oder höher
|
||||
- Docker (für MySQL-Container)
|
||||
- Die MySQL-Datenbank muss bereits laufen (siehe docker-compose.yml im übergeordneten Verzeichnis)
|
||||
|
||||
## Installation
|
||||
|
||||
1. **Dependencies installieren**:
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
2. **Umgebungsvariablen konfigurieren**:
|
||||
Die Datei `.env.local` ist bereits mit den korrekten Datenbankzugangsdaten angelegt:
|
||||
```
|
||||
DB_HOST=mydbase_mysql
|
||||
DB_USER=root
|
||||
DB_PASS=SFluorit
|
||||
DB_NAME=RXF
|
||||
```
|
||||
|
||||
3. **Datenbank prüfen**:
|
||||
Stelle sicher, dass die MySQL-Datenbank läuft und die Tabelle `RXF.Werte_BZG` existiert.
|
||||
|
||||
## Entwicklung
|
||||
|
||||
Development-Server starten:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
# or
|
||||
yarn dev
|
||||
# or
|
||||
pnpm dev
|
||||
# or
|
||||
bun dev
|
||||
```
|
||||
|
||||
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
||||
Die Anwendung ist dann unter [http://localhost:3000](http://localhost:3000) erreichbar.
|
||||
|
||||
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
|
||||
## Produktion
|
||||
|
||||
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
|
||||
### Lokale Produktion
|
||||
|
||||
## Learn More
|
||||
Build erstellen:
|
||||
|
||||
To learn more about Next.js, take a look at the following resources:
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
|
||||
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
|
||||
Produktions-Server starten:
|
||||
|
||||
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
|
||||
```bash
|
||||
npm start
|
||||
```
|
||||
|
||||
## Deploy on Vercel
|
||||
### Docker Deployment
|
||||
|
||||
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
|
||||
Für das Deployment auf einem externen Server ist die Anwendung vollständig containerisiert. Alle Details zur Installation und Verwaltung finden Sie in der **[DEPLOYMENT.md](DEPLOYMENT.md)**.
|
||||
|
||||
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
|
||||
Kurzanleitung:
|
||||
|
||||
```bash
|
||||
# Docker-Container bauen und starten
|
||||
docker-compose up -d --build
|
||||
|
||||
# Status prüfen
|
||||
docker-compose ps
|
||||
|
||||
# Logs anschauen
|
||||
docker-compose logs -f
|
||||
```
|
||||
|
||||
Die Anwendung ist dann unter `http://<server-ip>:3000` erreichbar.
|
||||
|
||||
## Projekt-Struktur
|
||||
|
||||
```
|
||||
werte_next/
|
||||
├── app/
|
||||
│ ├── api/
|
||||
│ │ └── werte/
|
||||
│ │ ├── route.ts # GET/POST API für Einträge
|
||||
│ │ └── [id]/
|
||||
│ │ └── route.ts # DELETE API für einzelne Einträge
|
||||
│ ├── layout.tsx # Root Layout
|
||||
│ ├── page.tsx # Hauptseite
|
||||
│ └── globals.css # Globale Styles
|
||||
├── components/
|
||||
│ ├── WerteForm.tsx # Eingabeformular
|
||||
│ └── WerteList.tsx # Liste der letzten Einträge
|
||||
├── lib/
|
||||
│ └── db.ts # Datenbankverbindung
|
||||
├── types/
|
||||
│ └── werte.ts # TypeScript-Typen
|
||||
├── .env.local # Umgebungsvariablen (lokal, gitignored)
|
||||
└── package.json
|
||||
```
|
||||
|
||||
## API-Endpunkte
|
||||
|
||||
### GET /api/werte
|
||||
Holt die letzten N Einträge aus der Datenbank.
|
||||
|
||||
**Query Parameters**:
|
||||
- `limit` (optional): Anzahl der Einträge (Standard: 10)
|
||||
|
||||
**Response**:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": [...]
|
||||
}
|
||||
```
|
||||
|
||||
### POST /api/werte
|
||||
Erstellt einen neuen Eintrag.
|
||||
|
||||
**Request Body**:
|
||||
```json
|
||||
{
|
||||
"Datum": "2026-02-21",
|
||||
"Zeit": "14:30",
|
||||
"Zucker": 120,
|
||||
"Essen": "nüchtern",
|
||||
"Gewicht": 75.5,
|
||||
"DruckS": 130,
|
||||
"DruckD": 85,
|
||||
"Puls": 72
|
||||
}
|
||||
```
|
||||
|
||||
### DELETE /api/werte/[id]
|
||||
Löscht einen Eintrag anhand der ID.
|
||||
|
||||
## Design
|
||||
|
||||
Das Design orientiert sich am ursprünglichen Werte-Projekt:
|
||||
- **Hintergrundfarbe**: #FFFFDD (hellgelb)
|
||||
- **Eingabebereich**: #CCCCFF (hellblau)
|
||||
- **Buttons**: #85B7D7 (blau)
|
||||
- **Schriftarten**: Arial, Verdana, Helvetica
|
||||
|
||||
## Unterschiede zum Original
|
||||
|
||||
- **Modernes Framework**: Next.js statt PHP
|
||||
- **TypeScript**: Type-Safety für bessere Wartbarkeit
|
||||
- **React**: Komponentenbasierte Architektur
|
||||
- **API Routes**: RESTful API statt direkter PHP-Skripte
|
||||
- **Vereinfachte Features**: Fokus auf Eingabe und letzte 10 Einträge (Listen- und Statistik-Tabs wurden weggelassen wie gewünscht)
|
||||
|
||||
## Bekannte Einschränkungen
|
||||
|
||||
- Die Tabs "Liste" und "Statistik" aus dem Original wurden bewusst nicht implementiert
|
||||
- Die jqGrid-Funktionalität wurde durch eine einfache Tabelle ersetzt
|
||||
|
||||
## Support
|
||||
|
||||
Bei Fragen oder Problemen: [rxf@gmx.de](mailto:rxf@gmx.de)
|
||||
|
||||
## Lizenz
|
||||
|
||||
Private Nutzung
|
||||
|
||||
56
app/api/werte/[id]/route.ts
Normal file
56
app/api/werte/[id]/route.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { query } from '@/lib/db';
|
||||
import { CreateWerteEntry } from '@/types/werte';
|
||||
|
||||
const TABLE = 'RXF.Werte_BZG';
|
||||
|
||||
export async function PUT(
|
||||
request: NextRequest,
|
||||
context: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
try {
|
||||
const { id } = await context.params;
|
||||
const body: CreateWerteEntry = await request.json();
|
||||
|
||||
const sql = `UPDATE ${TABLE} SET
|
||||
Datum = '${body.Datum}',
|
||||
Zeit = '${body.Zeit}',
|
||||
Zucker = ${body.Zucker || 'NULL'},
|
||||
Essen = ${body.Essen ? `'${body.Essen.replace(/'/g, "''")}'` : 'NULL'},
|
||||
Gewicht = ${body.Gewicht || 'NULL'},
|
||||
DruckS = ${body.DruckS || 'NULL'},
|
||||
DruckD = ${body.DruckD || 'NULL'},
|
||||
Puls = ${body.Puls || 'NULL'}
|
||||
WHERE ID = ${parseInt(id, 10)}`;
|
||||
|
||||
const result = await query(sql);
|
||||
|
||||
return NextResponse.json({ success: true, data: result });
|
||||
} catch (error) {
|
||||
console.error('Database error:', error);
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'Failed to update entry' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function DELETE(
|
||||
request: NextRequest,
|
||||
context: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
try {
|
||||
const { id } = await context.params;
|
||||
|
||||
const sql = `DELETE FROM ${TABLE} WHERE ID = ${parseInt(id, 10)}`;
|
||||
const result = await query(sql);
|
||||
|
||||
return NextResponse.json({ success: true, data: result });
|
||||
} catch (error) {
|
||||
console.error('Database error:', error);
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'Failed to delete entry' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
63
app/api/werte/route.ts
Normal file
63
app/api/werte/route.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { query } from '@/lib/db';
|
||||
import { CreateWerteEntry } from '@/types/werte';
|
||||
|
||||
const TABLE = 'RXF.Werte_BZG';
|
||||
|
||||
// GET - Fetch entries
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const searchParams = request.nextUrl.searchParams;
|
||||
const limit = parseInt(searchParams.get('limit') || '10', 10);
|
||||
|
||||
const sql = `SELECT ID, DATE_FORMAT(Datum, '%Y-%m-%d') as Datum, Zeit, Zucker, Essen, Gewicht, DruckD, DruckS, Puls FROM ${TABLE} ORDER BY Datum DESC, Zeit DESC LIMIT ${limit}`;
|
||||
const rows = await query(sql);
|
||||
|
||||
return NextResponse.json(
|
||||
{ success: true, data: rows },
|
||||
{
|
||||
headers: {
|
||||
'Cache-Control': 'no-store, no-cache, must-revalidate',
|
||||
'Pragma': 'no-cache',
|
||||
'Expires': '0',
|
||||
},
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Database error:', error);
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'Failed to fetch entries' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// POST - Create new entry
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body: CreateWerteEntry = await request.json();
|
||||
|
||||
const sql = `INSERT INTO ${TABLE} (Datum, Zeit, Zucker, Essen, Gewicht, DruckS, DruckD, Puls) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`;
|
||||
|
||||
const params = [
|
||||
body.Datum,
|
||||
body.Zeit,
|
||||
body.Zucker || null,
|
||||
body.Essen || null,
|
||||
body.Gewicht || null,
|
||||
body.DruckS || null,
|
||||
body.DruckD || null,
|
||||
body.Puls || null,
|
||||
];
|
||||
|
||||
const result = await query(sql, params);
|
||||
|
||||
return NextResponse.json({ success: true, data: result });
|
||||
} catch (error) {
|
||||
console.error('Database error:', error);
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'Failed to create entry' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
/* stylelint-disable at-rule-no-unknown */
|
||||
@import "tailwindcss";
|
||||
|
||||
:root {
|
||||
|
||||
@@ -13,8 +13,8 @@ const geistMono = Geist_Mono({
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Create Next App",
|
||||
description: "Generated by create next app",
|
||||
title: "Werte - Log",
|
||||
description: "Gesundheitsdaten Tracking Anwendung",
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
@@ -23,7 +23,7 @@ export default function RootLayout({
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<html lang="de">
|
||||
<body
|
||||
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
||||
>
|
||||
|
||||
158
app/page.tsx
158
app/page.tsx
@@ -1,64 +1,110 @@
|
||||
import Image from "next/image";
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import WerteForm from '@/components/WerteForm';
|
||||
import WerteList from '@/components/WerteList';
|
||||
import { WerteEntry } from '@/types/werte';
|
||||
import packageJson from '@/package.json';
|
||||
|
||||
export default function Home() {
|
||||
const [entries, setEntries] = useState<WerteEntry[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [selectedEntry, setSelectedEntry] = useState<WerteEntry | null>(null);
|
||||
|
||||
const version = packageJson.version;
|
||||
const buildDate = process.env.NEXT_PUBLIC_BUILD_DATE || new Date().toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' });
|
||||
|
||||
useEffect(() => {
|
||||
let isMounted = true;
|
||||
|
||||
// Fetch initial data
|
||||
(async () => {
|
||||
try {
|
||||
const response = await fetch('/api/werte?limit=14', {
|
||||
cache: 'no-store',
|
||||
headers: {
|
||||
'Cache-Control': 'no-cache',
|
||||
},
|
||||
});
|
||||
const data = await response.json();
|
||||
if (data.success && isMounted) {
|
||||
setEntries(data.data);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching entries:', error);
|
||||
}
|
||||
if (isMounted) {
|
||||
setIsLoading(false);
|
||||
}
|
||||
})();
|
||||
|
||||
return () => {
|
||||
isMounted = false;
|
||||
};
|
||||
}, []);
|
||||
|
||||
const refreshEntries = async () => {
|
||||
try {
|
||||
const response = await fetch('/api/werte?limit=14', {
|
||||
cache: 'no-store',
|
||||
headers: {
|
||||
'Cache-Control': 'no-cache',
|
||||
},
|
||||
});
|
||||
const data = await response.json();
|
||||
if (data.success) {
|
||||
setEntries(data.data);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching entries:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSuccess = () => {
|
||||
setSelectedEntry(null);
|
||||
// Small delay to ensure database commit is complete
|
||||
setTimeout(() => {
|
||||
refreshEntries();
|
||||
}, 100);
|
||||
};
|
||||
|
||||
const handleDelete = (id: number) => {
|
||||
setEntries(entries.filter(entry => entry.ID !== id));
|
||||
};
|
||||
|
||||
const handleEdit = (entry: WerteEntry) => {
|
||||
setSelectedEntry(entry);
|
||||
// Scroll to top
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex min-h-screen items-center justify-center bg-zinc-50 font-sans dark:bg-black">
|
||||
<main className="flex min-h-screen w-full max-w-3xl flex-col items-center justify-between py-32 px-16 bg-white dark:bg-black sm:items-start">
|
||||
<Image
|
||||
className="dark:invert"
|
||||
src="/next.svg"
|
||||
alt="Next.js logo"
|
||||
width={100}
|
||||
height={20}
|
||||
priority
|
||||
/>
|
||||
<div className="flex flex-col items-center gap-6 text-center sm:items-start sm:text-left">
|
||||
<h1 className="max-w-xs text-3xl font-semibold leading-10 tracking-tight text-black dark:text-zinc-50">
|
||||
To get started, edit the page.tsx file.
|
||||
</h1>
|
||||
<p className="max-w-md text-lg leading-8 text-zinc-600 dark:text-zinc-400">
|
||||
Looking for a starting point or more instructions? Head over to{" "}
|
||||
<a
|
||||
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
className="font-medium text-zinc-950 dark:text-zinc-50"
|
||||
>
|
||||
Templates
|
||||
</a>{" "}
|
||||
or the{" "}
|
||||
<a
|
||||
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
className="font-medium text-zinc-950 dark:text-zinc-50"
|
||||
>
|
||||
Learning
|
||||
</a>{" "}
|
||||
center.
|
||||
</p>
|
||||
<div className="min-h-screen bg-white py-4 px-4">
|
||||
<main className="max-w-6xl mx-auto border-2 border-black rounded-lg p-6 bg-[#FFFFDD]">
|
||||
<h1 className="text-3xl font-bold text-center mb-6">Werte - Log</h1>
|
||||
|
||||
<div className="mb-8">
|
||||
<h2 className="text-xl font-semibold mb-4">Eingabe</h2>
|
||||
<WerteForm onSuccess={handleSuccess} selectedEntry={selectedEntry} />
|
||||
</div>
|
||||
<div className="flex flex-col gap-4 text-base font-medium sm:flex-row">
|
||||
<a
|
||||
className="flex h-12 w-full items-center justify-center gap-2 rounded-full bg-foreground px-5 text-background transition-colors hover:bg-[#383838] dark:hover:bg-[#ccc] md:w-[158px]"
|
||||
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Image
|
||||
className="dark:invert"
|
||||
src="/vercel.svg"
|
||||
alt="Vercel logomark"
|
||||
width={16}
|
||||
height={16}
|
||||
/>
|
||||
Deploy Now
|
||||
</a>
|
||||
<a
|
||||
className="flex h-12 w-full items-center justify-center rounded-full border border-solid border-black/[.08] px-5 transition-colors hover:border-transparent hover:bg-black/[.04] dark:border-white/[.145] dark:hover:bg-[#1a1a1a] md:w-[158px]"
|
||||
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Documentation
|
||||
</a>
|
||||
|
||||
<div className="bg-white border border-black rounded-lg shadow-md p-6">
|
||||
<h2 className="text-xl font-semibold mb-4">Letzte 14 Einträge</h2>
|
||||
{isLoading ? (
|
||||
<div className="text-center py-4">Lade Daten...</div>
|
||||
) : (
|
||||
<WerteList entries={entries} onDelete={handleDelete} onEdit={handleEdit} />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<footer className="mt-8 flex justify-between items-center text-sm text-gray-600 px-4">
|
||||
<a href="mailto:rxf@gmx.de" className="hover:underline">
|
||||
rxf@gmx.de
|
||||
</a>
|
||||
<div>
|
||||
Version {version} - {buildDate}
|
||||
</div>
|
||||
</footer>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
|
||||
274
components/WerteForm.tsx
Normal file
274
components/WerteForm.tsx
Normal file
@@ -0,0 +1,274 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { CreateWerteEntry, WerteEntry } from '@/types/werte';
|
||||
|
||||
interface WerteFormProps {
|
||||
onSuccess: () => void;
|
||||
selectedEntry?: WerteEntry | null;
|
||||
}
|
||||
|
||||
export default function WerteForm({ onSuccess, selectedEntry }: WerteFormProps) {
|
||||
const [formData, setFormData] = useState<CreateWerteEntry>({
|
||||
Datum: '',
|
||||
Zeit: '',
|
||||
Zucker: '',
|
||||
Essen: 'nüchtern',
|
||||
Gewicht: '',
|
||||
DruckS: '',
|
||||
DruckD: '',
|
||||
Puls: '',
|
||||
});
|
||||
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [weekday, setWeekday] = useState('');
|
||||
const [editId, setEditId] = useState<number | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedEntry) {
|
||||
// Load selected entry for editing
|
||||
const dateStr = selectedEntry.Datum.toString().split('T')[0];
|
||||
const timeStr = selectedEntry.Zeit.toString().substring(0, 5);
|
||||
|
||||
setFormData({
|
||||
Datum: dateStr,
|
||||
Zeit: timeStr,
|
||||
Zucker: selectedEntry.Zucker || '',
|
||||
Essen: selectedEntry.Essen || '',
|
||||
Gewicht: selectedEntry.Gewicht || '',
|
||||
DruckS: selectedEntry.DruckS || '',
|
||||
DruckD: selectedEntry.DruckD || '',
|
||||
Puls: selectedEntry.Puls || '',
|
||||
});
|
||||
|
||||
setEditId(selectedEntry.ID || null);
|
||||
|
||||
// Parse date to avoid timezone issues
|
||||
const [year, month, day] = dateStr.split('T')[0].split('-');
|
||||
const date = new Date(Number(year), Number(month) - 1, Number(day));
|
||||
updateWeekday(date);
|
||||
} else {
|
||||
// Initialize with current date and time for new entry
|
||||
const now = new Date();
|
||||
const dateStr = now.toISOString().split('T')[0];
|
||||
const timeStr = `${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}`;
|
||||
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
Datum: dateStr,
|
||||
Zeit: timeStr,
|
||||
}));
|
||||
|
||||
setEditId(null);
|
||||
updateWeekday(now);
|
||||
}
|
||||
}, [selectedEntry]);
|
||||
|
||||
const updateWeekday = (date: Date) => {
|
||||
const weekdays = ['Sonntag', 'Montag', 'Dienstag', 'Mittwoch', 'Donnerstag', 'Freitag', 'Samstag'];
|
||||
setWeekday(weekdays[date.getDay()]);
|
||||
};
|
||||
|
||||
const handleDateChange = (dateStr: string) => {
|
||||
setFormData(prev => ({ ...prev, Datum: dateStr }));
|
||||
// Parse date to avoid timezone issues
|
||||
const [year, month, day] = dateStr.split('-');
|
||||
const date = new Date(Number(year), Number(month) - 1, Number(day));
|
||||
updateWeekday(date);
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setIsSubmitting(true);
|
||||
|
||||
try {
|
||||
const url = editId ? `/api/werte/${editId}` : '/api/werte';
|
||||
const method = editId ? 'PUT' : 'POST';
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: method,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(formData),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
// Reset form but keep date and time
|
||||
const now = new Date();
|
||||
const timeStr = `${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}`;
|
||||
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
Zeit: timeStr,
|
||||
Zucker: '',
|
||||
Essen: 'nüchtern',
|
||||
Gewicht: '',
|
||||
DruckS: '',
|
||||
DruckD: '',
|
||||
Puls: '',
|
||||
}));
|
||||
|
||||
setEditId(null);
|
||||
onSuccess();
|
||||
} else {
|
||||
alert('Fehler beim Speichern!');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
alert('Fehler beim Speichern!');
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
const now = new Date();
|
||||
const dateStr = now.toISOString().split('T')[0];
|
||||
const timeStr = `${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}`;
|
||||
|
||||
setFormData({
|
||||
Datum: dateStr,
|
||||
Zeit: timeStr,
|
||||
Zucker: '',
|
||||
Essen: 'nüchtern',
|
||||
Gewicht: '',
|
||||
DruckS: '',
|
||||
DruckD: '',
|
||||
Puls: '',
|
||||
});
|
||||
|
||||
setEditId(null);
|
||||
updateWeekday(now);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-[#CCCCFF] border border-black p-6 rounded-lg mb-6">
|
||||
{editId && (
|
||||
<div className="mb-4 p-3 bg-blue-100 border border-blue-400 rounded text-sm text-blue-800">
|
||||
ℹ️ <strong>Bearbeitungsmodus:</strong> Sie bearbeiten einen bestehenden Eintrag. Klicken Sie auf "Aktualisieren", um die Änderungen zu speichern, oder "Abbrechen", um zur Neuerfassung zurückzukehren.
|
||||
</div>
|
||||
)}
|
||||
<form onSubmit={handleSubmit}>
|
||||
<table className="w-full text-center">
|
||||
<thead>
|
||||
<tr className="border-b-2 border-gray-400">
|
||||
<th className="p-2">Datum</th>
|
||||
<th className="p-2">Zeit</th>
|
||||
<th className="p-2">Zucker</th>
|
||||
<th className="p-2">Essen</th>
|
||||
<th className="p-2">Gewicht</th>
|
||||
<th className="p-2">Druck sys</th>
|
||||
<th className="p-2">Druck dia</th>
|
||||
<th className="p-2">Puls</th>
|
||||
</tr>
|
||||
<tr className="border-b-2 border-gray-400">
|
||||
<th className="p-2 font-normal text-sm">{weekday}</th>
|
||||
<th className="p-2"></th>
|
||||
<th className="p-2 font-normal text-sm">mg/dl</th>
|
||||
<th className="p-2"></th>
|
||||
<th className="p-2 font-normal text-sm">kg</th>
|
||||
<th className="p-2 font-normal text-sm">mmHg</th>
|
||||
<th className="p-2 font-normal text-sm">mmHg</th>
|
||||
<th className="p-2 font-normal text-sm">bpm</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td className="p-2">
|
||||
<input
|
||||
type="date"
|
||||
className="w-full px-2 py-1 text-sm rounded border-2 border-gray-400 bg-white focus:border-blue-500 focus:outline-none"
|
||||
value={formData.Datum}
|
||||
onChange={(e) => handleDateChange(e.target.value)}
|
||||
required
|
||||
/>
|
||||
</td>
|
||||
<td className="p-2">
|
||||
<input
|
||||
type="time"
|
||||
className="w-full px-2 py-1 text-sm rounded border-2 border-gray-400 bg-white focus:border-blue-500 focus:outline-none"
|
||||
value={formData.Zeit}
|
||||
onChange={(e) => setFormData(prev => ({ ...prev, Zeit: e.target.value }))}
|
||||
required
|
||||
/>
|
||||
</td>
|
||||
<td className="p-2">
|
||||
<input
|
||||
type="number"
|
||||
className="w-20 px-2 py-1 text-sm rounded border-2 border-gray-400 bg-white focus:border-blue-500 focus:outline-none"
|
||||
value={formData.Zucker}
|
||||
onChange={(e) => setFormData(prev => ({ ...prev, Zucker: e.target.value }))}
|
||||
maxLength={4}
|
||||
/>
|
||||
</td>
|
||||
<td className="p-2">
|
||||
<textarea
|
||||
className="w-full px-2 py-1 text-sm rounded border-2 border-gray-400 bg-white focus:border-blue-500 focus:outline-none resize-none align-middle"
|
||||
style={{ height: '28px' }}
|
||||
value={formData.Essen}
|
||||
onChange={(e) => setFormData(prev => ({ ...prev, Essen: e.target.value }))}
|
||||
rows={1}
|
||||
/>
|
||||
</td>
|
||||
<td className="p-2">
|
||||
<input
|
||||
type="number"
|
||||
step="0.1"
|
||||
className="w-20 px-2 py-1 text-sm rounded border-2 border-gray-400 bg-white focus:border-blue-500 focus:outline-none"
|
||||
value={formData.Gewicht}
|
||||
onChange={(e) => setFormData(prev => ({ ...prev, Gewicht: e.target.value }))}
|
||||
maxLength={4}
|
||||
/>
|
||||
</td>
|
||||
<td className="p-2">
|
||||
<input
|
||||
type="number"
|
||||
className="w-20 px-2 py-1 text-sm rounded border-2 border-gray-400 bg-white focus:border-blue-500 focus:outline-none"
|
||||
value={formData.DruckS}
|
||||
onChange={(e) => setFormData(prev => ({ ...prev, DruckS: e.target.value }))}
|
||||
maxLength={4}
|
||||
/>
|
||||
</td>
|
||||
<td className="p-2">
|
||||
<input
|
||||
type="number"
|
||||
className="w-20 px-2 py-1 text-sm rounded border-2 border-gray-400 bg-white focus:border-blue-500 focus:outline-none"
|
||||
value={formData.DruckD}
|
||||
onChange={(e) => setFormData(prev => ({ ...prev, DruckD: e.target.value }))}
|
||||
maxLength={4}
|
||||
/>
|
||||
</td>
|
||||
<td className="p-2">
|
||||
<input
|
||||
type="number"
|
||||
className="w-20 px-2 py-1 text-sm rounded border-2 border-gray-400 bg-white focus:border-blue-500 focus:outline-none"
|
||||
value={formData.Puls}
|
||||
onChange={(e) => setFormData(prev => ({ ...prev, Puls: e.target.value }))}
|
||||
maxLength={4}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div className="flex justify-center gap-4 mt-6">
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isSubmitting}
|
||||
className="bg-[#85B7D7] hover:bg-[#6a9fc5] text-black font-medium py-2 px-8 rounded-lg transition-colors disabled:opacity-50"
|
||||
>
|
||||
{isSubmitting ? (editId ? 'Aktualisieren...' : 'Speichern...') : (editId ? 'Aktualisieren' : 'Speichern')}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleReset}
|
||||
className="bg-[#85B7D7] hover:bg-[#6a9fc5] text-black font-medium py-2 px-8 rounded-lg transition-colors"
|
||||
>
|
||||
{editId ? 'Abbrechen' : 'Löschen'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
135
components/WerteList.tsx
Normal file
135
components/WerteList.tsx
Normal file
@@ -0,0 +1,135 @@
|
||||
'use client';
|
||||
|
||||
import { WerteEntry } from '@/types/werte';
|
||||
|
||||
interface WerteListProps {
|
||||
entries: WerteEntry[];
|
||||
onDelete?: (id: number) => void;
|
||||
onEdit?: (entry: WerteEntry) => void;
|
||||
}
|
||||
|
||||
export default function WerteList({ entries, onDelete, onEdit }: WerteListProps) {
|
||||
const formatValue = (value: number | string | null | undefined) => {
|
||||
if (value === null || value === undefined || value === '' || value === '0' || value === '0.0') {
|
||||
return '-';
|
||||
}
|
||||
return value;
|
||||
};
|
||||
|
||||
const formatDate = (dateStr: string) => {
|
||||
// Convert YYYY-MM-DD to DD.MM.YYYY
|
||||
if (!dateStr) return '-';
|
||||
// Parse the date string directly (MySQL returns YYYY-MM-DD)
|
||||
const parts = dateStr.toString().split('T')[0].split('-');
|
||||
if (parts.length === 3) {
|
||||
return `${parts[2]}.${parts[1]}.${parts[0]}`;
|
||||
}
|
||||
return dateStr;
|
||||
};
|
||||
|
||||
const formatTime = (timeStr: string) => {
|
||||
// Convert HH:MM:SS to HH:MM
|
||||
if (!timeStr) return '-';
|
||||
const timeString = timeStr.toString();
|
||||
return timeString.substring(0, 5);
|
||||
};
|
||||
|
||||
const getWeekday = (dateStr: string) => {
|
||||
if (!dateStr) return '-';
|
||||
const weekdays = ['So', 'Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa'];
|
||||
// Parse date parts to avoid timezone issues
|
||||
const [year, month, day] = dateStr.toString().split('T')[0].split('-');
|
||||
const date = new Date(Number(year), Number(month) - 1, Number(day));
|
||||
return weekdays[date.getDay()];
|
||||
};
|
||||
|
||||
const handleDelete = async (id: number) => {
|
||||
if (!confirm('Eintrag wirklich löschen?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/werte/${id}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
|
||||
if (response.ok && onDelete) {
|
||||
onDelete(id);
|
||||
} else {
|
||||
alert('Fehler beim Löschen!');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
alert('Fehler beim Löschen!');
|
||||
}
|
||||
};
|
||||
|
||||
if (entries.length === 0) {
|
||||
return (
|
||||
<div className="text-center py-8 text-gray-500">
|
||||
Keine Einträge vorhanden
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full border-collapse">
|
||||
<thead>
|
||||
<tr className="bg-[#CCCCFF] border-b-2 border-gray-400">
|
||||
<th className="p-2 text-center">Datum</th>
|
||||
<th className="p-2 text-center">Tag</th>
|
||||
<th className="p-2 text-center">Zeit</th>
|
||||
<th className="p-2 text-center">Zucker<br/><span className="text-xs font-normal">mg/dl</span></th>
|
||||
<th className="p-2 text-center">Essen</th>
|
||||
<th className="p-2 text-center">Gewicht<br/><span className="text-xs font-normal">kg</span></th>
|
||||
<th className="p-2 text-center">Druck sys<br/><span className="text-xs font-normal">mmHg</span></th>
|
||||
<th className="p-2 text-center">Druck dia<br/><span className="text-xs font-normal">mmHg</span></th>
|
||||
<th className="p-2 text-center">Puls<br/><span className="text-xs font-normal">bpm</span></th>
|
||||
{onDelete && <th className="p-2 text-center">Aktion</th>}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{entries.map((entry, index) => (
|
||||
<tr
|
||||
key={entry.ID}
|
||||
className={`border-b border-gray-300 hover:bg-gray-50 ${index % 2 === 0 ? 'bg-white' : 'bg-gray-50'}`}
|
||||
>
|
||||
<td className="p-2 text-center">{formatDate(entry.Datum)}</td>
|
||||
<td className="p-2 text-center">{getWeekday(entry.Datum)}</td>
|
||||
<td className="p-2 text-center">{formatTime(entry.Zeit)}</td>
|
||||
<td className="p-2 text-center">{formatValue(entry.Zucker)}</td>
|
||||
<td className="p-2 text-center">{formatValue(entry.Essen)}</td>
|
||||
<td className="p-2 text-center">{formatValue(entry.Gewicht)}</td>
|
||||
<td className="p-2 text-center">{formatValue(entry.DruckS)}</td>
|
||||
<td className="p-2 text-center">{formatValue(entry.DruckD)}</td>
|
||||
<td className="p-2 text-center">{formatValue(entry.Puls)}</td>
|
||||
{(onDelete || onEdit) && (
|
||||
<td className="p-2 text-center">
|
||||
<div className="flex gap-2 justify-center">
|
||||
{onEdit && (
|
||||
<button
|
||||
onClick={() => onEdit(entry)}
|
||||
className="text-blue-600 hover:text-blue-800 text-sm"
|
||||
>
|
||||
Editieren
|
||||
</button>
|
||||
)}
|
||||
{onDelete && (
|
||||
<button
|
||||
onClick={() => entry.ID && handleDelete(entry.ID)}
|
||||
className="text-red-600 hover:text-red-800 text-sm"
|
||||
>
|
||||
Löschen
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
)}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
18
create_table.sql
Normal file
18
create_table.sql
Normal file
@@ -0,0 +1,18 @@
|
||||
-- Create database and table Werte_BZG for health data tracking
|
||||
|
||||
CREATE DATABASE IF NOT EXISTS RXF CHARACTER SET utf8 COLLATE utf8_general_ci;
|
||||
|
||||
USE RXF;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS Werte_BZG (
|
||||
ID INT AUTO_INCREMENT PRIMARY KEY,
|
||||
Datum DATE NOT NULL,
|
||||
Zeit TIME NOT NULL,
|
||||
Zucker INT NULL COMMENT 'Blood sugar in mg/dl',
|
||||
Essen TEXT NULL COMMENT 'Meal information',
|
||||
Gewicht DECIMAL(5,1) NULL COMMENT 'Weight in kg',
|
||||
DruckS INT NULL COMMENT 'Systolic blood pressure in mmHg',
|
||||
DruckD INT NULL COMMENT 'Diastolic blood pressure in mmHg',
|
||||
Puls INT NULL COMMENT 'Pulse rate',
|
||||
INDEX idx_datum_zeit (Datum, Zeit)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci;
|
||||
36
docker-compose.yml
Normal file
36
docker-compose.yml
Normal file
@@ -0,0 +1,36 @@
|
||||
services:
|
||||
werte-app:
|
||||
build:
|
||||
context: .
|
||||
args:
|
||||
BUILD_DATE: ${BUILD_DATE:-$(date +%d.%m.%Y)}
|
||||
container_name: werte-next-app
|
||||
restart: unless-stopped
|
||||
# Port wird nicht nach außen exponiert - Traefik greift über das Docker-Netzwerk zu
|
||||
expose:
|
||||
- 3000
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
- DB_HOST=${DB_HOST}
|
||||
- DB_USER=${DB_USER}
|
||||
- DB_PASS=${DB_PASS}
|
||||
- DB_NAME=${DB_NAME}
|
||||
labels:
|
||||
- traefik.enable=true
|
||||
- traefik.http.routers.werte.entrypoints=http
|
||||
- traefik.http.routers.werte.rule=Host(`werte.fuerst-stuttgart.de`)
|
||||
- traefik.http.middlewares.werte-https-redirect.redirectscheme.scheme=https
|
||||
- traefik.http.routers.werte.middlewares=werte-https-redirect
|
||||
- traefik.http.routers.werte-secure.entrypoints=https
|
||||
- traefik.http.routers.werte-secure.rule=Host(`werte.fuerst-stuttgart.de`)
|
||||
- traefik.http.routers.werte-secure.tls=true
|
||||
- traefik.http.routers.werte-secure.tls.certresolver=letsencrypt
|
||||
- traefik.http.routers.werte-secure.service=werte
|
||||
- traefik.http.services.werte.loadbalancer.server.port=3000
|
||||
networks:
|
||||
- proxy
|
||||
networks:
|
||||
proxy:
|
||||
name: dockge_default
|
||||
external: true
|
||||
|
||||
29
lib/db.ts
Normal file
29
lib/db.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import mysql from 'mysql2/promise';
|
||||
import type { QueryResult } from 'mysql2/promise';
|
||||
|
||||
// Database configuration
|
||||
const dbConfig = {
|
||||
host: process.env.DB_HOST || 'mydbase_mysql',
|
||||
user: process.env.DB_USER || 'root',
|
||||
password: process.env.DB_PASS || 'SFluorit',
|
||||
database: process.env.DB_NAME || 'RXF',
|
||||
waitForConnections: true,
|
||||
connectionLimit: 10,
|
||||
queueLimit: 0,
|
||||
};
|
||||
|
||||
// Create a connection pool
|
||||
let pool: mysql.Pool | null = null;
|
||||
|
||||
export function getPool() {
|
||||
if (!pool) {
|
||||
pool = mysql.createPool(dbConfig);
|
||||
}
|
||||
return pool;
|
||||
}
|
||||
|
||||
export async function query(sql: string, params?: (string | number | null)[]): Promise<QueryResult> {
|
||||
const pool = getPool();
|
||||
const [rows] = await pool.execute(sql, params || []);
|
||||
return rows as QueryResult;
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { NextConfig } from "next";
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
/* config options here */
|
||||
output: 'standalone',
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
|
||||
163
package-lock.json
generated
163
package-lock.json
generated
@@ -1,13 +1,14 @@
|
||||
{
|
||||
"name": "werte_next",
|
||||
"version": "0.1.0",
|
||||
"version": "0.1.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "werte_next",
|
||||
"version": "0.1.0",
|
||||
"version": "0.1.1",
|
||||
"dependencies": {
|
||||
"mysql2": "^3.17.4",
|
||||
"next": "16.1.6",
|
||||
"react": "19.2.3",
|
||||
"react-dom": "19.2.3"
|
||||
@@ -1766,24 +1767,37 @@
|
||||
"typescript": ">=4.8.4 <6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
|
||||
"integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "18 || 20 || >=22"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
|
||||
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
|
||||
"version": "5.0.3",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.3.tgz",
|
||||
"integrity": "sha512-fy6KJm2RawA5RcHkLa1z/ScpBeA762UF9KmZQxwIbDtRJrgLzM10depAiEQ+CXYcoiqW1/m96OAAoke2nE9EeA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0"
|
||||
"balanced-match": "^4.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": "18 || 20 || >=22"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": {
|
||||
"version": "9.0.5",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
|
||||
"integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
|
||||
"version": "9.0.6",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.6.tgz",
|
||||
"integrity": "sha512-kQAVowdR33euIqeA0+VZTDqU+qo1IeVY+hrKYtZMio3Pg0P0vuh/kwRylLUddJhB6pf3q/botcOvRtx4IN1wqQ==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^2.0.1"
|
||||
"brace-expansion": "^5.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16 || 14 >=14.17"
|
||||
@@ -2396,6 +2410,15 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/aws-ssl-profiles": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz",
|
||||
"integrity": "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/axe-core": {
|
||||
"version": "4.11.1",
|
||||
"resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.11.1.tgz",
|
||||
@@ -2775,6 +2798,15 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/denque": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz",
|
||||
"integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/detect-libc": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
|
||||
@@ -3233,6 +3265,7 @@
|
||||
"integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@rtsao/scc": "^1.1.0",
|
||||
"array-includes": "^3.1.9",
|
||||
@@ -3654,6 +3687,15 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/generate-function": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz",
|
||||
"integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-property": "^1.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/generator-function": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz",
|
||||
@@ -3918,6 +3960,22 @@
|
||||
"hermes-estree": "0.25.1"
|
||||
}
|
||||
},
|
||||
"node_modules/iconv-lite": {
|
||||
"version": "0.7.2",
|
||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz",
|
||||
"integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"safer-buffer": ">= 2.1.2 < 3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/ignore": {
|
||||
"version": "5.3.2",
|
||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
|
||||
@@ -4240,6 +4298,12 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/is-property": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz",
|
||||
"integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/is-regex": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz",
|
||||
@@ -4838,6 +4902,12 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/long": {
|
||||
"version": "5.3.2",
|
||||
"resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz",
|
||||
"integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/loose-envify": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
|
||||
@@ -4861,6 +4931,21 @@
|
||||
"yallist": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/lru.min": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/lru.min/-/lru.min-1.1.4.tgz",
|
||||
"integrity": "sha512-DqC6n3QQ77zdFpCMASA1a3Jlb64Hv2N2DciFGkO/4L9+q/IpIAuRlKOvCXabtRW6cQf8usbmM6BE/TOPysCdIA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"bun": ">=1.0.0",
|
||||
"deno": ">=1.30.0",
|
||||
"node": ">=8.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/wellwelwel"
|
||||
}
|
||||
},
|
||||
"node_modules/magic-string": {
|
||||
"version": "0.30.21",
|
||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
|
||||
@@ -4906,9 +4991,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/minimatch": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.3.tgz",
|
||||
"integrity": "sha512-M2GCs7Vk83NxkUyQV1bkABc4yxgz9kILhHImZiBPAZ9ybuvCb0/H7lEl5XvIg3g+9d4eNotkZA5IWwYl0tibaA==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
@@ -4935,6 +5020,37 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/mysql2": {
|
||||
"version": "3.17.4",
|
||||
"resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.17.4.tgz",
|
||||
"integrity": "sha512-RnfuK5tyIuaiPMWOCTTl4vQX/mQXqSA8eoIbwvWccadvPGvh+BYWWVecInMS5s7wcLUkze8LqJzwB/+A4uwuAA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"aws-ssl-profiles": "^1.1.2",
|
||||
"denque": "^2.1.0",
|
||||
"generate-function": "^2.3.1",
|
||||
"iconv-lite": "^0.7.2",
|
||||
"long": "^5.3.2",
|
||||
"lru.min": "^1.1.4",
|
||||
"named-placeholders": "^1.1.6",
|
||||
"sql-escaper": "^1.3.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/named-placeholders": {
|
||||
"version": "1.1.6",
|
||||
"resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.6.tgz",
|
||||
"integrity": "sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"lru.min": "^1.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
"version": "3.3.11",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
|
||||
@@ -5630,6 +5746,12 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/safer-buffer": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/scheduler": {
|
||||
"version": "0.27.0",
|
||||
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
|
||||
@@ -5861,6 +5983,21 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/sql-escaper": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/sql-escaper/-/sql-escaper-1.3.3.tgz",
|
||||
"integrity": "sha512-BsTCV265VpTp8tm1wyIm1xqQCS+Q9NHx2Sr+WcnUrgLrQ6yiDIvHYJV5gHxsj1lMBy2zm5twLaZao8Jd+S8JJw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"bun": ">=1.0.0",
|
||||
"deno": ">=2.0.0",
|
||||
"node": ">=12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/mysqljs/sql-escaper?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/stable-hash": {
|
||||
"version": "0.0.5",
|
||||
"resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.5.tgz",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "werte_next",
|
||||
"version": "0.1.0",
|
||||
"version": "0.1.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
@@ -9,6 +9,7 @@
|
||||
"lint": "eslint"
|
||||
},
|
||||
"dependencies": {
|
||||
"mysql2": "^3.17.4",
|
||||
"next": "16.1.6",
|
||||
"react": "19.2.3",
|
||||
"react-dom": "19.2.3"
|
||||
|
||||
22
types/werte.ts
Normal file
22
types/werte.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
export interface WerteEntry {
|
||||
ID?: number;
|
||||
Datum: string;
|
||||
Zeit: string;
|
||||
Zucker?: number | string;
|
||||
Essen?: string;
|
||||
Gewicht?: number | string;
|
||||
DruckS?: number | string;
|
||||
DruckD?: number | string;
|
||||
Puls?: number | string;
|
||||
}
|
||||
|
||||
export interface CreateWerteEntry {
|
||||
Datum: string;
|
||||
Zeit: string;
|
||||
Zucker?: number | string;
|
||||
Essen?: string;
|
||||
Gewicht?: number | string;
|
||||
DruckS?: number | string;
|
||||
DruckD?: number | string;
|
||||
Puls?: number | string;
|
||||
}
|
||||
Reference in New Issue
Block a user