Compare commits

...

2 Commits

9 changed files with 169 additions and 52 deletions

View File

@@ -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,12 +165,19 @@ 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>
<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} /> <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>

View File

@@ -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,23 +120,25 @@ 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 {
// Clear/collapse editor and clear upload file ref when opening/closing
setJsonEditorContent(''); setJsonEditorContent('');
setJsonEditorExpanded(false); setJsonEditorExpanded(false);
setJsonValidationMessage({ type: null, message: '' }); setJsonValidationMessage({ type: null, message: '' });
@@ -143,7 +146,6 @@ const DataManagementModal: React.FC<DataManagementModalProps> = ({
if (fileInputRef.current) { if (fileInputRef.current) {
fileInputRef.current.value = ''; fileInputRef.current.value = '';
} }
}
}, [isOpen]); }, [isOpen]);
if (!isOpen) return null; if (!isOpen) return null;
@@ -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,6 +738,7 @@ const DataManagementModal: React.FC<DataManagementModalProps> = ({
/> />
{jsonValidationMessage.type && ( {jsonValidationMessage.type && (
<div className="space-y-2">
<div <div
className={`flex items-center gap-2 text-sm ${ className={`flex items-center gap-2 text-sm ${
jsonValidationMessage.type === 'success' jsonValidationMessage.type === 'success'
@@ -742,12 +747,25 @@ const DataManagementModal: React.FC<DataManagementModalProps> = ({
}`} }`}
> >
{jsonValidationMessage.type === 'success' ? ( {jsonValidationMessage.type === 'success' ? (
<Check className="h-4 w-4" /> <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> <span>{jsonValidationMessage.message}</span>
</div> </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>
)}
</div>
)} )}
<div className="flex gap-2"> <div className="flex gap-2">

View File

@@ -10,12 +10,9 @@
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">
<Label className="text-sm font-medium">{t('languageSelectorLabel')}</Label>
<Select value={currentLanguage} onValueChange={onLanguageChange}> <Select value={currentLanguage} onValueChange={onLanguageChange}>
<SelectTrigger className="w-32"> <SelectTrigger className="w-32">
<SelectValue /> <SelectValue />
@@ -25,7 +22,6 @@ const LanguageSelector = ({ currentLanguage, onLanguageChange, t }: any) => {
<SelectItem value="de">{t('languageSelectorDE')}</SelectItem> <SelectItem value="de">{t('languageSelectorDE')}</SelectItem>
</SelectContent> </SelectContent>
</Select> </Select>
</div>
); );
}; };

View 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;

View File

@@ -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',
} }
}); });

View File

@@ -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",

View File

@@ -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",

View File

@@ -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));
} }

View File

@@ -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) {