This commit is contained in:
Bilal Teke 2026-04-15 21:19:28 +02:00
commit 787aab5e1d
16 changed files with 7129 additions and 0 deletions

7
.eslintrc.json Normal file
View file

@ -0,0 +1,7 @@
{
"extends": "next/core-web-vitals",
"rules": {
"react-hooks/rules-of-hooks": "error",
"react-hooks/exhaustive-deps": "warn"
}
}

34
.gitignore vendored Normal file
View file

@ -0,0 +1,34 @@
# dependencies
/node_modules
/.pnp
.pnp.js
.yarn/install-state.gz
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# local env files
.env*.local
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts

117
README.md Normal file
View file

@ -0,0 +1,117 @@
# Homelab Dashboard
Ein modernes, responsives Dashboard für die Verwaltung von Homelab-Diensten, gebaut mit **Next.js 15**, **TypeScript** und **Tailwind CSS**.
## Features
✨ **Moderne Benutzeroberfläche**
- Elegantes Dark/Light Mode Design
- Responsive Grid-Layout für alle Bildschirmgrößen
- Smooth Animations und Übergänge
🎯 **Service-Management**
- Service-Karten mit Name, Beschreibung und Status
- Farbcodierter Status-Indikator (Online, Warnung, Offline)
- Direkte Links zu Services
- Pulsierender Status-Punkt für visuelle Rückmeldung
📱 **Mobile-First Design**
- Optimiert für Smartphones, Tablets und Desktop
- Touch-freundliche Buttons
- Flexibles Grid-System
## Installation & Setup
### Voraussetzungen
- Node.js 18+ installiert
- npm oder yarn
### Schritt 1: Dependencies installieren
```bash
npm install
```
### Schritt 2: Entwicklungsserver starten
```bash
npm run dev
```
Der Server läuft dann unter `http://localhost:3000`
## Projekt-Struktur
```
homelab-dashboard/
├── app/
│ ├── components/
│ │ ├── Header.tsx # Header-Komponente
│ │ └── ServiceCard.tsx # Service-Karten-Komponente
│ ├── globals.css # Globale Tailwind Styles
│ ├── layout.tsx # Root Layout
│ └── page.tsx # Startseite
├── lib/
│ └── data.ts # Mock-Daten und Service-Typen
├── tailwind.config.ts # Tailwind Konfiguration
├── postcss.config.js # PostCSS Konfiguration
├── next.config.js # Next.js Konfiguration
├── tsconfig.json # TypeScript Konfiguration
└── package.json # Abhängigkeiten
```
## Dienste hinzufügen/bearbeiten
Bearbeite die Datei [src/data/services.ts](src/data/services.ts):
```typescript
export const services: Service[] = [
{
id: 'unique-id',
name: 'Dein Service',
description: 'Was macht dieser Service?',
category: 'Infrastruktur', // oder Smart Home, Medien, Dokumente
status: 'online', // online | warning | offline
url: 'http://localhost:8000',
icon: '🖥️', // Optional - ein Emoji für visuelles Feedback
},
// ... weitere Services
];
```
### Verfügbare Kategorien
- **Infrastruktur**: Server, Netzwerk, Monitoring
- **Smart Home**: Home Automation und IoT
- **Medien**: Medienserver und Downloading
- **Dokumente**: Dateispeicher und Code-Repositories
## Verfügbare Commands
- `npm run dev` - Entwicklungsserver starten
- `npm run build` - Produktions-Build erstellen
- `npm start` - Produktions-Server starten
- `npm run lint` - Linter ausführen
## Status-Indikatoren
| Status | Farbe | Beschreibung |
|--------|-------|-------------|
| Online | 🟢 Grün | Service läuft einwandfrei |
| Warning | 🟡 Orange | Service hat Probleme, läuft aber |
| Offline | 🔴 Rot | Service ist nicht erreichbar |
## Styling & Anpassungen
Das Projekt nutzt **Tailwind CSS** für Styling. Eigene Farben und Konfiguration können in [tailwind.config.ts](tailwind.config.ts) angepasst werden.
## Browser-Unterstützung
Das Dashboard funktioniert auf modernen Browsern:
- Chrome/Edge 90+
- Firefox 88+
- Safari 14+
## Lizenz
MIT

18
app/components/Header.tsx Normal file
View file

@ -0,0 +1,18 @@
'use client';
export function Header() {
return (
<header className="bg-gradient-to-r from-slate-900 to-slate-800 border-b border-slate-700 shadow-lg">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
<div className="flex flex-col gap-2">
<h1 className="text-3xl sm:text-4xl font-bold text-white tracking-tight">
Homelab Dashboard
</h1>
<p className="text-slate-400 text-sm">
Übersicht aller verwalteten Dienste
</p>
</div>
</div>
</header>
);
}

View file

@ -0,0 +1,97 @@
'use client';
import { Service } from '@/lib/data';
interface ServiceCardProps {
service: Service;
}
const statusConfig = {
online: {
bgColor: 'bg-green-50 dark:bg-green-950',
textColor: 'text-green-700 dark:text-green-300',
dotColor: 'bg-green-500',
label: 'Online',
},
warning: {
bgColor: 'bg-amber-50 dark:bg-amber-950',
textColor: 'text-amber-700 dark:text-amber-300',
dotColor: 'bg-amber-500',
label: 'Warnung',
},
offline: {
bgColor: 'bg-red-50 dark:bg-red-950',
textColor: 'text-red-700 dark:text-red-300',
dotColor: 'bg-red-500',
label: 'Offline',
},
};
export function ServiceCard({ service }: ServiceCardProps) {
const config = statusConfig[service.status];
return (
<a
href={service.url}
target="_blank"
rel="noopener noreferrer"
className="group block"
>
<div className="h-full bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-lg shadow-sm hover:shadow-md transition-all duration-300 overflow-hidden hover:border-slate-300 dark:hover:border-slate-600">
<div className="p-6 flex flex-col gap-4 h-full">
{/* Header mit Name und Status */}
<div className="flex items-start justify-between gap-4">
<div className="flex-1 flex items-center gap-3">
{service.icon && <span className="text-2xl">{service.icon}</span>}
<div>
<h3 className="text-lg font-semibold text-slate-900 dark:text-white group-hover:text-blue-600 dark:group-hover:text-blue-400 transition-colors">
{service.name}
</h3>
<p className="text-xs text-slate-500 dark:text-slate-400">
{service.category}
</p>
</div>
</div>
<div className={`flex items-center gap-2 px-3 py-1 rounded-full ${config.bgColor}`}>
<span className={`w-2 h-2 rounded-full ${config.dotColor} animate-pulse`} />
<span className={`text-xs font-medium ${config.textColor}`}>
{config.label}
</span>
</div>
</div>
{/* Beschreibung */}
<p className="text-sm text-slate-600 dark:text-slate-400 flex-grow">
{service.description}
</p>
{/* Button */}
<div>
<button
onClick={(e) => {
e.preventDefault();
window.open(service.url, '_blank');
}}
className="w-full bg-blue-600 hover:bg-blue-700 text-white font-medium py-2 px-4 rounded-lg transition-colors duration-200 flex items-center justify-center gap-2 group/btn"
>
Öffnen
<svg
className="w-4 h-4 group-hover/btn:translate-x-1 transition-transform"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"
/>
</svg>
</button>
</div>
</div>
</div>
</a>
);
}

21
app/globals.css Normal file
View file

@ -0,0 +1,21 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
* {
@apply border-slate-200;
}
html {
color-scheme: light dark;
}
body {
@apply bg-slate-50 dark:bg-slate-900 text-slate-900 dark:text-slate-50;
}
@layer components {
.card {
@apply bg-white dark:bg-slate-800 rounded-lg border border-slate-200 dark:border-slate-700;
}
}

20
app/layout.tsx Normal file
View file

@ -0,0 +1,20 @@
import type { Metadata } from 'next';
import { Metadata as NextMetadata } from 'next';
export const metadata: NextMetadata = {
manifest: '/manifest.json',
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="de">
<body className="antialiased">
{children}
</body>
</html>
);
}

52
app/page.tsx Normal file
View file

@ -0,0 +1,52 @@
import type { Metadata } from 'next';
import { Header } from './components/Header';
import { ServiceCard } from './components/ServiceCard';
import { services } from '@/lib/data';
import './globals.css';
export const metadata: Metadata = {
title: 'Homelab Dashboard',
description: 'Verwaltungs-Dashboard für dein Homelab',
};
export default function Home() {
return (
<div className="min-h-screen bg-slate-50 dark:bg-slate-900">
<Header />
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
{/* Services Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
{services.map((service) => (
<ServiceCard key={service.id} service={service} />
))}
</div>
{/* Empty State (falls keine Services) */}
{services.length === 0 && (
<div className="flex flex-col items-center justify-center py-12">
<svg
className="w-16 h-16 text-slate-400 mb-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={1.5}
d="M20 13V6a2 2 0 00-2-2H6a2 2 0 00-2 2v7m16 0v5a2 2 0 01-2 2H6a2 2 0 01-2-2v-5m16 0h-2.586a1 1 0 00-.707.293l-2.414 2.414a1 1 0 01-.707.293h-3.172a1 1 0 01-.707-.293l-2.414-2.414A1 1 0 006.586 13H4"
/>
</svg>
<h3 className="text-xl font-semibold text-slate-900 dark:text-white">
Keine Dienste konfiguriert
</h3>
<p className="text-slate-600 dark:text-slate-400 mt-2">
Füge Dienste in der Datenkonfiguration hinzu, um sie hier anzuzeigen
</p>
</div>
)}
</main>
</div>
);
}

2
lib/data.ts Normal file
View file

@ -0,0 +1,2 @@
// Re-export from the main services data file
export * from '@/src/data/services';

4
next.config.js Normal file
View file

@ -0,0 +1,4 @@
/** @type {import('next').NextConfig} */
const nextConfig = {}
module.exports = nextConfig

6521
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

29
package.json Normal file
View file

@ -0,0 +1,29 @@
{
"name": "homelab-dashboard",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"next": "^16.2.3",
"react": "^18.3.1",
"react-dom": "^18.3.1"
},
"devDependencies": {
"@types/node": "^20.10.6",
"@types/react": "^18.2.46",
"@types/react-dom": "^18.2.18",
"@typescript-eslint/eslint-plugin": "^8.58.2",
"@typescript-eslint/parser": "^8.58.2",
"autoprefixer": "^10.4.16",
"eslint": "^8.56.0",
"eslint-config-next": "^15.0.3",
"postcss": "^8.4.32",
"tailwindcss": "^3.4.1",
"typescript": "^5.3.3"
}
}

10
postcss.config.js Normal file
View file

@ -0,0 +1,10 @@
import type { Config } from 'postcss-load-config'
const config: Config = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}
export default config

130
src/data/services.ts Normal file
View file

@ -0,0 +1,130 @@
export type ServiceCategory = 'Infrastruktur' | 'Smart Home' | 'Medien' | 'Dokumente';
export type ServiceStatus = 'online' | 'warning' | 'offline';
export interface Service {
id: string;
name: string;
description: string;
category: ServiceCategory;
status: ServiceStatus;
url: string;
icon?: string;
}
export const services: Service[] = [
// Infrastruktur
{
id: 'unraid',
name: 'Unraid',
description: 'NAS und Server Management Platform',
category: 'Infrastruktur',
status: 'online',
url: 'http://localhost',
icon: '🖥️',
},
{
id: 'unifi',
name: 'UniFi',
description: 'Netzwerk-Management und WLAN-Controller',
category: 'Infrastruktur',
status: 'online',
url: 'http://localhost:8443',
icon: '🌐',
},
{
id: 'checkmk',
name: 'Checkmk',
description: 'Monitoring und Alerting System',
category: 'Infrastruktur',
status: 'online',
url: 'http://localhost/checkmk',
icon: '📊',
},
{
id: 'nginx-proxy',
name: 'Nginx Proxy Manager',
description: 'Reverse Proxy und SSL-Verwaltung',
category: 'Infrastruktur',
status: 'online',
url: 'http://localhost:81',
icon: '⚙️',
},
// Smart Home
{
id: 'home-assistant',
name: 'Home Assistant',
description: 'Smart Home Automation und Integration',
category: 'Smart Home',
status: 'offline',
url: 'http://localhost:8123',
icon: '🏠',
},
// Medien
{
id: 'plex',
name: 'Plex',
description: 'Medienserver und Streaming-Dienst',
category: 'Medien',
status: 'online',
url: 'http://localhost:32400',
icon: '🎬',
},
{
id: 'jellyfin',
name: 'Jellyfin',
description: 'Open-Source Medienserver',
category: 'Medien',
status: 'online',
url: 'http://localhost:8096',
icon: '📺',
},
{
id: 'radarr',
name: 'Radarr',
description: 'Automatisches Filmmanagement',
category: 'Medien',
status: 'online',
url: 'http://localhost:7878',
icon: '🎥',
},
{
id: 'sonarr',
name: 'Sonarr',
description: 'Automatisches Serienmanagement',
category: 'Medien',
status: 'online',
url: 'http://localhost:8989',
icon: '📺',
},
{
id: 'prowlarr',
name: 'Prowlarr',
description: 'Indexer und Proxy Manager',
category: 'Medien',
status: 'warning',
url: 'http://localhost:9696',
icon: '🔍',
},
// Dokumente
{
id: 'seafile',
name: 'Seafile',
description: 'Dateifreigabe und Cloud Storage',
category: 'Dokumente',
status: 'online',
url: 'http://localhost:8000',
icon: '☁️',
},
{
id: 'forgejo',
name: 'Forgejo',
description: 'Self-hosted Git und Code Repository',
category: 'Dokumente',
status: 'online',
url: 'http://localhost:3000',
icon: '💾',
},
];

18
tailwind.config.ts Normal file
View file

@ -0,0 +1,18 @@
import type { Config } from 'tailwindcss'
const config: Config = {
content: [
'./app/**/*.{js,ts,jsx,tsx,mdx}',
],
theme: {
extend: {
colors: {
success: '#10b981',
warning: '#f59e0b',
error: '#ef4444',
},
},
},
plugins: [],
}
export default config

49
tsconfig.json Normal file
View file

@ -0,0 +1,49 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": [
"ES2020",
"DOM",
"DOM.Iterable"
],
"module": "ESNext",
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"baseUrl": ".",
"paths": {
"@/*": [
"./*"
]
},
"allowJs": true,
"incremental": true,
"plugins": [
{
"name": "next"
}
]
},
"include": [
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts",
".next/dev/types/**/*.ts"
],
"exclude": [
"node_modules"
]
}