Compare commits
2 Commits
b9a2489225
...
3e3ca3621c
| Author | SHA1 | Date | |
|---|---|---|---|
| 3e3ca3621c | |||
| b67bfa7687 |
39
src/App.tsx
39
src/App.tsx
@@ -17,6 +17,7 @@ import DaySchedule from './components/day-schedule';
|
|||||||
import SimulationChart from './components/simulation-chart';
|
import SimulationChart from './components/simulation-chart';
|
||||||
import Settings from './components/settings';
|
import Settings from './components/settings';
|
||||||
import LanguageSelector from './components/language-selector';
|
import LanguageSelector from './components/language-selector';
|
||||||
|
import ThemeSelector from './components/theme-selector';
|
||||||
import DisclaimerModal from './components/disclaimer-modal';
|
import DisclaimerModal from './components/disclaimer-modal';
|
||||||
import DataManagementModal from './components/data-management-modal';
|
import DataManagementModal from './components/data-management-modal';
|
||||||
import { Button } from './components/ui/button';
|
import { Button } from './components/ui/button';
|
||||||
@@ -91,6 +92,33 @@ const MedPlanAssistant = () => {
|
|||||||
uiSettings
|
uiSettings
|
||||||
} = appState;
|
} = appState;
|
||||||
|
|
||||||
|
// Apply theme based on user preference or system setting
|
||||||
|
React.useEffect(() => {
|
||||||
|
const theme = uiSettings.theme || 'system';
|
||||||
|
const root = document.documentElement;
|
||||||
|
|
||||||
|
const applyTheme = (isDark: boolean) => {
|
||||||
|
if (isDark) {
|
||||||
|
root.classList.add('dark');
|
||||||
|
} else {
|
||||||
|
root.classList.remove('dark');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (theme === 'system') {
|
||||||
|
// Detect system preference
|
||||||
|
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
||||||
|
applyTheme(mediaQuery.matches);
|
||||||
|
|
||||||
|
// Listen for system theme changes
|
||||||
|
const listener = (e: MediaQueryListEvent) => applyTheme(e.matches);
|
||||||
|
mediaQuery.addEventListener('change', listener);
|
||||||
|
return () => mediaQuery.removeEventListener('change', listener);
|
||||||
|
} else {
|
||||||
|
applyTheme(theme === 'dark');
|
||||||
|
}
|
||||||
|
}, [uiSettings.theme]);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
showDayTimeOnXAxis,
|
showDayTimeOnXAxis,
|
||||||
chartView,
|
chartView,
|
||||||
@@ -137,11 +165,18 @@ const MedPlanAssistant = () => {
|
|||||||
|
|
||||||
<div className="max-w-7xl mx-auto">
|
<div className="max-w-7xl mx-auto">
|
||||||
<header className="mb-8">
|
<header className="mb-8">
|
||||||
<div className="flex justify-between items-start">
|
<div className="flex justify-between items-start gap-4">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-3xl md:text-4xl font-bold tracking-tight">{t('appTitle')}</h1>
|
<h1 className="text-3xl md:text-4xl font-bold tracking-tight">{t('appTitle')}</h1>
|
||||||
</div>
|
</div>
|
||||||
<LanguageSelector currentLanguage={currentLanguage} onLanguageChange={changeLanguage} t={t} />
|
<div className="flex flex-wrap gap-2 justify-end">
|
||||||
|
<ThemeSelector
|
||||||
|
currentTheme={uiSettings.theme || 'system'}
|
||||||
|
onThemeChange={(theme: 'light' | 'dark' | 'system') => updateUiSetting('theme', theme)}
|
||||||
|
t={t}
|
||||||
|
/>
|
||||||
|
<LanguageSelector currentLanguage={currentLanguage} onLanguageChange={changeLanguage} t={t} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-muted-foreground mt-1">{t('appSubtitle')}</p>
|
<p className="text-muted-foreground mt-1">{t('appSubtitle')}</p>
|
||||||
</header>
|
</header>
|
||||||
|
|||||||
@@ -112,6 +112,7 @@ const DataManagementModal: React.FC<DataManagementModalProps> = ({
|
|||||||
const [jsonValidationMessage, setJsonValidationMessage] = useState<{
|
const [jsonValidationMessage, setJsonValidationMessage] = useState<{
|
||||||
type: 'success' | 'error' | null;
|
type: 'success' | 'error' | null;
|
||||||
message: string;
|
message: string;
|
||||||
|
warnings?: string[];
|
||||||
}>({ type: null, message: '' });
|
}>({ type: null, message: '' });
|
||||||
|
|
||||||
// Clipboard feedback
|
// Clipboard feedback
|
||||||
@@ -119,30 +120,31 @@ const DataManagementModal: React.FC<DataManagementModalProps> = ({
|
|||||||
|
|
||||||
// Reset editor when modal opens/closes
|
// Reset editor when modal opens/closes
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (isOpen) {
|
// TODO nice to have: use can decide behavior via checkbox (near editor)
|
||||||
// Load current app data into editor when opening
|
// if (isOpen) {
|
||||||
const appState = {
|
// // Load current app data into editor when opening
|
||||||
pkParams,
|
// const appState = {
|
||||||
days,
|
// pkParams,
|
||||||
therapeuticRange,
|
// days,
|
||||||
doseIncrement,
|
// therapeuticRange,
|
||||||
uiSettings,
|
// doseIncrement,
|
||||||
steadyStateConfig: { daysOnMedication: pkParams.advanced.steadyStateDays },
|
// uiSettings,
|
||||||
};
|
// steadyStateConfig: { daysOnMedication: pkParams.advanced.steadyStateDays },
|
||||||
const exportData = exportSettings(appState, exportOptions, APP_VERSION);
|
// };
|
||||||
const jsonString = JSON.stringify(exportData, null, 2);
|
// const exportData = exportSettings(appState, exportOptions, APP_VERSION);
|
||||||
setJsonEditorContent(jsonString);
|
// const jsonString = JSON.stringify(exportData, null, 2);
|
||||||
setJsonEditorExpanded(true);
|
// setJsonEditorContent(jsonString);
|
||||||
validateJsonContent(jsonString);
|
// setJsonEditorExpanded(true);
|
||||||
} else {
|
// validateJsonContent(jsonString);
|
||||||
// Clear editor when closing
|
// } else {
|
||||||
setJsonEditorContent('');
|
|
||||||
setJsonEditorExpanded(false);
|
// Clear/collapse editor and clear upload file ref when opening/closing
|
||||||
setJsonValidationMessage({ type: null, message: '' });
|
setJsonEditorContent('');
|
||||||
setSelectedFile(null);
|
setJsonEditorExpanded(false);
|
||||||
if (fileInputRef.current) {
|
setJsonValidationMessage({ type: null, message: '' });
|
||||||
|
setSelectedFile(null);
|
||||||
|
if (fileInputRef.current) {
|
||||||
fileInputRef.current.value = '';
|
fileInputRef.current.value = '';
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}, [isOpen]);
|
}, [isOpen]);
|
||||||
|
|
||||||
@@ -300,9 +302,11 @@ const DataManagementModal: React.FC<DataManagementModalProps> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (validation.warnings.length > 0) {
|
if (validation.warnings.length > 0) {
|
||||||
|
// Show success with warnings - warnings will be displayed separately
|
||||||
setJsonValidationMessage({
|
setJsonValidationMessage({
|
||||||
type: 'success',
|
type: 'success',
|
||||||
message: t('jsonValidationSuccess') + ' ⚠️ ' + validation.warnings.length + ' warnings',
|
message: t('jsonValidationSuccess'),
|
||||||
|
warnings: validation.warnings,
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -734,19 +738,33 @@ const DataManagementModal: React.FC<DataManagementModalProps> = ({
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
{jsonValidationMessage.type && (
|
{jsonValidationMessage.type && (
|
||||||
<div
|
<div className="space-y-2">
|
||||||
className={`flex items-center gap-2 text-sm ${
|
<div
|
||||||
jsonValidationMessage.type === 'success'
|
className={`flex items-center gap-2 text-sm ${
|
||||||
? 'text-green-600 dark:text-green-400'
|
jsonValidationMessage.type === 'success'
|
||||||
: 'text-red-600 dark:text-red-400'
|
? 'text-green-600 dark:text-green-400'
|
||||||
}`}
|
: 'text-red-600 dark:text-red-400'
|
||||||
>
|
}`}
|
||||||
{jsonValidationMessage.type === 'success' ? (
|
>
|
||||||
<Check className="h-4 w-4" />
|
{jsonValidationMessage.type === 'success' ? (
|
||||||
) : (
|
<Check className="h-4 w-4 flex-shrink-0" />
|
||||||
<X className="h-4 w-4" />
|
) : (
|
||||||
|
<X className="h-4 w-4 flex-shrink-0" />
|
||||||
|
)}
|
||||||
|
<span>{jsonValidationMessage.message}</span>
|
||||||
|
</div>
|
||||||
|
{jsonValidationMessage.warnings && jsonValidationMessage.warnings.length > 0 && (
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
{jsonValidationMessage.warnings.map((warning, index) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className="bg-yellow-500 text-white text-xs p-2 rounded-md"
|
||||||
|
>
|
||||||
|
{warning}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
<span>{jsonValidationMessage.message}</span>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -10,22 +10,18 @@
|
|||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from './ui/select';
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from './ui/select';
|
||||||
import { Label } from './ui/label';
|
|
||||||
|
|
||||||
const LanguageSelector = ({ currentLanguage, onLanguageChange, t }: any) => {
|
const LanguageSelector = ({ currentLanguage, onLanguageChange, t }: any) => {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-wrap-reverse items-center gap-2">
|
<Select value={currentLanguage} onValueChange={onLanguageChange}>
|
||||||
<Label className="text-sm font-medium">{t('languageSelectorLabel')}</Label>
|
<SelectTrigger className="w-32">
|
||||||
<Select value={currentLanguage} onValueChange={onLanguageChange}>
|
<SelectValue />
|
||||||
<SelectTrigger className="w-32">
|
</SelectTrigger>
|
||||||
<SelectValue />
|
<SelectContent>
|
||||||
</SelectTrigger>
|
<SelectItem value="en">{t('languageSelectorEN')}</SelectItem>
|
||||||
<SelectContent>
|
<SelectItem value="de">{t('languageSelectorDE')}</SelectItem>
|
||||||
<SelectItem value="en">{t('languageSelectorEN')}</SelectItem>
|
</SelectContent>
|
||||||
<SelectItem value="de">{t('languageSelectorDE')}</SelectItem>
|
</Select>
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
29
src/components/theme-selector.tsx
Normal file
29
src/components/theme-selector.tsx
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
/**
|
||||||
|
* Theme Selector Component
|
||||||
|
*
|
||||||
|
* Provides UI for switching between light/dark/system theme modes.
|
||||||
|
* Uses shadcn/ui Select component.
|
||||||
|
*
|
||||||
|
* @author Andreas Weyer
|
||||||
|
* @license MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from './ui/select';
|
||||||
|
|
||||||
|
const ThemeSelector = ({ currentTheme, onThemeChange, t }: any) => {
|
||||||
|
return (
|
||||||
|
<Select value={currentTheme} onValueChange={onThemeChange}>
|
||||||
|
<SelectTrigger className="w-36">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="light">{t('themeSelectorLight')}</SelectItem>
|
||||||
|
<SelectItem value="dark">{t('themeSelectorDark')}</SelectItem>
|
||||||
|
<SelectItem value="system">{t('themeSelectorSystem')}</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ThemeSelector;
|
||||||
@@ -99,6 +99,7 @@ export interface UiSettings {
|
|||||||
showTherapeuticRange?: boolean;
|
showTherapeuticRange?: boolean;
|
||||||
steadyStateDaysEnabled?: boolean;
|
steadyStateDaysEnabled?: boolean;
|
||||||
stickyChart: boolean;
|
stickyChart: boolean;
|
||||||
|
theme?: 'light' | 'dark' | 'system';
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AppState {
|
export interface AppState {
|
||||||
@@ -171,5 +172,6 @@ export const getDefaultState = (): AppState => ({
|
|||||||
showTherapeuticRange: false,
|
showTherapeuticRange: false,
|
||||||
steadyStateDaysEnabled: true,
|
steadyStateDaysEnabled: true,
|
||||||
stickyChart: false,
|
stickyChart: false,
|
||||||
|
theme: 'system',
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -17,6 +17,11 @@ export const de = {
|
|||||||
languageSelectorEN: "English",
|
languageSelectorEN: "English",
|
||||||
languageSelectorDE: "Deutsch",
|
languageSelectorDE: "Deutsch",
|
||||||
|
|
||||||
|
// Theme selector
|
||||||
|
themeSelectorLight: "☀️ Hell",
|
||||||
|
themeSelectorDark: "🌙 Dunkel",
|
||||||
|
themeSelectorSystem: "💻 System",
|
||||||
|
|
||||||
// Dose Schedule
|
// Dose Schedule
|
||||||
myPlan: "Mein Plan",
|
myPlan: "Mein Plan",
|
||||||
morning: "Morgens",
|
morning: "Morgens",
|
||||||
|
|||||||
@@ -17,6 +17,11 @@ export const en = {
|
|||||||
languageSelectorEN: "English",
|
languageSelectorEN: "English",
|
||||||
languageSelectorDE: "Deutsch",
|
languageSelectorDE: "Deutsch",
|
||||||
|
|
||||||
|
// Theme selector
|
||||||
|
themeSelectorLight: "☀️ Light",
|
||||||
|
themeSelectorDark: "🌙 Dark",
|
||||||
|
themeSelectorSystem: "💻 System",
|
||||||
|
|
||||||
// Dose Schedule
|
// Dose Schedule
|
||||||
myPlan: "My Plan",
|
myPlan: "My Plan",
|
||||||
morning: "Morning",
|
morning: "Morning",
|
||||||
|
|||||||
@@ -31,6 +31,33 @@
|
|||||||
--radius: 0.625rem;
|
--radius: 0.625rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
--background: 0 0% 10%;
|
||||||
|
--foreground: 0 0% 95%;
|
||||||
|
--card: 0 0% 14%;
|
||||||
|
--card-foreground: 0 0% 95%;
|
||||||
|
--popover: 0 0% 12%;
|
||||||
|
--popover-foreground: 0 0% 95%;
|
||||||
|
--primary: 217 91% 60%;
|
||||||
|
--primary-foreground: 0 0% 100%;
|
||||||
|
--secondary: 220 15% 20%;
|
||||||
|
--secondary-foreground: 0 0% 90%;
|
||||||
|
--muted: 220 10% 18%;
|
||||||
|
--muted-foreground: 0 0% 60%;
|
||||||
|
--accent: 220 10% 18%;
|
||||||
|
--accent-foreground: 0 0% 90%;
|
||||||
|
--destructive: 0 84% 60%;
|
||||||
|
--destructive-foreground: 0 0% 98%;
|
||||||
|
--border: 0 0% 25%;
|
||||||
|
--input: 0 0% 25%;
|
||||||
|
--ring: 0 0% 40%;
|
||||||
|
--chart-1: 12 76% 61%;
|
||||||
|
--chart-2: 173 58% 39%;
|
||||||
|
--chart-3: 197 37% 24%;
|
||||||
|
--chart-4: 43 74% 66%;
|
||||||
|
--chart-5: 27 87% 67%;
|
||||||
|
}
|
||||||
|
|
||||||
* {
|
* {
|
||||||
border-color: hsl(var(--border));
|
border-color: hsl(var(--border));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -219,7 +219,7 @@ export const validateImportData = (data: any): ImportValidationResult => {
|
|||||||
|
|
||||||
// Validate advanced settings
|
// Validate advanced settings
|
||||||
if (importData.advancedSettings !== undefined) {
|
if (importData.advancedSettings !== undefined) {
|
||||||
const validCategories = ['standardVd', 'weightBasedVd', 'foodEffect', 'urinePh', 'fOral', 'steadyStateDays'];
|
const validCategories = ['standardVd', 'weightBasedVd', 'foodEffect', 'urinePh', 'fOral', 'steadyStateDays', 'ageGroup', 'renalFunction'];
|
||||||
const importedCategories = Object.keys(importData.advancedSettings);
|
const importedCategories = Object.keys(importData.advancedSettings);
|
||||||
const unknownCategories = importedCategories.filter(c => !validCategories.includes(c));
|
const unknownCategories = importedCategories.filter(c => !validCategories.includes(c));
|
||||||
if (unknownCategories.length > 0) {
|
if (unknownCategories.length > 0) {
|
||||||
|
|||||||
Reference in New Issue
Block a user