Initial commit: Complete NodeJS-native setup
- Migrated from Python pre-commit to NodeJS-native solution - Reorganized documentation structure - Set up Husky + lint-staged for efficient pre-commit hooks - Fixed Dockerfile healthcheck issue - Added comprehensive documentation index
This commit is contained in:
112
components/ui/BarChart.tsx
Normal file
112
components/ui/BarChart.tsx
Normal file
@@ -0,0 +1,112 @@
|
||||
import React from 'react';
|
||||
import { DailyStat } from '../../types';
|
||||
|
||||
interface BarChartProps {
|
||||
data: DailyStat[];
|
||||
}
|
||||
|
||||
const BarChart: React.FC<BarChartProps> = ({ data }) => {
|
||||
const chartHeight = 150;
|
||||
const barWidth = 30;
|
||||
const barMargin = 15;
|
||||
const chartWidth = data.length * (barWidth + barMargin);
|
||||
|
||||
const getDayLabel = (dateString: string) => {
|
||||
const date = new Date(dateString);
|
||||
const userTimezoneOffset = date.getTimezoneOffset() * 60000;
|
||||
const adjustedDate = new Date(date.getTime() + userTimezoneOffset);
|
||||
return adjustedDate.toLocaleDateString('en-US', { weekday: 'short' });
|
||||
};
|
||||
|
||||
return (
|
||||
<div className='w-full overflow-x-auto pb-4'>
|
||||
<svg
|
||||
viewBox={`0 0 ${chartWidth} ${chartHeight + 40}`}
|
||||
width='100%'
|
||||
height='190'
|
||||
aria-labelledby='chart-title'
|
||||
role='img'
|
||||
>
|
||||
<title id='chart-title'>Weekly Medication Adherence Chart</title>
|
||||
|
||||
{/* Y-Axis Labels */}
|
||||
<g className='text-xs fill-current text-slate-500 dark:text-slate-400'>
|
||||
<text x='-5' y='15' textAnchor='end'>
|
||||
100%
|
||||
</text>
|
||||
<text x='-5' y={chartHeight / 2 + 5} textAnchor='end'>
|
||||
50%
|
||||
</text>
|
||||
<text x='-5' y={chartHeight + 5} textAnchor='end'>
|
||||
0%
|
||||
</text>
|
||||
</g>
|
||||
|
||||
{/* Y-Axis Grid Lines */}
|
||||
<line
|
||||
x1='0'
|
||||
y1='10'
|
||||
x2={chartWidth}
|
||||
y2='10'
|
||||
className='stroke-current text-slate-200 dark:text-slate-600'
|
||||
strokeDasharray='2,2'
|
||||
/>
|
||||
<line
|
||||
x1='0'
|
||||
y1={chartHeight / 2 + 2.5}
|
||||
x2={chartWidth}
|
||||
y2={chartHeight / 2 + 2.5}
|
||||
className='stroke-current text-slate-200 dark:text-slate-600'
|
||||
strokeDasharray='2,2'
|
||||
/>
|
||||
<line
|
||||
x1='0'
|
||||
y1={chartHeight}
|
||||
x2={chartWidth}
|
||||
y2={chartHeight}
|
||||
className='stroke-current text-slate-300 dark:text-slate-500'
|
||||
/>
|
||||
|
||||
{data.map((item, index) => {
|
||||
const x = index * (barWidth + barMargin);
|
||||
const barHeight = (item.adherence / 100) * (chartHeight - 10);
|
||||
const y = chartHeight - barHeight;
|
||||
|
||||
const barColorClass =
|
||||
item.adherence >= 90
|
||||
? 'fill-current text-green-500 dark:text-green-400'
|
||||
: item.adherence >= 70
|
||||
? 'fill-current text-amber-500 dark:text-amber-400'
|
||||
: 'fill-current text-red-500 dark:text-red-400';
|
||||
|
||||
return (
|
||||
<g key={item.date}>
|
||||
<rect
|
||||
x={x}
|
||||
y={y}
|
||||
width={barWidth}
|
||||
height={barHeight}
|
||||
rx='4'
|
||||
className={barColorClass}
|
||||
>
|
||||
<title>
|
||||
{getDayLabel(item.date)}: {item.adherence}% adherence
|
||||
</title>
|
||||
</rect>
|
||||
<text
|
||||
x={x + barWidth / 2}
|
||||
y={chartHeight + 20}
|
||||
textAnchor='middle'
|
||||
className='text-xs fill-current text-slate-600 dark:text-slate-300 font-medium'
|
||||
>
|
||||
{getDayLabel(item.date)}
|
||||
</text>
|
||||
</g>
|
||||
);
|
||||
})}
|
||||
</svg>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default BarChart;
|
||||
38
components/ui/ReminderCard.tsx
Normal file
38
components/ui/ReminderCard.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
import React from 'react';
|
||||
import { ReminderInstance } from '../../types';
|
||||
import { ClockIcon, getReminderIcon } from '../icons/Icons';
|
||||
|
||||
interface ReminderCardProps {
|
||||
reminder: ReminderInstance;
|
||||
}
|
||||
|
||||
const ReminderCard: React.FC<ReminderCardProps> = ({ reminder }) => {
|
||||
const timeString = reminder.scheduledTime.toLocaleTimeString([], {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
});
|
||||
const ReminderIcon = getReminderIcon(reminder.icon);
|
||||
|
||||
return (
|
||||
<li className='shadow-md rounded-lg p-4 flex flex-col justify-between transition-all duration-300 bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700'>
|
||||
<div>
|
||||
<div className='flex justify-between items-start'>
|
||||
<div className='flex items-center space-x-3'>
|
||||
<ReminderIcon className='w-7 h-7 text-sky-500 dark:text-sky-400 flex-shrink-0' />
|
||||
<div>
|
||||
<h4 className='font-bold text-lg text-slate-800 dark:text-slate-100'>
|
||||
{reminder.title}
|
||||
</h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex items-center space-x-2 mt-4 font-semibold text-lg text-slate-500 dark:text-slate-400'>
|
||||
<ClockIcon className='w-5 h-5' />
|
||||
<span>{timeString}</span>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
};
|
||||
|
||||
export default ReminderCard;
|
||||
74
components/ui/ThemeSwitcher.tsx
Normal file
74
components/ui/ThemeSwitcher.tsx
Normal file
@@ -0,0 +1,74 @@
|
||||
import React, { useState, useRef, useEffect } from 'react';
|
||||
import { useTheme } from '../../hooks/useTheme';
|
||||
import { SunIcon, MoonIcon, DesktopIcon } from '../icons/Icons';
|
||||
|
||||
type Theme = 'light' | 'dark' | 'system';
|
||||
|
||||
const themeOptions: {
|
||||
value: Theme;
|
||||
label: string;
|
||||
icon: React.FC<React.ComponentProps<'svg'>>;
|
||||
}[] = [
|
||||
{ value: 'light', label: 'Light', icon: SunIcon },
|
||||
{ value: 'dark', label: 'Dark', icon: MoonIcon },
|
||||
{ value: 'system', label: 'System', icon: DesktopIcon },
|
||||
];
|
||||
|
||||
const ThemeSwitcher: React.FC = () => {
|
||||
const { theme, setTheme } = useTheme();
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const currentTheme =
|
||||
themeOptions.find(t => t.value === theme) || themeOptions[2];
|
||||
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (
|
||||
dropdownRef.current &&
|
||||
!dropdownRef.current.contains(event.target as Node)
|
||||
) {
|
||||
setIsOpen(false);
|
||||
}
|
||||
};
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
return () => document.removeEventListener('mousedown', handleClickOutside);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className='relative' ref={dropdownRef}>
|
||||
<button
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
className='flex items-center justify-center w-10 h-10 rounded-lg bg-slate-100 hover:bg-slate-200 dark:bg-slate-700 dark:hover:bg-slate-600 transition-colors'
|
||||
aria-label={`Current theme: ${currentTheme.label}. Change theme.`}
|
||||
>
|
||||
<SunIcon className='w-5 h-5 text-slate-700 dark:hidden' />
|
||||
<MoonIcon className='w-5 h-5 text-slate-200 hidden dark:block' />
|
||||
</button>
|
||||
|
||||
{isOpen && (
|
||||
<div className='absolute right-0 mt-2 w-36 bg-white dark:bg-slate-800 rounded-md shadow-lg ring-1 ring-black ring-opacity-5 py-1 z-30 border dark:border-slate-700'>
|
||||
{themeOptions.map(option => (
|
||||
<button
|
||||
key={option.value}
|
||||
onClick={() => {
|
||||
setTheme(option.value);
|
||||
setIsOpen(false);
|
||||
}}
|
||||
className={`w-full text-left flex items-center space-x-2 px-3 py-2 text-sm ${
|
||||
theme === option.value
|
||||
? 'bg-indigo-600 text-white'
|
||||
: 'text-slate-700 dark:text-slate-200 hover:bg-slate-100 dark:hover:bg-slate-700'
|
||||
}`}
|
||||
>
|
||||
<option.icon className='w-4 h-4' />
|
||||
<span>{option.label}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ThemeSwitcher;
|
||||
4
components/ui/index.ts
Normal file
4
components/ui/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
// UI Components
|
||||
export { default as BarChart } from './BarChart';
|
||||
export { default as ReminderCard } from './ReminderCard';
|
||||
export { default as ThemeSwitcher } from './ThemeSwitcher';
|
||||
Reference in New Issue
Block a user