From 63c93fa7e4f50706a59244aeeda84bf206b82d35 Mon Sep 17 00:00:00 2001 From: Bilal Teke Date: Wed, 15 Apr 2026 21:41:48 +0200 Subject: [PATCH] v3.-funktioniert --- app/components/EmptyState.tsx | 31 ++++++++ app/components/FilterBar.tsx | 114 ++++++++++++++++++++++++++++++ app/components/Header.tsx | 19 ++++- app/components/ServiceCard.tsx | 84 +++++++++++++--------- app/components/ServiceGrid.tsx | 27 +++++++ app/components/ServiceSection.tsx | 84 ++++++++++++++++++++++ app/components/index.ts | 9 +++ app/page.tsx | 37 +--------- postcss.config.js | 7 +- 9 files changed, 336 insertions(+), 76 deletions(-) create mode 100644 app/components/EmptyState.tsx create mode 100644 app/components/FilterBar.tsx create mode 100644 app/components/ServiceGrid.tsx create mode 100644 app/components/ServiceSection.tsx create mode 100644 app/components/index.ts diff --git a/app/components/EmptyState.tsx b/app/components/EmptyState.tsx new file mode 100644 index 0000000..055e7ca --- /dev/null +++ b/app/components/EmptyState.tsx @@ -0,0 +1,31 @@ +'use client'; + +/** + * EmptyState - Zeigt eine Nachricht an, wenn keine Services konfiguriert sind + */ +export function EmptyState() { + return ( +
+ +

+ Keine Dienste konfiguriert +

+

+ Füge Dienste in der Konfigurationsdatei src/data/services.ts hinzu, um sie hier anzuzeigen +

+
+ ); +} diff --git a/app/components/FilterBar.tsx b/app/components/FilterBar.tsx new file mode 100644 index 0000000..ce37837 --- /dev/null +++ b/app/components/FilterBar.tsx @@ -0,0 +1,114 @@ +'use client'; + +import { ServiceCategory, ServiceStatus } from '@/lib/data'; + +interface FilterBarProps { + categories: ServiceCategory[]; + statuses: ServiceStatus[]; + selectedCategories: ServiceCategory[]; + selectedStatuses: ServiceStatus[]; + onCategoryChange: (category: ServiceCategory) => void; + onStatusChange: (status: ServiceStatus) => void; + onReset: () => void; +} + +/** + * FilterBar - Filterleiste zum Filtern von Services nach Kategorie und Status + * @param categories - Verfügbare Kategorien + * @param statuses - Verfügbare Status + * @param selectedCategories - Aktuell ausgewählte Kategorien + * @param selectedStatuses - Aktuell ausgewählte Status + * @param onCategoryChange - Callback beim Ändern einer Kategorie + * @param onStatusChange - Callback beim Ändern eines Status + * @param onReset - Callback zum Zurücksetzen aller Filter + */ +export function FilterBar({ + categories, + statuses, + selectedCategories, + selectedStatuses, + onCategoryChange, + onStatusChange, + onReset, +}: FilterBarProps) { + const hasActiveFilters = + selectedCategories.length > 0 || selectedStatuses.length > 0; + + return ( +
+
+ {/* Kategorie Filter */} +
+

+ Kategorie +

+
+ {categories.map((category) => ( + + ))} +
+
+ + {/* Status Filter */} +
+

+ Status +

+
+ {statuses.map((status) => ( + + ))} +
+
+ + {/* Reset Button */} + {hasActiveFilters && ( +
+ + + {selectedCategories.length + selectedStatuses.length} Filter aktiv + +
+ )} +
+
+ ); +} diff --git a/app/components/Header.tsx b/app/components/Header.tsx index 52958d5..2c453cd 100644 --- a/app/components/Header.tsx +++ b/app/components/Header.tsx @@ -1,15 +1,28 @@ 'use client'; -export function Header() { +interface HeaderProps { + title?: string; + subtitle?: string; +} + +/** + * Header - Hauptkopfzeile des Dashboards + * @param title - Der Titel des Headers (Standard: "Homelab Dashboard") + * @param subtitle - Der Untertitel des Headers (Standard: "Übersicht aller verwalteten Dienste") + */ +export function Header({ + title = 'Homelab Dashboard', + subtitle = 'Übersicht aller verwalteten Dienste' +}: HeaderProps) { return (

- Homelab Dashboard + {title}

- Übersicht aller verwalteten Dienste + {subtitle}

diff --git a/app/components/ServiceCard.tsx b/app/components/ServiceCard.tsx index b9c795f..d16ad49 100644 --- a/app/components/ServiceCard.tsx +++ b/app/components/ServiceCard.tsx @@ -6,7 +6,7 @@ interface ServiceCardProps { service: Service; } -const statusConfig = { +const STATUS_CONFIG = { online: { bgColor: 'bg-green-50 dark:bg-green-950', textColor: 'text-green-700 dark:text-green-300', @@ -25,10 +25,14 @@ const statusConfig = { dotColor: 'bg-red-500', label: 'Offline', }, -}; +} as const; +/** + * ServiceCard - Zeigt einen einzelnen Service mit Status, Icon und Öffnen-Button + * @param service - Das Service-Objekt mit Name, Beschreibung, Status, etc. + */ export function ServiceCard({ service }: ServiceCardProps) { - const config = statusConfig[service.status]; + const config = STATUS_CONFIG[service.status]; return (
- {/* Header mit Name und Status */} + {/* Header mit Icon, Name, Kategorie und Status */}
- {service.icon && {service.icon}} -
-

+ {service.icon && ( + + )} +
+

{service.name}

-

+

{service.category}

-
- + + {/* Status Badge */} +
+ {config.label} @@ -61,35 +75,35 @@ export function ServiceCard({ service }: ServiceCardProps) {
{/* Beschreibung */} -

+

{service.description}

- {/* Button */} -
- -
+ + +
diff --git a/app/components/ServiceGrid.tsx b/app/components/ServiceGrid.tsx new file mode 100644 index 0000000..4d2bace --- /dev/null +++ b/app/components/ServiceGrid.tsx @@ -0,0 +1,27 @@ +'use client'; + +import { Service } from '@/lib/data'; +import { ServiceCard } from './ServiceCard'; +import { EmptyState } from './EmptyState'; + +interface ServiceGridProps { + services: Service[]; +} + +/** + * ServiceGrid - Rendert Services in einem responsiven Grid-Layout + * @param services - Array von Service-Objekten + */ +export function ServiceGrid({ services }: ServiceGridProps) { + if (services.length === 0) { + return ; + } + + return ( +
+ {services.map((service) => ( + + ))} +
+ ); +} diff --git a/app/components/ServiceSection.tsx b/app/components/ServiceSection.tsx new file mode 100644 index 0000000..38cfd52 --- /dev/null +++ b/app/components/ServiceSection.tsx @@ -0,0 +1,84 @@ +'use client'; + +import { useState, useMemo } from 'react'; +import { Service, ServiceCategory, ServiceStatus } from '@/lib/data'; +import { FilterBar } from './FilterBar'; +import { ServiceGrid } from './ServiceGrid'; + +interface ServiceSectionProps { + services: Service[]; +} + +/** + * ServiceSection - Verwaltet Filterlogik und rendert FilterBar + ServiceGrid + * @param services - Array aller verfügbaren Services + */ +export function ServiceSection({ services }: ServiceSectionProps) { + const [selectedCategories, setSelectedCategories] = useState< + ServiceCategory[] + >([]); + const [selectedStatuses, setSelectedStatuses] = useState([]); + + // Extrahiere eindeutige Kategorien und Status aus Services + const categories = useMemo( + () => Array.from(new Set(services.map((s) => s.category))).sort(), + [services] + ); + + const statuses = useMemo( + () => + (['online', 'warning', 'offline'] as ServiceStatus[]).filter((status) => + services.some((s) => s.status === status) + ), + [services] + ); + + // Filtere Services basierend auf ausgewählten Filtern + const filteredServices = useMemo(() => { + return services.filter((service) => { + const categoryMatch = + selectedCategories.length === 0 || + selectedCategories.includes(service.category); + + const statusMatch = + selectedStatuses.length === 0 || + selectedStatuses.includes(service.status); + + return categoryMatch && statusMatch; + }); + }, [services, selectedCategories, selectedStatuses]); + + const handleCategoryChange = (category: ServiceCategory) => { + setSelectedCategories((prev) => + prev.includes(category) + ? prev.filter((c) => c !== category) + : [...prev, category] + ); + }; + + const handleStatusChange = (status: ServiceStatus) => { + setSelectedStatuses((prev) => + prev.includes(status) ? prev.filter((s) => s !== status) : [...prev, status] + ); + }; + + const handleReset = () => { + setSelectedCategories([]); + setSelectedStatuses([]); + }; + + return ( + <> + + + + ); +} diff --git a/app/components/index.ts b/app/components/index.ts new file mode 100644 index 0000000..04f96d3 --- /dev/null +++ b/app/components/index.ts @@ -0,0 +1,9 @@ +'use client'; + +// Re-export all components from this directory +export { Header } from './Header'; +export { ServiceCard } from './ServiceCard'; +export { ServiceGrid } from './ServiceGrid'; +export { ServiceSection } from './ServiceSection'; +export { FilterBar } from './FilterBar'; +export { EmptyState } from './EmptyState'; diff --git a/app/page.tsx b/app/page.tsx index bc0a616..ebbf244 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,6 +1,5 @@ import type { Metadata } from 'next'; -import { Header } from './components/Header'; -import { ServiceCard } from './components/ServiceCard'; +import { Header, ServiceSection } from './components'; import { services } from '@/lib/data'; import './globals.css'; @@ -13,39 +12,9 @@ export default function Home() { return (
- -
- {/* Services Grid */} -
- {services.map((service) => ( - - ))} -
- {/* Empty State (falls keine Services) */} - {services.length === 0 && ( -
- - - -

- Keine Dienste konfiguriert -

-

- Füge Dienste in der Datenkonfiguration hinzu, um sie hier anzuzeigen -

-
- )} +
+
); diff --git a/postcss.config.js b/postcss.config.js index 1130ccd..bad971d 100644 --- a/postcss.config.js +++ b/postcss.config.js @@ -1,10 +1,9 @@ -import type { Config } from 'postcss-load-config' - -const config: Config = { +/** @type {import('postcss-load-config').Config} */ +const config = { plugins: { tailwindcss: {}, autoprefixer: {}, }, } -export default config +export default config \ No newline at end of file