Update App.js, new modular structure
This commit is contained in:
70
src/components/DeviationList.js
Normal file
70
src/components/DeviationList.js
Normal file
@@ -0,0 +1,70 @@
|
||||
import React from 'react';
|
||||
import TimeInput from './TimeInput.js';
|
||||
import NumericInput from './NumericInput.js';
|
||||
|
||||
const DeviationList = ({
|
||||
deviations,
|
||||
doseIncrement,
|
||||
simulationDays,
|
||||
onAddDeviation,
|
||||
onRemoveDeviation,
|
||||
onDeviationChange
|
||||
}) => {
|
||||
return (
|
||||
<div className="bg-amber-50 p-5 rounded-lg shadow-sm border border-amber-200">
|
||||
<h2 className="text-xl font-semibold mb-4 text-gray-700">Abweichungen vom Plan</h2>
|
||||
{deviations.map((dev, index) => (
|
||||
<div key={index} className="flex items-center space-x-2 mb-2 p-2 bg-white rounded flex-wrap">
|
||||
<select
|
||||
value={dev.dayOffset || 0}
|
||||
onChange={e => onDeviationChange(index, 'dayOffset', parseInt(e.target.value, 10))}
|
||||
className="p-2 border rounded-md text-sm"
|
||||
>
|
||||
{[...Array(parseInt(simulationDays, 10) || 1).keys()].map(day => (
|
||||
<option key={day} value={day}>Tag {day + 1}</option>
|
||||
))}
|
||||
</select>
|
||||
<TimeInput
|
||||
value={dev.time}
|
||||
onChange={newTime => onDeviationChange(index, 'time', newTime)}
|
||||
/>
|
||||
<div className="w-32">
|
||||
<NumericInput
|
||||
value={dev.dose}
|
||||
onChange={newDose => onDeviationChange(index, 'dose', newDose)}
|
||||
increment={doseIncrement}
|
||||
min={0}
|
||||
unit="mg"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => onRemoveDeviation(index)}
|
||||
className="text-red-500 hover:text-red-700 font-bold text-lg"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
<div className="flex items-center mt-1" title="Mark this if it was an extra dose instead of a replacement for a planned one.">
|
||||
<input
|
||||
type="checkbox"
|
||||
id={`add_dose_${index}`}
|
||||
checked={dev.isAdditional}
|
||||
onChange={e => onDeviationChange(index, 'isAdditional', e.target.checked)}
|
||||
className="h-4 w-4 rounded border-gray-300 text-sky-600 focus:ring-sky-500"
|
||||
/>
|
||||
<label htmlFor={`add_dose_${index}`} className="ml-2 text-xs text-gray-600">
|
||||
Zusätzlich
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<button
|
||||
onClick={onAddDeviation}
|
||||
className="mt-2 w-full bg-amber-500 text-white py-2 rounded-md hover:bg-amber-600 text-sm"
|
||||
>
|
||||
Abweichung hinzufügen
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DeviationList;
|
||||
31
src/components/DoseSchedule.js
Normal file
31
src/components/DoseSchedule.js
Normal file
@@ -0,0 +1,31 @@
|
||||
import React from 'react';
|
||||
import TimeInput from './TimeInput.js';
|
||||
import NumericInput from './NumericInput.js';
|
||||
|
||||
const DoseSchedule = ({ doses, doseIncrement, onUpdateDoses }) => {
|
||||
return (
|
||||
<div className="bg-white p-5 rounded-lg shadow-sm border">
|
||||
<h2 className="text-xl font-semibold mb-4 text-gray-700">Mein Plan</h2>
|
||||
{doses.map((dose, index) => (
|
||||
<div key={index} className="flex items-center space-x-3 mb-3">
|
||||
<TimeInput
|
||||
value={dose.time}
|
||||
onChange={newTime => onUpdateDoses(doses.map((d, i) => i === index ? {...d, time: newTime} : d))}
|
||||
/>
|
||||
<div className="w-40">
|
||||
<NumericInput
|
||||
value={dose.dose}
|
||||
onChange={newDose => onUpdateDoses(doses.map((d, i) => i === index ? {...d, dose: newDose} : d))}
|
||||
increment={doseIncrement}
|
||||
min={0}
|
||||
unit="mg"
|
||||
/>
|
||||
</div>
|
||||
<span className="text-gray-600 text-sm flex-1">{dose.label}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DoseSchedule;
|
||||
55
src/components/NumericInput.js
Normal file
55
src/components/NumericInput.js
Normal file
@@ -0,0 +1,55 @@
|
||||
import React from 'react';
|
||||
|
||||
const NumericInput = ({ value, onChange, increment, min = -Infinity, max = Infinity, placeholder, unit }) => {
|
||||
const updateValue = (direction) => {
|
||||
const numIncrement = parseFloat(increment) || 1;
|
||||
let numValue = parseFloat(value) || 0;
|
||||
numValue += direction * numIncrement;
|
||||
numValue = Math.max(min, numValue);
|
||||
numValue = Math.min(max, numValue);
|
||||
const finalValue = String(Math.round(numValue * 100) / 100);
|
||||
onChange(finalValue);
|
||||
};
|
||||
|
||||
const handleKeyDown = (e) => {
|
||||
if (e.key === 'ArrowUp' || e.key === 'ArrowDown') {
|
||||
e.preventDefault();
|
||||
updateValue(e.key === 'ArrowUp' ? 1 : -1);
|
||||
}
|
||||
};
|
||||
|
||||
const handleChange = (e) => {
|
||||
const val = e.target.value;
|
||||
if (val === '' || /^-?\d*\.?\d*$/.test(val)) {
|
||||
onChange(val);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex items-center w-full">
|
||||
<button
|
||||
onClick={() => updateValue(-1)}
|
||||
className="px-2 py-1 border rounded-l-md bg-gray-100 hover:bg-gray-200 text-lg font-bold"
|
||||
>
|
||||
-
|
||||
</button>
|
||||
<input
|
||||
type="text"
|
||||
value={value}
|
||||
onChange={handleChange}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder={placeholder}
|
||||
className="p-2 border-t border-b w-full text-sm text-center"
|
||||
/>
|
||||
<button
|
||||
onClick={() => updateValue(1)}
|
||||
className="px-2 py-1 border rounded-r-md bg-gray-100 hover:bg-gray-200 text-lg font-bold"
|
||||
>
|
||||
+
|
||||
</button>
|
||||
{unit && <span className="ml-2 text-gray-500 text-sm whitespace-nowrap">{unit}</span>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default NumericInput;
|
||||
153
src/components/Settings.js
Normal file
153
src/components/Settings.js
Normal file
@@ -0,0 +1,153 @@
|
||||
import React from 'react';
|
||||
import NumericInput from './NumericInput.js';
|
||||
|
||||
const Settings = ({
|
||||
pkParams,
|
||||
therapeuticRange,
|
||||
uiSettings,
|
||||
onUpdatePkParams,
|
||||
onUpdateTherapeuticRange,
|
||||
onUpdateUiSetting,
|
||||
onReset
|
||||
}) => {
|
||||
const { showDayTimeXAxis, yAxisMin, yAxisMax, simulationDays, displayedDays } = uiSettings;
|
||||
|
||||
return (
|
||||
<div className="bg-white p-5 rounded-lg shadow-sm border">
|
||||
<h2 className="text-xl font-semibold mb-4 text-gray-700">Erweiterte Einstellungen</h2>
|
||||
<div className="space-y-4 text-sm">
|
||||
<div className="flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="showDayTimeXAxis"
|
||||
checked={showDayTimeXAxis}
|
||||
onChange={e => onUpdateUiSetting('showDayTimeXAxis', e.target.checked)}
|
||||
className="h-4 w-4 rounded border-gray-300 text-sky-600 focus:ring-sky-500"
|
||||
/>
|
||||
<label htmlFor="showDayTimeXAxis" className="ml-3 block font-medium text-gray-600">
|
||||
24h-Zeitachse anzeigen
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<label className="block font-medium text-gray-600 pt-2">Simulationsdauer</label>
|
||||
<div className="w-40">
|
||||
<NumericInput
|
||||
value={simulationDays}
|
||||
onChange={val => onUpdateUiSetting('simulationDays', val)}
|
||||
increment={'1'}
|
||||
min={2}
|
||||
max={7}
|
||||
unit="Tage"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<label className="block font-medium text-gray-600">Angezeigte Tage</label>
|
||||
<div className="w-40">
|
||||
<NumericInput
|
||||
value={displayedDays}
|
||||
onChange={val => onUpdateUiSetting('displayedDays', val)}
|
||||
increment={'1'}
|
||||
min={1}
|
||||
max={parseInt(simulationDays, 10) || 1}
|
||||
unit="Tage"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<label className="block font-medium text-gray-600 pt-2">Y-Achsen-Bereich</label>
|
||||
<div className="flex items-center space-x-2 mt-1">
|
||||
<div className="w-32">
|
||||
<NumericInput
|
||||
value={yAxisMin}
|
||||
onChange={val => onUpdateUiSetting('yAxisMin', val)}
|
||||
increment={'5'}
|
||||
min={0}
|
||||
placeholder="Auto"
|
||||
unit="ng/ml"
|
||||
/>
|
||||
</div>
|
||||
<span className="text-gray-500">-</span>
|
||||
<div className="w-32">
|
||||
<NumericInput
|
||||
value={yAxisMax}
|
||||
onChange={val => onUpdateUiSetting('yAxisMax', val)}
|
||||
increment={'5'}
|
||||
min={0}
|
||||
placeholder="Auto"
|
||||
unit="ng/ml"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<label className="block font-medium text-gray-600">Therapeutischer Bereich</label>
|
||||
<div className="flex items-center space-x-2 mt-1">
|
||||
<div className="w-32">
|
||||
<NumericInput
|
||||
value={therapeuticRange.min}
|
||||
onChange={val => onUpdateTherapeuticRange('min', val)}
|
||||
increment={'0.5'}
|
||||
min={0}
|
||||
placeholder="Min"
|
||||
unit="ng/ml"
|
||||
/>
|
||||
</div>
|
||||
<span className="text-gray-500">-</span>
|
||||
<div className="w-32">
|
||||
<NumericInput
|
||||
value={therapeuticRange.max}
|
||||
onChange={val => onUpdateTherapeuticRange('max', val)}
|
||||
increment={'0.5'}
|
||||
min={0}
|
||||
placeholder="Max"
|
||||
unit="ng/ml"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 className="text-lg font-semibold mt-4 pt-4 border-t">d-Amphetamin Parameter</h3>
|
||||
<div className="w-40">
|
||||
<label className="block font-medium text-gray-600">Halbwertszeit</label>
|
||||
<NumericInput
|
||||
value={pkParams.damph.halfLife}
|
||||
onChange={val => onUpdatePkParams('damph', { halfLife: val })}
|
||||
increment={'0.5'}
|
||||
min={0.1}
|
||||
unit="h"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<h3 className="text-lg font-semibold mt-4 pt-4 border-t">Lisdexamfetamin Parameter</h3>
|
||||
<div className="w-40">
|
||||
<label className="block font-medium text-gray-600">Umwandlungs-Halbwertszeit</label>
|
||||
<NumericInput
|
||||
value={pkParams.ldx.halfLife}
|
||||
onChange={val => onUpdatePkParams('ldx', { halfLife: val })}
|
||||
increment={'0.1'}
|
||||
min={0.1}
|
||||
unit="h"
|
||||
/>
|
||||
</div>
|
||||
<div className="w-40">
|
||||
<label className="block font-medium text-gray-600">Absorptionsrate</label>
|
||||
<NumericInput
|
||||
value={pkParams.ldx.absorptionRate}
|
||||
onChange={val => onUpdatePkParams('ldx', { absorptionRate: val })}
|
||||
increment={'0.1'}
|
||||
min={0.1}
|
||||
unit="(schneller >)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="pt-4">
|
||||
<button
|
||||
onClick={onReset}
|
||||
className="w-full bg-red-600 text-white py-2 rounded-md hover:bg-red-700 text-sm"
|
||||
>
|
||||
Alle Einstellungen zurücksetzen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Settings;
|
||||
181
src/components/SimulationChart.js
Normal file
181
src/components/SimulationChart.js
Normal file
@@ -0,0 +1,181 @@
|
||||
import React from 'react';
|
||||
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ReferenceLine, ResponsiveContainer } from 'recharts';
|
||||
|
||||
const SimulationChart = ({
|
||||
idealProfile,
|
||||
deviatedProfile,
|
||||
correctedProfile,
|
||||
chartView,
|
||||
showDayTimeXAxis,
|
||||
therapeuticRange,
|
||||
simulationDays,
|
||||
displayedDays,
|
||||
yAxisMin,
|
||||
yAxisMax
|
||||
}) => {
|
||||
const totalHours = (parseInt(simulationDays, 10) || 3) * 24;
|
||||
const chartTicks = Array.from({length: Math.floor(totalHours / 6) + 1}, (_, i) => i * 6);
|
||||
const chartWidthPercentage = Math.max(100, (totalHours / ( (parseInt(displayedDays, 10) || 2) * 24)) * 100);
|
||||
|
||||
const chartDomain = React.useMemo(() => {
|
||||
const numMin = parseFloat(yAxisMin);
|
||||
const numMax = parseFloat(yAxisMax);
|
||||
const domainMin = !isNaN(numMin) ? numMin : 'auto';
|
||||
const domainMax = !isNaN(numMax) ? numMax : 'auto';
|
||||
return [domainMin, domainMax];
|
||||
}, [yAxisMin, yAxisMax]);
|
||||
|
||||
return (
|
||||
<div className="flex-grow w-full overflow-x-auto">
|
||||
<div style={{ width: `${chartWidthPercentage}%`, height: '100%', minWidth: '100%' }}>
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<LineChart margin={{ top: 20, right: 20, left: 0, bottom: 5 }}>
|
||||
<CartesianGrid strokeDasharray="3 3" />
|
||||
<XAxis
|
||||
dataKey="timeHours"
|
||||
type="number"
|
||||
domain={[0, totalHours]}
|
||||
ticks={chartTicks}
|
||||
tickFormatter={(h) => `${h}h`}
|
||||
xAxisId="continuous"
|
||||
/>
|
||||
{showDayTimeXAxis && (
|
||||
<XAxis
|
||||
dataKey="timeHours"
|
||||
type="number"
|
||||
domain={[0, totalHours]}
|
||||
ticks={chartTicks}
|
||||
tickFormatter={(h) => `${h % 24}h`}
|
||||
xAxisId="daytime"
|
||||
orientation="top"
|
||||
/>
|
||||
)}
|
||||
<YAxis
|
||||
label={{ value: 'Konzentration (ng/ml)', angle: -90, position: 'insideLeft', offset: -10 }}
|
||||
domain={chartDomain}
|
||||
allowDecimals={false}
|
||||
/>
|
||||
<Tooltip
|
||||
formatter={(value, name) => [`${value.toFixed(1)} ng/ml`, name]}
|
||||
labelFormatter={(label) => `Stunde: ${label}h`}
|
||||
/>
|
||||
<Legend verticalAlign="top" height={36} />
|
||||
|
||||
{(chartView === 'damph' || chartView === 'both') && (
|
||||
<ReferenceLine
|
||||
y={parseFloat(therapeuticRange.min) || 0}
|
||||
label={{ value: 'Min', position: 'insideTopLeft' }}
|
||||
stroke="green"
|
||||
strokeDasharray="3 3"
|
||||
xAxisId="continuous"
|
||||
/>
|
||||
)}
|
||||
{(chartView === 'damph' || chartView === 'both') && (
|
||||
<ReferenceLine
|
||||
y={parseFloat(therapeuticRange.max) || 0}
|
||||
label={{ value: 'Max', position: 'insideTopLeft' }}
|
||||
stroke="red"
|
||||
strokeDasharray="3 3"
|
||||
xAxisId="continuous"
|
||||
/>
|
||||
)}
|
||||
|
||||
{[...Array(parseInt(simulationDays, 10) || 0).keys()].map(day => (
|
||||
day > 0 && (
|
||||
<ReferenceLine
|
||||
key={day}
|
||||
x={day * 24}
|
||||
stroke="#999"
|
||||
strokeDasharray="5 5"
|
||||
xAxisId="continuous"
|
||||
/>
|
||||
)
|
||||
))}
|
||||
|
||||
{(chartView === 'damph' || chartView === 'both') && (
|
||||
<Line
|
||||
type="monotone"
|
||||
data={idealProfile}
|
||||
dataKey="damph"
|
||||
name="d-Amphetamin (Ideal)"
|
||||
stroke="#3b82f6"
|
||||
strokeWidth={2.5}
|
||||
dot={false}
|
||||
xAxisId="continuous"
|
||||
/>
|
||||
)}
|
||||
{(chartView === 'ldx' || chartView === 'both') && (
|
||||
<Line
|
||||
type="monotone"
|
||||
data={idealProfile}
|
||||
dataKey="ldx"
|
||||
name="Lisdexamfetamin (Ideal)"
|
||||
stroke="#8b5cf6"
|
||||
strokeWidth={2}
|
||||
dot={false}
|
||||
strokeDasharray="3 3"
|
||||
xAxisId="continuous"
|
||||
/>
|
||||
)}
|
||||
|
||||
{deviatedProfile && (chartView === 'damph' || chartView === 'both') && (
|
||||
<Line
|
||||
type="monotone"
|
||||
data={deviatedProfile}
|
||||
dataKey="damph"
|
||||
name="d-Amphetamin (Abweichung)"
|
||||
stroke="#f59e0b"
|
||||
strokeWidth={2}
|
||||
strokeDasharray="5 5"
|
||||
dot={false}
|
||||
xAxisId="continuous"
|
||||
/>
|
||||
)}
|
||||
{deviatedProfile && (chartView === 'ldx' || chartView === 'both') && (
|
||||
<Line
|
||||
type="monotone"
|
||||
data={deviatedProfile}
|
||||
dataKey="ldx"
|
||||
name="Lisdexamfetamin (Abweichung)"
|
||||
stroke="#f97316"
|
||||
strokeWidth={1.5}
|
||||
strokeDasharray="5 5"
|
||||
dot={false}
|
||||
xAxisId="continuous"
|
||||
/>
|
||||
)}
|
||||
|
||||
{correctedProfile && (chartView === 'damph' || chartView === 'both') && (
|
||||
<Line
|
||||
type="monotone"
|
||||
data={correctedProfile}
|
||||
dataKey="damph"
|
||||
name="d-Amphetamin (Korrektur)"
|
||||
stroke="#10b981"
|
||||
strokeWidth={2.5}
|
||||
strokeDasharray="3 7"
|
||||
dot={false}
|
||||
xAxisId="continuous"
|
||||
/>
|
||||
)}
|
||||
{correctedProfile && (chartView === 'ldx' || chartView === 'both') && (
|
||||
<Line
|
||||
type="monotone"
|
||||
data={correctedProfile}
|
||||
dataKey="ldx"
|
||||
name="Lisdexamfetamin (Korrektur)"
|
||||
stroke="#059669"
|
||||
strokeWidth={2}
|
||||
strokeDasharray="3 7"
|
||||
dot={false}
|
||||
xAxisId="continuous"
|
||||
/>
|
||||
)}
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SimulationChart;
|
||||
28
src/components/SuggestionPanel.js
Normal file
28
src/components/SuggestionPanel.js
Normal file
@@ -0,0 +1,28 @@
|
||||
import React from 'react';
|
||||
|
||||
const SuggestionPanel = ({ suggestion, onApplySuggestion }) => {
|
||||
if (!suggestion) return null;
|
||||
|
||||
return (
|
||||
<div className="bg-sky-100 border-l-4 border-sky-500 p-4 rounded-r-lg shadow-md">
|
||||
<h3 className="font-bold text-lg mb-2">Was wäre wenn?</h3>
|
||||
{suggestion.dose ? (
|
||||
<>
|
||||
<p className="text-sm text-sky-800 mb-3">
|
||||
Vorschlag: <span className="font-bold">{suggestion.dose}mg</span> (statt {suggestion.originalDose}mg) um <span className="font-bold">{suggestion.time}</span>.
|
||||
</p>
|
||||
<button
|
||||
onClick={onApplySuggestion}
|
||||
className="w-full bg-sky-600 text-white py-2 rounded-md hover:bg-sky-700 text-sm"
|
||||
>
|
||||
Vorschlag als Abweichung übernehmen
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<p className="text-sm text-sky-800">{suggestion.text}</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SuggestionPanel;
|
||||
109
src/components/TimeInput.js
Normal file
109
src/components/TimeInput.js
Normal file
@@ -0,0 +1,109 @@
|
||||
import React from 'react';
|
||||
|
||||
const TimeInput = ({ value, onChange }) => {
|
||||
const [displayValue, setDisplayValue] = React.useState(value);
|
||||
const [isPickerOpen, setIsPickerOpen] = React.useState(false);
|
||||
const [pickerHours, pickerMinutes] = (value || "00:00").split(':').map(Number);
|
||||
|
||||
React.useEffect(() => {
|
||||
setDisplayValue(value);
|
||||
}, [value]);
|
||||
|
||||
const handleBlur = (e) => {
|
||||
let input = e.target.value.replace(/[^0-9]/g, '');
|
||||
let hours = '00', minutes = '00';
|
||||
if (input.length <= 2) {
|
||||
hours = input.padStart(2, '0');
|
||||
}
|
||||
else if (input.length === 3) {
|
||||
hours = input.substring(0, 1).padStart(2, '0');
|
||||
minutes = input.substring(1, 3);
|
||||
}
|
||||
else {
|
||||
hours = input.substring(0, 2);
|
||||
minutes = input.substring(2, 4);
|
||||
}
|
||||
hours = Math.min(23, parseInt(hours, 10) || 0).toString().padStart(2, '0');
|
||||
minutes = Math.min(59, parseInt(minutes, 10) || 0).toString().padStart(2, '0');
|
||||
const formattedTime = `${hours}:${minutes}`;
|
||||
setDisplayValue(formattedTime);
|
||||
onChange(formattedTime);
|
||||
};
|
||||
|
||||
const handleChange = (e) => {
|
||||
setDisplayValue(e.target.value);
|
||||
};
|
||||
|
||||
const handlePickerChange = (part, val) => {
|
||||
let newHours = pickerHours, newMinutes = pickerMinutes;
|
||||
if (part === 'h') {
|
||||
newHours = val;
|
||||
} else {
|
||||
newMinutes = val;
|
||||
}
|
||||
const formattedTime = `${String(newHours).padStart(2, '0')}:${String(newMinutes).padStart(2, '0')}`;
|
||||
onChange(formattedTime);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="relative flex items-center">
|
||||
<input
|
||||
type="text"
|
||||
value={displayValue}
|
||||
onChange={handleChange}
|
||||
onBlur={handleBlur}
|
||||
placeholder="HH:MM"
|
||||
className="p-2 border rounded-md w-24 text-sm text-center"
|
||||
/>
|
||||
<button
|
||||
onClick={() => setIsPickerOpen(!isPickerOpen)}
|
||||
className="ml-2 p-2 text-gray-500 hover:text-gray-700"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm1-12a1 1 0 10-2 0v4a1 1 0 00.293.707l2.828 2.829a1 1 0 101.414-1.415L11 9.586V6z" clipRule="evenodd" />
|
||||
</svg>
|
||||
</button>
|
||||
{isPickerOpen && (
|
||||
<div className="absolute top-full mt-2 z-10 bg-white p-4 rounded-lg shadow-xl border w-64">
|
||||
<div className="text-center text-lg font-bold mb-3">{value}</div>
|
||||
<div>
|
||||
<div className="mb-2"><span className="font-semibold">Stunde:</span></div>
|
||||
<div className="grid grid-cols-6 gap-1">
|
||||
{[...Array(24).keys()].map(h => (
|
||||
<button
|
||||
key={h}
|
||||
onClick={() => handlePickerChange('h', h)}
|
||||
className={`p-1 rounded text-xs ${h === pickerHours ? 'bg-sky-500 text-white' : 'bg-gray-200 hover:bg-sky-200'}`}
|
||||
>
|
||||
{String(h).padStart(2,'0')}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4">
|
||||
<div className="mb-2"><span className="font-semibold">Minute:</span></div>
|
||||
<div className="grid grid-cols-4 gap-1">
|
||||
{[0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55].map(m => (
|
||||
<button
|
||||
key={m}
|
||||
onClick={() => handlePickerChange('m', m)}
|
||||
className={`p-1 rounded text-xs ${m === pickerMinutes ? 'bg-sky-500 text-white' : 'bg-gray-200 hover:bg-sky-200'}`}
|
||||
>
|
||||
{String(m).padStart(2,'0')}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setIsPickerOpen(false)}
|
||||
className="mt-4 w-full bg-gray-600 text-white py-1 rounded-md text-sm"
|
||||
>
|
||||
Schließen
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TimeInput;
|
||||
Reference in New Issue
Block a user