mirror of
https://github.com/ferdzo/iotDashboard.git
synced 2026-04-05 09:06:26 +00:00
Added Calendar viewer and Environment overview with AI, Health overviewer
This commit is contained in:
@@ -18,8 +18,12 @@ export default function AddWidgetModal({ isOpen, onClose, onAdd }: AddWidgetModa
|
||||
const [selectedMetrics, setSelectedMetrics] = useState<string[]>([])
|
||||
const [timeframeHours, setTimeframeHours] = useState(24)
|
||||
const [widgetWidth, setWidgetWidth] = useState(1)
|
||||
const [widgetHeight, setWidgetHeight] = useState(2)
|
||||
const [widgetHeight, setWidgetHeight] = useState(3)
|
||||
const [city, setCity] = useState('Skopje')
|
||||
const [calendarUrl, setCalendarUrl] = useState('')
|
||||
const [showCalendarUrl, setShowCalendarUrl] = useState(false)
|
||||
const [calendarRangeHours, setCalendarRangeHours] = useState(72)
|
||||
const [briefingType, setBriefingType] = useState<'schedule' | 'environment' | 'full'>('full')
|
||||
|
||||
// Fetch devices
|
||||
const { data: devicesData } = useQuery({
|
||||
@@ -44,6 +48,10 @@ export default function AddWidgetModal({ isOpen, onClose, onAdd }: AddWidgetModa
|
||||
const devices = devicesData?.results || []
|
||||
const availableMetrics = deviceMetricsData || []
|
||||
|
||||
// Validation helpers
|
||||
const needsDevice = !['weather', 'air-quality', 'calendar', 'daily-briefing'].includes(widgetType)
|
||||
const needsMetrics = !['weather', 'air-quality', 'run-suitability', 'health-stats', 'calendar', 'daily-briefing'].includes(widgetType)
|
||||
|
||||
// Reset form when modal opens
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
@@ -54,7 +62,11 @@ export default function AddWidgetModal({ isOpen, onClose, onAdd }: AddWidgetModa
|
||||
setSelectedMetrics([])
|
||||
setTimeframeHours(24)
|
||||
setWidgetWidth(1)
|
||||
setWidgetHeight(2)
|
||||
setWidgetHeight(3)
|
||||
setCalendarUrl('')
|
||||
setShowCalendarUrl(false)
|
||||
setCalendarRangeHours(72)
|
||||
setBriefingType('full')
|
||||
}
|
||||
}, [isOpen])
|
||||
|
||||
@@ -64,12 +76,31 @@ export default function AddWidgetModal({ isOpen, onClose, onAdd }: AddWidgetModa
|
||||
}, [selectedDevices])
|
||||
|
||||
const handleSubmit = () => {
|
||||
// Weather and air-quality widgets don't need device/metric validation
|
||||
if (widgetType !== 'weather' && widgetType !== 'air-quality') {
|
||||
if (selectedDevices.length === 0 || selectedMetrics.length === 0) {
|
||||
alert('Please select at least one device and one metric')
|
||||
return
|
||||
}
|
||||
// Validation
|
||||
if (needsDevice && selectedDevices.length === 0) {
|
||||
alert('Please select a device')
|
||||
return
|
||||
}
|
||||
|
||||
if (needsMetrics && selectedMetrics.length === 0) {
|
||||
alert('Please select at least one metric')
|
||||
return
|
||||
}
|
||||
|
||||
const wellnessWidgets = ['run-suitability', 'health-stats']
|
||||
if (wellnessWidgets.includes(widgetType) && !city) {
|
||||
alert('Please enter a city for wellness widgets')
|
||||
return
|
||||
}
|
||||
|
||||
if (widgetType === 'calendar' && !calendarUrl.trim()) {
|
||||
alert('Please provide an iCal URL')
|
||||
return
|
||||
}
|
||||
|
||||
if (widgetType === 'daily-briefing' && !city) {
|
||||
alert('Please enter a city for daily briefing')
|
||||
return
|
||||
}
|
||||
|
||||
// Create title
|
||||
@@ -78,6 +109,16 @@ export default function AddWidgetModal({ isOpen, onClose, onAdd }: AddWidgetModa
|
||||
defaultTitle = `Weather - ${city}`
|
||||
} else if (widgetType === 'air-quality') {
|
||||
defaultTitle = `Air Quality - ${city}`
|
||||
} else if (widgetType === 'run-suitability') {
|
||||
defaultTitle = `Run Suitability - ${city}`
|
||||
} else if (widgetType === 'health-stats') {
|
||||
const selectedDevice = devices.find(d => d.id === selectedDevices[0])
|
||||
defaultTitle = `Health StaTts - ${selectedDevice?.name || 'Device'}`
|
||||
} else if (widgetType === 'calendar') {
|
||||
defaultTitle = 'Calendar Agenda'
|
||||
} else if (widgetType === 'daily-briefing') {
|
||||
const briefingLabels = { schedule: 'Schedule', environment: 'Environment', full: 'Full' }
|
||||
defaultTitle = `${briefingLabels[briefingType]} Briefing`
|
||||
} else {
|
||||
const selectedDevice = devices.find(d => d.id === selectedDevices[0])
|
||||
defaultTitle = createDefaultWidgetTitle(widgetType, selectedDevice?.name, selectedMetrics)
|
||||
@@ -87,8 +128,8 @@ export default function AddWidgetModal({ isOpen, onClose, onAdd }: AddWidgetModa
|
||||
id: `widget-${Date.now()}`,
|
||||
type: widgetType,
|
||||
title: title || defaultTitle,
|
||||
deviceIds: widgetType === 'weather' || widgetType === 'air-quality' ? [] : selectedDevices,
|
||||
metricIds: widgetType === 'weather' || widgetType === 'air-quality' ? [] : selectedMetrics,
|
||||
deviceIds: needsDevice ? selectedDevices : [],
|
||||
metricIds: needsMetrics ? selectedMetrics : [],
|
||||
timeframe: {
|
||||
hours: timeframeHours,
|
||||
},
|
||||
@@ -96,13 +137,24 @@ export default function AddWidgetModal({ isOpen, onClose, onAdd }: AddWidgetModa
|
||||
showLegend: true,
|
||||
showGrid: true,
|
||||
height: widgetType === 'line-chart' ? 300 : undefined,
|
||||
city: widgetType === 'weather' || widgetType === 'air-quality' ? city : undefined,
|
||||
city: (widgetType === 'weather' || widgetType === 'air-quality' || wellnessWidgets.includes(widgetType) || widgetType === 'daily-briefing') ? city : undefined,
|
||||
},
|
||||
calendar: widgetType === 'calendar' ? {
|
||||
icalUrl: calendarUrl.trim(),
|
||||
timeRangeHours: calendarRangeHours,
|
||||
} : undefined,
|
||||
briefing: widgetType === 'daily-briefing' ? {
|
||||
briefingType,
|
||||
city,
|
||||
healthDeviceId: selectedDevices[0],
|
||||
calendarUrl: calendarUrl.trim() || undefined,
|
||||
calendarRangeHours: calendarRangeHours,
|
||||
} : undefined,
|
||||
position: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
w: widgetWidth,
|
||||
h: widgetHeight,
|
||||
h: ['run-suitability', 'health-stats', 'daily-briefing'].includes(widgetType) ? 2 : widgetHeight,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -249,6 +301,58 @@ export default function AddWidgetModal({ isOpen, onClose, onAdd }: AddWidgetModa
|
||||
<div className="text-xs opacity-70">Multi-factor</div>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
className={`btn ${widgetType === 'run-suitability' ? 'btn-primary' : 'btn-outline'} justify-start`}
|
||||
onClick={() => setWidgetType('run-suitability')}
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" />
|
||||
</svg>
|
||||
<div className="text-left">
|
||||
<div className="font-semibold">Run Suitability</div>
|
||||
<div className="text-xs opacity-70">Go / No Go</div>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
className={`btn ${widgetType === 'health-stats' ? 'btn-primary' : 'btn-outline'} justify-start`}
|
||||
onClick={() => setWidgetType('health-stats')}
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z" />
|
||||
</svg>
|
||||
<div className="text-left">
|
||||
<div className="font-semibold">Health Stats</div>
|
||||
<div className="text-xs opacity-70">Steps & HR</div>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
className={`btn ${widgetType === 'calendar' ? 'btn-primary' : 'btn-outline'} justify-start`}
|
||||
onClick={() => setWidgetType('calendar')}
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 7V3m8 4V3m-9 8h10m-12 8h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
|
||||
</svg>
|
||||
<div className="text-left">
|
||||
<div className="font-semibold">Calendar</div>
|
||||
<div className="text-xs opacity-70">Agenda (iCal)</div>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
className={`btn ${widgetType === 'daily-briefing' ? 'btn-primary' : 'btn-outline'} justify-start`}
|
||||
onClick={() => setWidgetType('daily-briefing')}
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z" />
|
||||
</svg>
|
||||
<div className="text-left">
|
||||
<div className="font-semibold">Daily Briefing</div>
|
||||
<div className="text-xs opacity-70">AI Summary</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -260,7 +364,7 @@ export default function AddWidgetModal({ isOpen, onClose, onAdd }: AddWidgetModa
|
||||
className="btn btn-primary"
|
||||
onClick={() => {
|
||||
// Skip data source step for weather and air quality widgets
|
||||
if (widgetType === 'weather' || widgetType === 'air-quality') {
|
||||
if (widgetType === 'weather' || widgetType === 'air-quality' || widgetType === 'calendar' || widgetType === 'daily-briefing') {
|
||||
setStep(3)
|
||||
} else {
|
||||
setStep(2)
|
||||
@@ -274,8 +378,29 @@ export default function AddWidgetModal({ isOpen, onClose, onAdd }: AddWidgetModa
|
||||
)}
|
||||
|
||||
{/* Step 2: Data Source */}
|
||||
{step === 2 && widgetType !== 'weather' && widgetType !== 'air-quality' && (
|
||||
{step === 2 && widgetType !== 'weather' && widgetType !== 'air-quality' && widgetType !== 'calendar' && widgetType !== 'daily-briefing' && (
|
||||
<div className="space-y-4">
|
||||
{/* City input for wellness widgets */}
|
||||
{['run-suitability', 'health-stats'].includes(widgetType) && (
|
||||
<div className="form-control">
|
||||
<label className="label">
|
||||
<span className="label-text font-semibold">City (for weather/air quality)</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
className="input input-bordered"
|
||||
placeholder="Enter city name (e.g., Skopje)"
|
||||
value={city}
|
||||
onChange={(e) => setCity(e.target.value)}
|
||||
/>
|
||||
<label className="label">
|
||||
<span className="label-text-alt">
|
||||
Used for weather and air quality context
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="form-control">
|
||||
<label className="label">
|
||||
<span className="label-text font-semibold">Select Device</span>
|
||||
@@ -305,48 +430,53 @@ export default function AddWidgetModal({ isOpen, onClose, onAdd }: AddWidgetModa
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form-control">
|
||||
<label className="label">
|
||||
<span className="label-text font-semibold">Select Metric(s)</span>
|
||||
<span className="label-text-alt">{selectedMetrics.length} selected</span>
|
||||
</label>
|
||||
{selectedDevices.length === 0 ? (
|
||||
<div className="border rounded-lg p-4 text-center text-base-content/60">
|
||||
Please select a device first
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{(['stat', 'gauge'].includes(widgetType)) && (
|
||||
<div className="alert alert-info mb-2">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" className="stroke-current shrink-0 w-5 h-5">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||
</svg>
|
||||
<span className="text-sm">This widget type supports only one metric</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="border rounded-lg p-2 max-h-48 overflow-y-auto space-y-1">
|
||||
{availableMetrics.length === 0 ? (
|
||||
<div className="text-center text-base-content/60 py-4">
|
||||
No metrics found for this device
|
||||
</div>
|
||||
) : (
|
||||
availableMetrics.map((metric) => (
|
||||
<label key={metric} className="flex items-center gap-2 p-2 hover:bg-base-200 rounded cursor-pointer">
|
||||
<input
|
||||
type={(['stat', 'gauge'].includes(widgetType)) ? 'radio' : 'checkbox'}
|
||||
name={(['stat', 'gauge'].includes(widgetType)) ? 'single-metric' : undefined}
|
||||
className={(['stat', 'gauge'].includes(widgetType)) ? 'radio radio-sm' : 'checkbox checkbox-sm'}
|
||||
checked={selectedMetrics.includes(metric)}
|
||||
onChange={() => toggleMetric(metric)}
|
||||
/>
|
||||
<span className="capitalize">{metric.replace(/_/g, ' ')}</span>
|
||||
</label>
|
||||
))
|
||||
)}
|
||||
|
||||
|
||||
{/* Metric Selection - Skip for widgets with hardcoded metrics */}
|
||||
{!['run-suitability', 'health-stats'].includes(widgetType) && (
|
||||
<div className="form-control">
|
||||
<label className="label">
|
||||
<span className="label-text font-semibold">Select Metric(s)</span>
|
||||
<span className="label-text-alt">{selectedMetrics.length} selected</span>
|
||||
</label>
|
||||
{selectedDevices.length === 0 ? (
|
||||
<div className="border rounded-lg p-4 text-center text-base-content/60">
|
||||
Please select a device first
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{(['stat', 'gauge'].includes(widgetType)) && (
|
||||
<div className="alert alert-info mb-2">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" className="stroke-current shrink-0 w-5 h-5">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||
</svg>
|
||||
<span className="text-sm">This widget type supports only one metric</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="border rounded-lg p-2 max-h-48 overflow-y-auto space-y-1">
|
||||
{availableMetrics.length === 0 ? (
|
||||
<div className="text-center text-base-content/60 py-4">
|
||||
No metrics found for this device
|
||||
</div>
|
||||
) : (
|
||||
availableMetrics.map((metric) => (
|
||||
<label key={metric} className="flex items-center gap-2 p-2 hover:bg-base-200 rounded cursor-pointer">
|
||||
<input
|
||||
type={(['stat', 'gauge'].includes(widgetType)) ? 'radio' : 'checkbox'}
|
||||
name={(['stat', 'gauge'].includes(widgetType)) ? 'single-metric' : undefined}
|
||||
className={(['stat', 'gauge'].includes(widgetType)) ? 'radio radio-sm' : 'checkbox checkbox-sm'}
|
||||
checked={selectedMetrics.includes(metric)}
|
||||
onChange={() => toggleMetric(metric)}
|
||||
/>
|
||||
<span className="capitalize">{metric.replace(/_/g, ' ')}</span>
|
||||
</label>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="modal-action">
|
||||
<button className="btn btn-ghost" onClick={() => setStep(1)}>
|
||||
@@ -354,8 +484,12 @@ export default function AddWidgetModal({ isOpen, onClose, onAdd }: AddWidgetModa
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
|
||||
onClick={() => setStep(3)}
|
||||
disabled={selectedDevices.length === 0 || selectedMetrics.length === 0}
|
||||
disabled={
|
||||
(needsDevice && selectedDevices.length === 0) ||
|
||||
(needsMetrics && selectedMetrics.length === 0)
|
||||
}
|
||||
>
|
||||
Next
|
||||
</button>
|
||||
@@ -402,6 +536,194 @@ export default function AddWidgetModal({ isOpen, onClose, onAdd }: AddWidgetModa
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
) : widgetType === 'calendar' ? (
|
||||
<>
|
||||
<div className="form-control">
|
||||
<label className="label">
|
||||
<span className="label-text font-semibold">iCal URL</span>
|
||||
</label>
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
type={showCalendarUrl ? 'text' : 'password'}
|
||||
className="input input-bordered flex-1"
|
||||
placeholder="https://calendar.google.com/calendar/ical/..."
|
||||
value={calendarUrl}
|
||||
onChange={(e) => setCalendarUrl(e.target.value)}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-outline"
|
||||
onClick={() => setShowCalendarUrl((prev) => !prev)}
|
||||
>
|
||||
{showCalendarUrl ? 'Hide' : 'Show'}
|
||||
</button>
|
||||
</div>
|
||||
<label className="label">
|
||||
<span className="label-text-alt">Link is saved with the widget configuration</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="form-control">
|
||||
<label className="label">
|
||||
<span className="label-text font-semibold">Agenda Range</span>
|
||||
</label>
|
||||
<select
|
||||
className="select select-bordered"
|
||||
value={calendarRangeHours}
|
||||
onChange={(e) => setCalendarRangeHours(Number(e.target.value))}
|
||||
>
|
||||
<option value={24}>Next 24 hours</option>
|
||||
<option value={72}>Next 3 days</option>
|
||||
<option value={168}>Next 7 days</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="form-control">
|
||||
<label className="label">
|
||||
<span className="label-text font-semibold">Widget Title (Optional)</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
className="input input-bordered"
|
||||
placeholder="Calendar Agenda"
|
||||
value={title}
|
||||
onChange={(e) => setTitle(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
) : widgetType === 'daily-briefing' ? (
|
||||
<>
|
||||
<div className="form-control">
|
||||
<label className="label">
|
||||
<span className="label-text font-semibold">Briefing Type</span>
|
||||
</label>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
type="button"
|
||||
className={`btn flex-1 ${briefingType === 'schedule' ? 'btn-primary' : 'btn-outline'}`}
|
||||
onClick={() => setBriefingType('schedule')}
|
||||
>
|
||||
📅 Schedule
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`btn flex-1 ${briefingType === 'environment' ? 'btn-primary' : 'btn-outline'}`}
|
||||
onClick={() => setBriefingType('environment')}
|
||||
>
|
||||
🌡️ Environment
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`btn flex-1 ${briefingType === 'full' ? 'btn-primary' : 'btn-outline'}`}
|
||||
onClick={() => setBriefingType('full')}
|
||||
>
|
||||
✨ Full
|
||||
</button>
|
||||
</div>
|
||||
<label className="label">
|
||||
<span className="label-text-alt">
|
||||
{briefingType === 'schedule' && 'Focus on your calendar and daily activity'}
|
||||
{briefingType === 'environment' && 'Focus on indoor/outdoor conditions for productivity'}
|
||||
{briefingType === 'full' && 'Complete briefing with all insights combined'}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="form-control">
|
||||
<label className="label">
|
||||
<span className="label-text font-semibold">City (for weather/air quality)</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
className="input input-bordered"
|
||||
placeholder="Enter city name (e.g., Skopje)"
|
||||
value={city}
|
||||
onChange={(e) => setCity(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{(briefingType === 'schedule' || briefingType === 'full') && (
|
||||
<>
|
||||
<div className="form-control">
|
||||
<label className="label">
|
||||
<span className="label-text font-semibold">Calendar URL (Optional)</span>
|
||||
</label>
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
type={showCalendarUrl ? 'text' : 'password'}
|
||||
className="input input-bordered flex-1"
|
||||
placeholder="https://calendar.google.com/calendar/ical/..."
|
||||
value={calendarUrl}
|
||||
onChange={(e) => setCalendarUrl(e.target.value)}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-outline"
|
||||
onClick={() => setShowCalendarUrl((prev) => !prev)}
|
||||
>
|
||||
{showCalendarUrl ? 'Hide' : 'Show'}
|
||||
</button>
|
||||
</div>
|
||||
<label className="label">
|
||||
<span className="label-text-alt">Optional: Include calendar events in your briefing</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{calendarUrl && (
|
||||
<div className="form-control">
|
||||
<label className="label">
|
||||
<span className="label-text font-semibold">Calendar Range</span>
|
||||
</label>
|
||||
<select
|
||||
className="select select-bordered"
|
||||
value={calendarRangeHours}
|
||||
onChange={(e) => setCalendarRangeHours(Number(e.target.value))}
|
||||
>
|
||||
<option value={24}>Next 24 hours</option>
|
||||
<option value={72}>Next 3 days</option>
|
||||
<option value={168}>Next 7 days</option>
|
||||
</select>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{devices.length > 0 && (
|
||||
<div className="form-control">
|
||||
<label className="label">
|
||||
<span className="label-text font-semibold">Health Device (Optional)</span>
|
||||
</label>
|
||||
<select
|
||||
className="select select-bordered"
|
||||
value={selectedDevices[0] || ''}
|
||||
onChange={(e) => setSelectedDevices(e.target.value ? [e.target.value] : [])}
|
||||
>
|
||||
<option value="">No health device</option>
|
||||
{devices.map((device) => (
|
||||
<option key={device.id} value={device.id}>
|
||||
{device.name} ({device.location || 'No location'})
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<label className="label">
|
||||
<span className="label-text-alt">Optional: Include step count and activity data</span>
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className="form-control">
|
||||
<label className="label">
|
||||
<span className="label-text font-semibold">Widget Title (Optional)</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
className="input input-bordered"
|
||||
placeholder={`${briefingType.charAt(0).toUpperCase() + briefingType.slice(1)} Briefing`}
|
||||
value={title}
|
||||
onChange={(e) => setTitle(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
// Original configuration for sensor-based widgets
|
||||
<>
|
||||
@@ -478,14 +800,23 @@ export default function AddWidgetModal({ isOpen, onClose, onAdd }: AddWidgetModa
|
||||
<div className="text-sm">
|
||||
<div className="font-semibold">Widget Summary</div>
|
||||
<div>Type: <span className="badge badge-sm">{widgetType}</span></div>
|
||||
<div>Device: {devices.find(d => d.id === selectedDevices[0])?.name}</div>
|
||||
<div>Metrics: {selectedMetrics.join(', ')}</div>
|
||||
<div>Device: {needsDevice ? (devices.find(d => d.id === selectedDevices[0])?.name || 'Not selected') : 'Not required'}</div>
|
||||
<div>Metrics: {needsMetrics ? (selectedMetrics.join(', ') || 'Not selected') : 'Not required'}</div>
|
||||
<div>Size: {widgetWidth} × {widgetHeight}</div>
|
||||
{widgetType === 'calendar' && (
|
||||
<div>Range: Next {calendarRangeHours}h</div>
|
||||
)}
|
||||
{widgetType === 'daily-briefing' && (
|
||||
<div>Briefing: {briefingType.charAt(0).toUpperCase() + briefingType.slice(1)}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="modal-action">
|
||||
<button className="btn btn-ghost" onClick={() => setStep(2)}>
|
||||
<button
|
||||
className="btn btn-ghost"
|
||||
onClick={() => setStep((widgetType === 'weather' || widgetType === 'air-quality' || widgetType === 'calendar' || widgetType === 'daily-briefing') ? 1 : 2)}
|
||||
>
|
||||
Back
|
||||
</button>
|
||||
<button className="btn btn-primary" onClick={handleSubmit}>
|
||||
|
||||
Reference in New Issue
Block a user