v0.2.1 — Code Review Fixes

Fixes:
- CRITICAL: compareVersions used string comparison, fails for 1.10 vs 1.9 (now numeric)
- CRITICAL: Double CloseDatabase() in main.go (defer + OnShutdown)
- CRITICAL: Tailwind v4 in package.json but v3 config/syntax (downgraded to v3)
- CRITICAL: react-router-dom v7 with v5 types (switched to v6, removed deprecated types)
- IMPORTANT: UpdatePlanEntry hook signature mismatch (7 args vs Go's 4)
- IMPORTANT: AllergenPicker hidden checkbox inaccessible to screenreaders (sr-only)
- IMPORTANT: weekHelper getWeekFromDate returned wrong ISO year for edge cases
- IMPORTANT: getWeeksInYear bug for years where Dec 31 is in week 1 of next year
- IMPORTANT: getDateFromWeek off-by-one for some years (use Jan 4 anchor)
- IMPORTANT: ProductSearch click-outside missed dropdown (use container ref)
- IMPORTANT: seed.go LastInsertId=0 on INSERT OR IGNORE skip
- IMPORTANT: SQLite missing PRAGMA foreign_keys=ON and WAL mode
- IMPORTANT: AdditivePicker ADDITIVE_NAMES used numeric IDs but data uses letters
- IMPORTANT: Missing role=dialog/aria-modal on all modal dialogs
- IMPORTANT: Missing Escape key handler on ProductForm modal
- IMPORTANT: Sidebar NavLink aria-current used function instead of string
- IMPORTANT: useProducts searchProducts null safety for allergens/additives
- NICE-TO-HAVE: Added aria-live=polite to WeekPlanner for dynamic updates
- NICE-TO-HAVE: Added postcss.config.js for Tailwind v3
- NICE-TO-HAVE: Updated model comments to match actual day/meal conventions
- NICE-TO-HAVE: Modernized vite/typescript/plugin versions
This commit is contained in:
clawd
2026-02-20 10:11:54 +00:00
parent e146442513
commit df9e7c5541
17 changed files with 98 additions and 84 deletions

View File

@@ -7,33 +7,8 @@ interface AdditivePickerProps {
className?: string;
}
// Deutsche Namen für häufige Zusatzstoffe (E-Nummern)
const ADDITIVE_NAMES: Record<string, string> = {
'1': 'Farbstoff',
'2': 'Konservierungsstoff',
'3': 'Antioxidationsmittel',
'4': 'Geschmacksverstärker',
'5': 'Geschwefelt',
'6': 'Geschwärzt',
'7': 'Gewachst',
'8': 'Phosphat',
'9': 'Süßungsmittel',
'10': 'Phenylalaninquelle',
'11': 'Koffeinhaltig',
'12': 'Chininhaltig',
'13': 'Alkoholhaltig',
'14': 'Nitritpökelsalz',
'15': 'Milchsäure',
'16': 'Citronensäure',
'17': 'Ascorbinsäure',
'18': 'Tocopherol',
'19': 'Lecithin',
'20': 'Johannisbrotkernmehl',
'21': 'Guarkernmehl',
'22': 'Xanthan',
'23': 'Carrageen',
'24': 'Agar'
};
// Die Zusatzstoffe kommen direkt aus der DB mit id + name,
// daher wird hier kein separates Mapping benötigt.
export function AdditivePicker({ additives, selectedIds, onChange, className = '' }: AdditivePickerProps) {
const handleToggle = (additiveId: string) => {
@@ -68,7 +43,7 @@ export function AdditivePicker({ additives, selectedIds, onChange, className = '
>
{sortedAdditives.map(additive => {
const isSelected = selectedIds.includes(additive.id);
const displayName = ADDITIVE_NAMES[additive.id] || additive.name;
const displayName = additive.name;
return (
<label

View File

@@ -65,7 +65,7 @@ export function AllergenPicker({ allergens, selectedIds, onChange, className = '
type="checkbox"
checked={isSelected}
onChange={() => handleToggle(allergen.id)}
className="hidden"
className="sr-only"
aria-describedby={`allergen-${allergen.id}-description`}
/>

View File

@@ -82,8 +82,24 @@ export function ProductForm({
const isEditing = !!product;
// Escape-Taste schließt den Dialog
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Escape') {
onCancel();
}
};
document.addEventListener('keydown', handleKeyDown);
return () => document.removeEventListener('keydown', handleKeyDown);
}, [onCancel]);
return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50">
<div
className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50"
role="dialog"
aria-modal="true"
aria-label={isEditing ? 'Produkt bearbeiten' : 'Neues Produkt erstellen'}
>
<div className="bg-white rounded-lg shadow-xl max-w-4xl w-full max-h-[90vh] overflow-y-auto">
<form onSubmit={handleSubmit}>
{/* Header */}

View File

@@ -24,6 +24,7 @@ export function ProductSearch({
const [selectedIndex, setSelectedIndex] = useState(-1);
const inputRef = useRef<HTMLInputElement>(null);
const listRef = useRef<HTMLUListElement>(null);
const containerRef = useRef<HTMLDivElement>(null);
// Filter products based on search query
const filteredProducts = query.trim()
@@ -95,7 +96,7 @@ export function ProductSearch({
// Close dropdown when clicking outside
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (inputRef.current && !inputRef.current.contains(event.target as Node)) {
if (containerRef.current && !containerRef.current.contains(event.target as Node)) {
setIsOpen(false);
setSelectedIndex(-1);
}
@@ -116,7 +117,7 @@ export function ProductSearch({
const showResults = isOpen && (filteredProducts.length > 0 || (allowCustom && query.trim()));
return (
<div className={`relative ${className}`} role="combobox" aria-expanded={isOpen}>
<div ref={containerRef} className={`relative ${className}`} role="combobox" aria-expanded={isOpen}>
<input
ref={inputRef}
type="text"

View File

@@ -204,7 +204,7 @@ export function Sidebar({
: 'text-gray-700 hover:bg-gray-100 hover:text-gray-900'
}`
}
aria-current={({ isActive }) => isActive ? 'page' : undefined}
end={item.to === '/'}
>
{item.icon}
<span className="ml-3">{item.name}</span>
@@ -257,7 +257,13 @@ function CopyWeekDialog({ targetYear, targetWeek, onCopy, onCancel }: CopyWeekDi
};
return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50">
<div
className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50"
role="dialog"
aria-modal="true"
aria-label="Kalenderwoche kopieren"
onKeyDown={(e) => { if (e.key === 'Escape') onCancel(); }}
>
<div className="bg-white rounded-lg shadow-xl max-w-md w-full">
<form onSubmit={handleSubmit}>
<div className="p-6">

View File

@@ -35,7 +35,13 @@ export function SpecialDayDialog({
const isEditing = !!existingSpecialDay;
return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50">
<div
className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50"
role="dialog"
aria-modal="true"
aria-label={`Sondertag für ${DAY_NAMES[day]}`}
onKeyDown={(e) => { if (e.key === 'Escape') onCancel(); }}
>
<div className="bg-white rounded-lg shadow-xl max-w-md w-full">
<form onSubmit={handleSubmit}>
{/* Header */}

View File

@@ -136,7 +136,7 @@ export function WeekPlanner({ year, week, className = '' }: WeekPlannerProps) {
}
return (
<div className={className}>
<div className={className} aria-live="polite">
{/* Error Banner */}
{error && (
<div className="bg-red-50 border border-red-200 rounded-md p-4 mb-6">