Add blog posts, cleanup unused files, update components
- Add 100 blog posts covering AI, development, and tech topics - Add .env.example for environment configuration - Add accessibility and lighthouse audit scripts - Remove obsolete SEO reports and temporary files - Remove dev-dist build artifacts and backup files - Remove unused portrait images (moved/consolidated elsewhere) - Update contact form and component improvements Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,809 @@
|
||||
# Smart Home Dashboard Development
|
||||
|
||||
**Meta-Description:** Smart Home Dashboard mit React und WebSocket. Real-time Updates, Device Control und responsive Design für die Heimautomatisierung.
|
||||
|
||||
**Keywords:** Smart Home Dashboard, React, WebSocket, Home Automation, Real-time UI, Device Control, IoT Dashboard
|
||||
|
||||
---
|
||||
|
||||
## Einführung
|
||||
|
||||
Ein **Smart Home Dashboard** ist die zentrale Steuerung für alle vernetzten Geräte. Mit **React**, **WebSocket** und modernem UI-Design bauen wir ein responsives Dashboard für Echtzeit-Monitoring und Gerätesteuerung.
|
||||
|
||||
---
|
||||
|
||||
## Dashboard Architecture
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ SMART HOME DASHBOARD ARCHITECTURE │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ Frontend: │
|
||||
│ ┌─────────────────────────────────────────────────────┐ │
|
||||
│ │ React + Next.js │ │
|
||||
│ │ ┌───────────┐ ┌───────────┐ ┌───────────┐ │ │
|
||||
│ │ │ Device │ │ Room │ │ Scenes │ │ │
|
||||
│ │ │ Cards │ │ View │ │ Manager │ │ │
|
||||
│ │ └───────────┘ └───────────┘ └───────────┘ │ │
|
||||
│ │ ┌───────────┐ ┌───────────┐ ┌───────────┐ │ │
|
||||
│ │ │ Energy │ │ Climate │ │ Security │ │ │
|
||||
│ │ │ Monitor │ │ Control │ │ Panel │ │ │
|
||||
│ │ └───────────┘ └───────────┘ └───────────┘ │ │
|
||||
│ └─────────────────────────────────────────────────────┘ │
|
||||
│ │ │
|
||||
│ WebSocket / REST │
|
||||
│ │ │
|
||||
│ Backend: ▼ │
|
||||
│ ┌─────────────────────────────────────────────────────┐ │
|
||||
│ │ API Gateway / BFF │ │
|
||||
│ └─────────────────────────────────────────────────────┘ │
|
||||
│ │ │
|
||||
│ ┌────────────┬───────────┴───────────┬────────────┐ │
|
||||
│ │ MQTT │ Home Assistant │ Database │ │
|
||||
│ │ Broker │ API │ (States) │ │
|
||||
│ └────────────┴───────────────────────┴────────────┘ │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## React Dashboard Components
|
||||
|
||||
```typescript
|
||||
// components/dashboard/DeviceCard.tsx
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
import { Slider } from '@/components/ui/slider';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
interface DeviceCardProps {
|
||||
device: {
|
||||
id: string;
|
||||
name: string;
|
||||
type: 'light' | 'switch' | 'sensor' | 'thermostat';
|
||||
state: 'on' | 'off';
|
||||
brightness?: number;
|
||||
temperature?: number;
|
||||
humidity?: number;
|
||||
setpoint?: number;
|
||||
};
|
||||
onStateChange: (id: string, state: Partial<typeof device>) => void;
|
||||
}
|
||||
|
||||
export function DeviceCard({ device, onStateChange }: DeviceCardProps) {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const handleToggle = async (checked: boolean) => {
|
||||
setIsLoading(true);
|
||||
await onStateChange(device.id, { state: checked ? 'on' : 'off' });
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
const handleBrightness = async (value: number[]) => {
|
||||
await onStateChange(device.id, { brightness: value[0] });
|
||||
};
|
||||
|
||||
const handleSetpoint = async (value: number[]) => {
|
||||
await onStateChange(device.id, { setpoint: value[0] });
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className={cn(
|
||||
'transition-all duration-200',
|
||||
device.state === 'on' && 'ring-2 ring-primary'
|
||||
)}>
|
||||
<CardHeader className="flex flex-row items-center justify-between pb-2">
|
||||
<CardTitle className="text-sm font-medium">{device.name}</CardTitle>
|
||||
{(device.type === 'light' || device.type === 'switch') && (
|
||||
<Switch
|
||||
checked={device.state === 'on'}
|
||||
onCheckedChange={handleToggle}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
)}
|
||||
</CardHeader>
|
||||
|
||||
<CardContent>
|
||||
{/* Light with Brightness */}
|
||||
{device.type === 'light' && device.brightness !== undefined && (
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between text-sm">
|
||||
<span>Brightness</span>
|
||||
<span>{device.brightness}%</span>
|
||||
</div>
|
||||
<Slider
|
||||
value={[device.brightness]}
|
||||
onValueChange={handleBrightness}
|
||||
max={100}
|
||||
step={1}
|
||||
disabled={device.state === 'off'}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Sensor Readings */}
|
||||
{device.type === 'sensor' && (
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
{device.temperature !== undefined && (
|
||||
<div className="text-center">
|
||||
<div className="text-2xl font-bold">{device.temperature}°C</div>
|
||||
<div className="text-xs text-muted-foreground">Temperature</div>
|
||||
</div>
|
||||
)}
|
||||
{device.humidity !== undefined && (
|
||||
<div className="text-center">
|
||||
<div className="text-2xl font-bold">{device.humidity}%</div>
|
||||
<div className="text-xs text-muted-foreground">Humidity</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Thermostat */}
|
||||
{device.type === 'thermostat' && (
|
||||
<div className="space-y-4">
|
||||
<div className="text-center">
|
||||
<div className="text-3xl font-bold">{device.temperature}°C</div>
|
||||
<div className="text-xs text-muted-foreground">Current</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between text-sm">
|
||||
<span>Setpoint</span>
|
||||
<span>{device.setpoint}°C</span>
|
||||
</div>
|
||||
<Slider
|
||||
value={[device.setpoint || 20]}
|
||||
onValueChange={handleSetpoint}
|
||||
min={16}
|
||||
max={28}
|
||||
step={0.5}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Room View
|
||||
|
||||
```typescript
|
||||
// components/dashboard/RoomView.tsx
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { DeviceCard } from './DeviceCard';
|
||||
import { useSmartHome } from '@/hooks/useSmartHome';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
import {
|
||||
Home,
|
||||
Sofa,
|
||||
Bed,
|
||||
UtensilsCrossed,
|
||||
Bath,
|
||||
TreeDeciduous
|
||||
} from 'lucide-react';
|
||||
|
||||
const roomIcons: Record<string, any> = {
|
||||
all: Home,
|
||||
living_room: Sofa,
|
||||
bedroom: Bed,
|
||||
kitchen: UtensilsCrossed,
|
||||
bathroom: Bath,
|
||||
outdoor: TreeDeciduous
|
||||
};
|
||||
|
||||
export function RoomView() {
|
||||
const { devices, rooms, updateDevice, isConnected } = useSmartHome();
|
||||
const [activeRoom, setActiveRoom] = useState('all');
|
||||
|
||||
const filteredDevices = activeRoom === 'all'
|
||||
? devices
|
||||
: devices.filter(d => d.room === activeRoom);
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Connection Status */}
|
||||
<div className="flex items-center gap-2">
|
||||
<div className={`w-2 h-2 rounded-full ${
|
||||
isConnected ? 'bg-green-500' : 'bg-red-500'
|
||||
}`} />
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{isConnected ? 'Connected' : 'Disconnected'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Room Tabs */}
|
||||
<Tabs value={activeRoom} onValueChange={setActiveRoom}>
|
||||
<TabsList className="flex-wrap h-auto">
|
||||
<TabsTrigger value="all" className="gap-2">
|
||||
<Home className="w-4 h-4" />
|
||||
All
|
||||
</TabsTrigger>
|
||||
{rooms.map(room => {
|
||||
const Icon = roomIcons[room.id] || Home;
|
||||
return (
|
||||
<TabsTrigger key={room.id} value={room.id} className="gap-2">
|
||||
<Icon className="w-4 h-4" />
|
||||
{room.name}
|
||||
</TabsTrigger>
|
||||
);
|
||||
})}
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value={activeRoom} className="mt-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
|
||||
{filteredDevices.map(device => (
|
||||
<DeviceCard
|
||||
key={device.id}
|
||||
device={device}
|
||||
onStateChange={updateDevice}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{filteredDevices.length === 0 && (
|
||||
<div className="text-center py-12 text-muted-foreground">
|
||||
No devices in this room
|
||||
</div>
|
||||
)}
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## WebSocket Hook
|
||||
|
||||
```typescript
|
||||
// hooks/useSmartHome.ts
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect, useCallback, useRef } from 'react';
|
||||
|
||||
interface Device {
|
||||
id: string;
|
||||
name: string;
|
||||
type: 'light' | 'switch' | 'sensor' | 'thermostat';
|
||||
room: string;
|
||||
state: 'on' | 'off';
|
||||
brightness?: number;
|
||||
temperature?: number;
|
||||
humidity?: number;
|
||||
setpoint?: number;
|
||||
lastUpdated: string;
|
||||
}
|
||||
|
||||
interface Room {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
interface SmartHomeState {
|
||||
devices: Device[];
|
||||
rooms: Room[];
|
||||
isConnected: boolean;
|
||||
}
|
||||
|
||||
export function useSmartHome() {
|
||||
const [state, setState] = useState<SmartHomeState>({
|
||||
devices: [],
|
||||
rooms: [],
|
||||
isConnected: false
|
||||
});
|
||||
|
||||
const wsRef = useRef<WebSocket | null>(null);
|
||||
const reconnectTimeoutRef = useRef<NodeJS.Timeout>();
|
||||
|
||||
const connect = useCallback(() => {
|
||||
const wsUrl = process.env.NEXT_PUBLIC_WS_URL || 'ws://localhost:3001';
|
||||
const ws = new WebSocket(wsUrl);
|
||||
|
||||
ws.onopen = () => {
|
||||
console.log('WebSocket connected');
|
||||
setState(prev => ({ ...prev, isConnected: true }));
|
||||
|
||||
// Request initial state
|
||||
ws.send(JSON.stringify({ type: 'get_state' }));
|
||||
};
|
||||
|
||||
ws.onmessage = (event) => {
|
||||
const message = JSON.parse(event.data);
|
||||
|
||||
switch (message.type) {
|
||||
case 'initial_state':
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
devices: message.devices,
|
||||
rooms: message.rooms
|
||||
}));
|
||||
break;
|
||||
|
||||
case 'device_update':
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
devices: prev.devices.map(d =>
|
||||
d.id === message.device.id
|
||||
? { ...d, ...message.device, lastUpdated: new Date().toISOString() }
|
||||
: d
|
||||
)
|
||||
}));
|
||||
break;
|
||||
|
||||
case 'device_added':
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
devices: [...prev.devices, message.device]
|
||||
}));
|
||||
break;
|
||||
|
||||
case 'device_removed':
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
devices: prev.devices.filter(d => d.id !== message.deviceId)
|
||||
}));
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
ws.onclose = () => {
|
||||
console.log('WebSocket disconnected');
|
||||
setState(prev => ({ ...prev, isConnected: false }));
|
||||
|
||||
// Reconnect after 5 seconds
|
||||
reconnectTimeoutRef.current = setTimeout(connect, 5000);
|
||||
};
|
||||
|
||||
ws.onerror = (error) => {
|
||||
console.error('WebSocket error:', error);
|
||||
};
|
||||
|
||||
wsRef.current = ws;
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
connect();
|
||||
|
||||
return () => {
|
||||
if (reconnectTimeoutRef.current) {
|
||||
clearTimeout(reconnectTimeoutRef.current);
|
||||
}
|
||||
wsRef.current?.close();
|
||||
};
|
||||
}, [connect]);
|
||||
|
||||
const updateDevice = useCallback(async (
|
||||
deviceId: string,
|
||||
updates: Partial<Device>
|
||||
) => {
|
||||
// Optimistic Update
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
devices: prev.devices.map(d =>
|
||||
d.id === deviceId ? { ...d, ...updates } : d
|
||||
)
|
||||
}));
|
||||
|
||||
// Send to server
|
||||
wsRef.current?.send(JSON.stringify({
|
||||
type: 'update_device',
|
||||
deviceId,
|
||||
updates
|
||||
}));
|
||||
}, []);
|
||||
|
||||
const executeScene = useCallback((sceneId: string) => {
|
||||
wsRef.current?.send(JSON.stringify({
|
||||
type: 'execute_scene',
|
||||
sceneId
|
||||
}));
|
||||
}, []);
|
||||
|
||||
return {
|
||||
...state,
|
||||
updateDevice,
|
||||
executeScene
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Scene Manager
|
||||
|
||||
```typescript
|
||||
// components/dashboard/SceneManager.tsx
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import {
|
||||
Sun,
|
||||
Moon,
|
||||
Film,
|
||||
Coffee,
|
||||
PartyPopper,
|
||||
Power
|
||||
} from 'lucide-react';
|
||||
|
||||
interface Scene {
|
||||
id: string;
|
||||
name: string;
|
||||
icon: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
const defaultScenes: Scene[] = [
|
||||
{ id: 'morning', name: 'Good Morning', icon: 'sun', description: 'Wake up routine' },
|
||||
{ id: 'movie', name: 'Movie Time', icon: 'film', description: 'Dim lights for movies' },
|
||||
{ id: 'goodnight', name: 'Good Night', icon: 'moon', description: 'All lights off' },
|
||||
{ id: 'party', name: 'Party Mode', icon: 'party', description: 'Colorful lights' },
|
||||
{ id: 'away', name: 'Away', icon: 'power', description: 'Energy saving mode' },
|
||||
{ id: 'coffee', name: 'Coffee Break', icon: 'coffee', description: 'Cozy atmosphere' }
|
||||
];
|
||||
|
||||
const iconMap: Record<string, any> = {
|
||||
sun: Sun,
|
||||
moon: Moon,
|
||||
film: Film,
|
||||
coffee: Coffee,
|
||||
party: PartyPopper,
|
||||
power: Power
|
||||
};
|
||||
|
||||
interface SceneManagerProps {
|
||||
onExecute: (sceneId: string) => void;
|
||||
}
|
||||
|
||||
export function SceneManager({ onExecute }: SceneManagerProps) {
|
||||
const [activeScene, setActiveScene] = useState<string | null>(null);
|
||||
const [isExecuting, setIsExecuting] = useState(false);
|
||||
|
||||
const handleExecute = async (sceneId: string) => {
|
||||
setIsExecuting(true);
|
||||
setActiveScene(sceneId);
|
||||
|
||||
await onExecute(sceneId);
|
||||
|
||||
setTimeout(() => {
|
||||
setIsExecuting(false);
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Scenes</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 gap-3">
|
||||
{defaultScenes.map(scene => {
|
||||
const Icon = iconMap[scene.icon] || Sun;
|
||||
const isActive = activeScene === scene.id;
|
||||
|
||||
return (
|
||||
<Button
|
||||
key={scene.id}
|
||||
variant={isActive ? 'default' : 'outline'}
|
||||
className="h-auto py-4 flex flex-col gap-2"
|
||||
onClick={() => handleExecute(scene.id)}
|
||||
disabled={isExecuting}
|
||||
>
|
||||
<Icon className="w-6 h-6" />
|
||||
<span className="text-sm font-medium">{scene.name}</span>
|
||||
</Button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Climate Control
|
||||
|
||||
```typescript
|
||||
// components/dashboard/ClimateControl.tsx
|
||||
'use client';
|
||||
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Thermometer, Droplets, Wind, Plus, Minus } from 'lucide-react';
|
||||
|
||||
interface ClimateControlProps {
|
||||
temperature: number;
|
||||
humidity: number;
|
||||
setpoint: number;
|
||||
mode: 'heat' | 'cool' | 'auto' | 'off';
|
||||
onSetpointChange: (value: number) => void;
|
||||
onModeChange: (mode: string) => void;
|
||||
}
|
||||
|
||||
export function ClimateControl({
|
||||
temperature,
|
||||
humidity,
|
||||
setpoint,
|
||||
mode,
|
||||
onSetpointChange,
|
||||
onModeChange
|
||||
}: ClimateControlProps) {
|
||||
const adjustSetpoint = (delta: number) => {
|
||||
const newValue = Math.max(16, Math.min(28, setpoint + delta));
|
||||
onSetpointChange(newValue);
|
||||
};
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Thermometer className="w-5 h-5" />
|
||||
Climate
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
{/* Current Values */}
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="text-center p-4 bg-muted rounded-lg">
|
||||
<Thermometer className="w-6 h-6 mx-auto mb-2 text-orange-500" />
|
||||
<div className="text-3xl font-bold">{temperature}°C</div>
|
||||
<div className="text-sm text-muted-foreground">Current</div>
|
||||
</div>
|
||||
<div className="text-center p-4 bg-muted rounded-lg">
|
||||
<Droplets className="w-6 h-6 mx-auto mb-2 text-blue-500" />
|
||||
<div className="text-3xl font-bold">{humidity}%</div>
|
||||
<div className="text-sm text-muted-foreground">Humidity</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Setpoint Control */}
|
||||
<div className="flex items-center justify-center gap-6">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="w-12 h-12 rounded-full"
|
||||
onClick={() => adjustSetpoint(-0.5)}
|
||||
>
|
||||
<Minus className="w-6 h-6" />
|
||||
</Button>
|
||||
|
||||
<div className="text-center">
|
||||
<div className="text-5xl font-bold">{setpoint}°C</div>
|
||||
<div className="text-sm text-muted-foreground">Target</div>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="w-12 h-12 rounded-full"
|
||||
onClick={() => adjustSetpoint(0.5)}
|
||||
>
|
||||
<Plus className="w-6 h-6" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Mode Selection */}
|
||||
<div className="flex gap-2">
|
||||
{['heat', 'cool', 'auto', 'off'].map(m => (
|
||||
<Button
|
||||
key={m}
|
||||
variant={mode === m ? 'default' : 'outline'}
|
||||
className="flex-1"
|
||||
onClick={() => onModeChange(m)}
|
||||
>
|
||||
{m.charAt(0).toUpperCase() + m.slice(1)}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Energy Monitor
|
||||
|
||||
```typescript
|
||||
// components/dashboard/EnergyMonitor.tsx
|
||||
'use client';
|
||||
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Progress } from '@/components/ui/progress';
|
||||
import { Zap, TrendingDown, TrendingUp } from 'lucide-react';
|
||||
|
||||
interface EnergyData {
|
||||
currentPower: number; // Watts
|
||||
todayUsage: number; // kWh
|
||||
monthlyUsage: number; // kWh
|
||||
monthlyBudget: number; // kWh
|
||||
trend: 'up' | 'down' | 'stable';
|
||||
trendPercent: number;
|
||||
}
|
||||
|
||||
interface EnergyMonitorProps {
|
||||
data: EnergyData;
|
||||
}
|
||||
|
||||
export function EnergyMonitor({ data }: EnergyMonitorProps) {
|
||||
const budgetUsage = (data.monthlyUsage / data.monthlyBudget) * 100;
|
||||
const TrendIcon = data.trend === 'up' ? TrendingUp : TrendingDown;
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Zap className="w-5 h-5 text-yellow-500" />
|
||||
Energy
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
{/* Current Power */}
|
||||
<div className="text-center">
|
||||
<div className="text-4xl font-bold">
|
||||
{data.currentPower.toLocaleString()} W
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground">Current Power</div>
|
||||
</div>
|
||||
|
||||
{/* Usage Stats */}
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="text-center p-3 bg-muted rounded-lg">
|
||||
<div className="text-xl font-semibold">
|
||||
{data.todayUsage.toFixed(1)} kWh
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground">Today</div>
|
||||
</div>
|
||||
<div className="text-center p-3 bg-muted rounded-lg">
|
||||
<div className="text-xl font-semibold">
|
||||
{data.monthlyUsage.toFixed(0)} kWh
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground">This Month</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Budget Progress */}
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between text-sm">
|
||||
<span>Monthly Budget</span>
|
||||
<span>{budgetUsage.toFixed(0)}%</span>
|
||||
</div>
|
||||
<Progress value={budgetUsage} className="h-2" />
|
||||
<div className="text-xs text-muted-foreground text-right">
|
||||
{data.monthlyUsage.toFixed(0)} / {data.monthlyBudget} kWh
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Trend */}
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
<TrendIcon className={`w-5 h-5 ${
|
||||
data.trend === 'up' ? 'text-red-500' : 'text-green-500'
|
||||
}`} />
|
||||
<span className={`font-medium ${
|
||||
data.trend === 'up' ? 'text-red-500' : 'text-green-500'
|
||||
}`}>
|
||||
{data.trend === 'up' ? '+' : '-'}{data.trendPercent}%
|
||||
</span>
|
||||
<span className="text-sm text-muted-foreground">vs last week</span>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Main Dashboard Layout
|
||||
|
||||
```typescript
|
||||
// app/dashboard/page.tsx
|
||||
'use client';
|
||||
|
||||
import { RoomView } from '@/components/dashboard/RoomView';
|
||||
import { SceneManager } from '@/components/dashboard/SceneManager';
|
||||
import { ClimateControl } from '@/components/dashboard/ClimateControl';
|
||||
import { EnergyMonitor } from '@/components/dashboard/EnergyMonitor';
|
||||
import { useSmartHome } from '@/hooks/useSmartHome';
|
||||
|
||||
export default function DashboardPage() {
|
||||
const { devices, executeScene, updateDevice } = useSmartHome();
|
||||
|
||||
// Aggregate climate data
|
||||
const climateDevice = devices.find(d => d.type === 'thermostat');
|
||||
|
||||
// Mock energy data (in production: from API)
|
||||
const energyData = {
|
||||
currentPower: 1234,
|
||||
todayUsage: 12.5,
|
||||
monthlyUsage: 245,
|
||||
monthlyBudget: 300,
|
||||
trend: 'down' as const,
|
||||
trendPercent: 8
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background p-6">
|
||||
<header className="mb-8">
|
||||
<h1 className="text-3xl font-bold">Smart Home</h1>
|
||||
<p className="text-muted-foreground">Welcome back!</p>
|
||||
</header>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
{/* Main Area - Devices */}
|
||||
<div className="lg:col-span-2 space-y-6">
|
||||
<RoomView />
|
||||
</div>
|
||||
|
||||
{/* Sidebar */}
|
||||
<div className="space-y-6">
|
||||
<SceneManager onExecute={executeScene} />
|
||||
|
||||
{climateDevice && (
|
||||
<ClimateControl
|
||||
temperature={climateDevice.temperature || 21}
|
||||
humidity={50}
|
||||
setpoint={climateDevice.setpoint || 21}
|
||||
mode="auto"
|
||||
onSetpointChange={(value) =>
|
||||
updateDevice(climateDevice.id, { setpoint: value })
|
||||
}
|
||||
onModeChange={(mode) =>
|
||||
console.log('Mode:', mode)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
<EnergyMonitor data={energyData} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Fazit
|
||||
|
||||
Smart Home Dashboard mit React bietet:
|
||||
|
||||
1. **Real-time Updates**: WebSocket für Live-Daten
|
||||
2. **Intuitive UI**: Room-basierte Navigation
|
||||
3. **Responsive Design**: Desktop & Mobile
|
||||
4. **Szenen & Automation**: One-Click Control
|
||||
|
||||
Das perfekte Interface für Smart Home Control.
|
||||
|
||||
---
|
||||
|
||||
## Bildprompts
|
||||
|
||||
1. "Smart home dashboard on tablet, dark mode with glowing device cards"
|
||||
2. "Room view with connected devices, modern UI design"
|
||||
3. "Energy monitoring graph with real-time power consumption"
|
||||
|
||||
---
|
||||
|
||||
## Quellen
|
||||
|
||||
- [React Documentation](https://react.dev/)
|
||||
- [shadcn/ui Components](https://ui.shadcn.com/)
|
||||
- [WebSocket API](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket)
|
||||
- [Home Assistant Dashboard](https://www.home-assistant.io/dashboards/)
|
||||
Reference in New Issue
Block a user