(function(){ // ============================================================ // habits.jsx — Gewohnheiten-Tracker // Components: HabitView, HabitForm, SportIcon // Depends on (window): todayISO, localISO // ============================================================ const { useState } = React; // Alle verfügbaren Icons aus habit-icons/ const HABIT_ICONS = [ // Sport { key: "runner", label: "Laufen" }, { key: "bicycle", label: "Velo" }, { key: "dumbbell", label: "Gym" }, { key: "swimming", label: "Schwimmen" }, { key: "yoga", label: "Yoga" }, { key: "walk", label: "Spazieren" }, { key: "hiking", label: "Wandern" }, { key: "stretch", label: "Dehnen" }, { key: "activity", label: "Aktivität" }, { key: "basketball", label: "Basketball" }, { key: "boxing", label: "Boxen" }, { key: "soccer", label: "Fussball" }, { key: "tennis", label: "Tennis" }, { key: "golf", label: "Golf" }, { key: "stopwatch", label: "Workout" }, { key: "target", label: "Ziel" }, { key: "trophy", label: "Trophy" }, // Gesundheit & Lifestyle { key: "water", label: "Wasser" }, { key: "sleep", label: "Schlafen" }, { key: "vitamins", label: "Vitamine" }, { key: "medicine", label: "Medizin" }, { key: "healthy", label: "Gesund" }, { key: "skincare", label: "Hautpflege" }, { key: "teeth", label: "Zähne" }, { key: "sunrise", label: "Frühaufsteher"}, // Geist & Entspannung { key: "meditation", label: "Meditation" }, { key: "reading", label: "Lesen" }, { key: "journaling", label: "Tagebuch" }, { key: "gratitude", label: "Dankbarkeit" }, { key: "pray", label: "Beten" }, // Produktivität { key: "study", label: "Lernen" }, { key: "coding", label: "Coden" }, { key: "music", label: "Musik" }, { key: "money", label: "Finanzen" }, // Haushalt { key: "cook", label: "Kochen" }, { key: "cleaning", label: "Putzen" }, { key: "laundry", label: "Wäsche" }, // Verzicht { key: "nosmoking", label: "Nicht rauchen"}, { key: "alcohol", label: "Kein Alkohol" }, { key: "nophone", label: "Kein Handy" }, { key: "screentime", label: "Weniger Screen"}, ]; // Fallback: alte icon-keys auf neue mappen const ICON_KEY_MAP = { run: "runner", bike: "bicycle", gym: "dumbbell", swim: "swimming", walk: "walk", yoga: "yoga", stretch: "stretch", other: "activity" }; function resolveIconKey(key) { if (!key) return "activity"; if (ICON_KEY_MAP[key]) return ICON_KEY_MAP[key]; if (HABIT_ICONS.find(i => i.key === key)) return key; return "activity"; } function SportIcon({ iconKey, size, color }) { size = size || 20; const resolved = resolveIconKey(iconKey); // color filter: wenn accent-farbe → CSS filter; sonst grau const isAccent = !color || color === "var(--col-accent)"; const style = { width: size, height: size, display: "block", opacity: isAccent ? 1 : 0.45, filter: isAccent ? "var(--icon-accent-filter, none)" : "none", }; return React.createElement("img", { src: "./habit-icons/" + resolved + ".svg", width: size, height: size, alt: resolved, style, draggable: false }); } function HabitView({ habits, onAdd, onUpdate, onDelete, isMobile }) { const [showForm, setShowForm] = useState(false); const [viewMode, setViewMode] = useState("week"); // FEATURE: Monats-Habittracker const [monthOffset, setMonthOffset] = useState(0); const today = todayISO(); habits = (habits || []).filter(h => !h.deletedAt); // Letzte 7 Tage (älteste zuerst, heute letzte) const DAY_KEYS=["habit.day.sun","habit.day.mon","habit.day.tue","habit.day.wed","habit.day.thu","habit.day.fri","habit.day.sat"]; const days = Array.from({length: 7}, (_, i) => { const d = new Date(); d.setDate(d.getDate() - 6 + i); const iso = localISO(d); return { iso, label: t(DAY_KEYS[d.getDay()]), date: d.getDate(), isToday: iso === today }; }); // Tage des angezeigten Monats (für Monats-Ansicht) const monthBase = new Date(); monthBase.setDate(1); monthBase.setMonth(monthBase.getMonth() + monthOffset); const monthLabel = monthBase.toLocaleDateString(window.currentLang==="en"?"en-GB":"de-CH", {month:"long", year:"numeric"}); const daysInMonth = new Date(monthBase.getFullYear(), monthBase.getMonth()+1, 0).getDate(); const monthDays = Array.from({length: daysInMonth}, (_, i) => { const d = new Date(monthBase.getFullYear(), monthBase.getMonth(), i+1); const iso = localISO(d); return { iso, date: i+1, isToday: iso === today }; }); function isCompleted(habit, iso) { return (habit.completions || []).includes(iso); } function toggleCompletion(habit, iso) { if (iso > today) return; const c = habit.completions || []; onUpdate({...habit, completions: c.includes(iso) ? c.filter(d => d !== iso) : [...c, iso], updatedAt: Date.now() }); } // Returns Monday-ISO of the week containing dateIso function weekOf(dateIso) { const d = new Date(dateIso + "T12:00:00"); const dow = (d.getDay() + 6) % 7; // 0=Mon d.setDate(d.getDate() - dow); return localISO(d); } function getStreak(habit) { const set = new Set(habit.completions || []); let streak = 0, gapUsed = false; // FIXED C3: grace day option added const d = new Date(today + "T12:00:00"); for (let i = 0; i < 365; i++) { if (set.has(localISO(d))) { streak++; } else if (habit.graceDay && !gapUsed) { gapUsed = true; } else { break; } d.setDate(d.getDate() - 1); } return streak; } function getLongest(habit) { const set = new Set(habit.completions || []); if (!set.size) return 0; const earliest = [...set].sort()[0]; const d = new Date(today + "T12:00:00"); let best = 0, cur = 0, gapUsed = false; // FIXED C3: grace day option added while (localISO(d) >= earliest) { if (set.has(localISO(d))) { cur++; best = Math.max(best, cur); } else if (habit.graceDay && !gapUsed) { gapUsed = true; } else { cur = 0; gapUsed = false; } d.setDate(d.getDate() - 1); } return best; } function getDays30(habit) { const set = new Set(habit.completions || []); const base = new Date(today + "T12:00:00"); base.setDate(base.getDate() - 29); let done = 0; for (let i = 0; i < 30; i++) { const d = new Date(base); d.setDate(d.getDate() + i); if (localISO(d) > today) break; if (set.has(localISO(d))) done++; } return done; } const todayDone = habits.filter(h => isCompleted(h, today)).length; const maxStreak = habits.length > 0 ? Math.max(...habits.map(getStreak)) : 0; const maxLongest = habits.length > 0 ? Math.max(...habits.map(getLongest)) : 0; const totalDays30 = habits.length > 0 ? Math.round(habits.reduce((s, h) => s + getDays30(h), 0) / habits.length) : 0; return (
{habits.length === 0 ? t("habit.noneYet") : t("habit.doneToday",{done:todayDone,total:habits.length}) + (maxStreak > 0 ? t("habit.streakSuffix",{n:maxStreak}) : "")}
{[["week",t("habit.week")],["month",t("habit.month")]].map(([id,label])=>( ))}
{viewMode==="month"&&(
{monthLabel}
)} {showForm && ( { onAdd(h); setShowForm(false); }} onCancel={() => setShowForm(false)}/> )} {habits.length === 0 && !showForm && (

{t("habit.createFirst")}

)} {habits.length > 0 && viewMode==="week" && ( <> {/* Tages-Header */}
{days.map(d => (
{d.label.slice(0,2)}
{d.date}
))}
{/* Gewohnheits-Zeilen */} {habits.map(h => { const streak = getStreak(h); return (
{h.name}
{(()=>{ const ws=weekOf(today); const we=new Date(ws+"T12:00:00");we.setDate(we.getDate()+6);const wEnd=localISO(we); const doneDays=(h.completions||[]).filter(iso=>iso>=ws&&iso<=wEnd&&iso<=today) .map(iso=>t(DAY_KEYS[new Date(iso+"T12:00:00").getDay()])); if(doneDays.length===0)return{t("habit.nothingThisWeek")}; const td=isCompleted(h,today); return{doneDays.join(", ")}{td?" ✓":""}; })()}
{days.map(d => { const done = isCompleted(h, d.iso); const future = d.iso > today; return (
!future && toggleCompletion(h, d.iso)} style={{ width:34,height:34,borderRadius:"50%", display:"flex",alignItems:"center",justifyContent:"center", margin:"0 auto",cursor:future?"default":"pointer", fontSize:done?14:12, background: done ? (d.isToday?"var(--col-accent)":"#166534") : "transparent", border: done ? "none" : d.isToday ? "2px solid var(--col-accent)" : future ? "1.5px dashed var(--col-border)" : "1.5px solid var(--col-border)", color: done ? "white" : d.isToday ? "var(--col-accent)" : "var(--col-text-3)", boxShadow: done && d.isToday ? "0 0 0 3px var(--col-accent-wash)" : "none", transition:"all .12s" }}> {done ? "✓" : future ? "" : "○"}
); })}
); })} )} {habits.length > 0 && viewMode==="month" && ( <> {habits.map(h => { const monthDone = monthDays.filter(d=>isCompleted(h,d.iso)&&d.iso<=today).length; return (
{h.name}
{monthDone}/{monthDays.filter(d=>d.iso<=today).length}
{monthDays.map(d => { const done = isCompleted(h, d.iso); const future = d.iso > today; return (
!future && toggleCompletion(h, d.iso)} title={d.iso} style={{ width:24,height:24,borderRadius:6, display:"flex",alignItems:"center",justifyContent:"center", cursor:future?"default":"pointer", fontSize:10, background: done ? (d.isToday?"var(--col-accent)":"#166534") : "transparent", border: done ? "none" : d.isToday ? "2px solid var(--col-accent)" : future ? "1px dashed var(--col-border)" : "1px solid var(--col-border)", color: done ? "white" : d.isToday ? "var(--col-accent)" : "var(--col-text-3)", transition:"all .12s" }}> {d.date}
); })}
); })} )} {/* Statistiken */} {habits.length > 0 && (
{[ {n: maxStreak + (window.currentLang==="en"?"d":"T"), l: t("habit.streakCurrent"), col: maxStreak > 0 ? "var(--col-accent)" : "var(--col-text)"}, {n: maxLongest + (window.currentLang==="en"?"d":"T"), l: t("habit.streakLongest"), col: "var(--col-text)"}, {n: totalDays30 + (window.currentLang==="en"?"d":"T"),l: t("habit.avgDays30"), col: "var(--col-text)"} ].map((s, i) => (
{s.n}
{s.l}
))}
)}
); } function HabitForm({ onSave, onCancel, habit }) { const [name, setName] = useState(""); const [selectedIcon, setSelectedIcon] = useState("runner"); const [graceDay, setGraceDay] = useState(habit?.graceDay||false); // FIXED C3: grace day option added function save() { if (!name.trim()) return; onSave({ id: Date.now() + Math.random(), name: name.trim(), icon: selectedIcon, emoji: null, freq: "simple", days: [], timesPerWeek: null, completions: [], graceDay, createdAt: Date.now(), updatedAt: Date.now() }); } return (
{/* Icon-Auswahl */}
{HABIT_ICONS.map(ic => ( ))}
{/* Name */}
setName(e.target.value)} placeholder={t("habit.namePlaceholder")} onKeyDown={e=>e.key==="Enter"&&save()} autoFocus style={{width:"100%",boxSizing:"border-box",fontSize:13,borderRadius:8,border:"1px solid var(--col-border)",padding:"7px 10px",background:"var(--col-bg)",color:"var(--col-text)"}}/>
{t("habit.tip")}
); } function HabitEveningPopup({ habits, onUpdate, onClose }) { const today = todayISO(); habits = (habits || []).filter(h => !h.deletedAt); function toggle(habit) { const c = habit.completions || []; onUpdate({...habit, completions: c.includes(today) ? c.filter(d => d !== today) : [...c, today], updatedAt: Date.now() }); } return (
e.stopPropagation()}>
{t("habit.eveningTitle")}
{t("habit.eveningSubtitle")}
{habits.length===0&&(

{t("habit.none")}

)} {habits.map(h=>{ const done=(h.completions||[]).includes(today); return ( ); })}
); } Object.assign(window, { HabitView, SportIcon, HabitEveningPopup }); })();