Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
138 changes: 72 additions & 66 deletions src/app/plans/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
} from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
import { useAutoAnimate } from '@formkit/auto-animate/react';
import { Drawer } from 'vaul';
import Card from '@/components/Card';
import Button from '@/components/Button';
import Input from '@/components/Input';
Expand Down Expand Up @@ -1080,76 +1081,81 @@ export default function PlanEditorPage() {
</DragOverlay>
</DndContext>

{/* Import Dialog */}
{showImportDialog && (
<div className="fixed inset-0 bg-black/70 flex items-center justify-center z-50 p-4">
<Card className="w-full max-w-md max-h-[80vh] flex flex-col">
<div className="flex items-center justify-between p-4 border-b border-zinc-800">
<h2 className="text-lg font-semibold">Mahlzeiten importieren</h2>
<button
onClick={() => {
setShowImportDialog(false);
setSelectedImportMeals(new Set());
}}
className="p-2 text-zinc-400 hover:text-white"
>
<X size={20} />
</button>
</div>

<div className="flex-1 overflow-y-auto p-4 space-y-4">
{otherPlans.length === 0 ? (
<p className="text-zinc-500 text-center py-8">Keine anderen Pläne vorhanden</p>
) : (
otherPlans.map(plan => (
<div key={plan.id}>
<p className="text-sm text-zinc-400 mb-2">{plan.name}</p>
<div className="space-y-1">
{plan.meals.map(meal => {
const key = `${plan.id}::${meal.id}`;
const isSelected = selectedImportMeals.has(key);
const mealTotals = getMealTotals(meal);
return (
<button
key={meal.id}
onClick={() => toggleImportMeal(key)}
className={`w-full flex items-center gap-3 p-3 rounded-lg text-left transition-colors ${
isSelected
? 'bg-amber-500/20 border border-amber-500/50'
: 'bg-zinc-800/50 hover:bg-zinc-800 border border-transparent'
}`}
>
<div className="text-zinc-400">
<MealIcon icon={meal.icon} size={18} />
</div>
<div className="flex-1 min-w-0">
<p className="font-medium truncate">{meal.name}</p>
<p className="text-xs text-zinc-500">
{mealTotals.calories} kcal · {mealTotals.protein}g P · {meal.items.length} Items
</p>
</div>
{isSelected && (
<Check size={18} className="text-amber-500 shrink-0" />
)}
</button>
);
})}
{/* Import Sheet */}
<Drawer.Root
open={showImportDialog}
onOpenChange={(open) => {
setShowImportDialog(open);
if (!open) setSelectedImportMeals(new Set());
}}
>
<Drawer.Portal>
<Drawer.Overlay className="fixed inset-0 bg-black/60 z-50" />
<Drawer.Content className="fixed bottom-0 left-0 right-0 z-50 outline-none max-h-[85vh] flex flex-col">
<div className="bg-zinc-900 rounded-t-2xl border-t border-zinc-800 flex flex-col flex-1 overflow-hidden">
<div className="mx-auto w-12 h-1.5 flex-shrink-0 rounded-full bg-zinc-700 my-4" />
<Drawer.Title className="text-lg font-semibold text-white px-6 mb-4">
Mahlzeiten importieren
</Drawer.Title>

<div className="flex-1 overflow-y-auto px-6 space-y-4">
{otherPlans.length === 0 ? (
<p className="text-zinc-500 text-center py-8 text-[15px]">Keine anderen Pläne vorhanden</p>
) : (
otherPlans.map(plan => (
<div key={plan.id}>
<p className="text-[13px] text-zinc-400 mb-2">{plan.name}</p>
<div className="space-y-1">
{plan.meals.map(meal => {
const key = `${plan.id}::${meal.id}`;
const isSelected = selectedImportMeals.has(key);
const mealTotals = getMealTotals(meal);
return (
<button
key={meal.id}
onClick={() => toggleImportMeal(key)}
className={`w-full flex items-center gap-3 p-4 min-h-[44px] rounded-xl text-left transition-colors ${
isSelected
? 'bg-amber-500/20 border border-amber-500/50'
: 'bg-zinc-800/50 hover:bg-zinc-800 border border-transparent'
}`}
>
<div className="text-zinc-400">
<MealIcon icon={meal.icon} size={20} />
</div>
<div className="flex-1 min-w-0">
<p className="text-[17px] font-medium truncate">{meal.name}</p>
<p className="text-[13px] text-zinc-500">
{mealTotals.calories} kcal · {mealTotals.protein}g P · {meal.items.length} Items
</p>
</div>
{isSelected && (
<Check size={20} className="text-amber-500 shrink-0" />
)}
</button>
);
})}
</div>
</div>
</div>
))
)}
</div>
))
)}
</div>

{selectedImportMeals.size > 0 && (
<div className="p-4 border-t border-zinc-800">
<Button onClick={importSelectedMeals} className="w-full">
{selectedImportMeals.size} Mahlzeit{selectedImportMeals.size > 1 ? 'en' : ''} importieren
<div className="p-6 pt-4">
<Button
onClick={importSelectedMeals}
className="w-full"
disabled={selectedImportMeals.size === 0}
>
{selectedImportMeals.size > 0
? `${selectedImportMeals.size} Mahlzeit${selectedImportMeals.size > 1 ? 'en' : ''} importieren`
: 'Mahlzeiten auswählen'}
</Button>
</div>
)}
</Card>
</div>
)}
</div>
</Drawer.Content>
</Drawer.Portal>
</Drawer.Root>

<ConfirmDialog
open={deleteMealDialog.open}
Expand Down
137 changes: 64 additions & 73 deletions src/app/weight/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@ import { useState, useMemo } from 'react';
import { format, differenceInDays, addDays, subDays } from 'date-fns';
import { de } from 'date-fns/locale';
import { LineChart, Line, XAxis, YAxis, ResponsiveContainer, Tooltip, ReferenceLine } from 'recharts';
import { Plus, TrendingDown, Zap, Info, X, Download } from 'lucide-react';
import { Plus, TrendingDown, Zap, Info, Download } from 'lucide-react';
import Card from '@/components/Card';
import Button from '@/components/Button';
import Input from '@/components/Input';
import InfoSheet from '@/components/InfoSheet';
import { useData } from '@/lib/data-store';
import { getItemTotals, getPlanTotals, DEFAULT_PLAN_A, DEFAULT_PLAN_B } from '@/lib/mealPlan';

Expand Down Expand Up @@ -550,84 +551,74 @@ export default function WeightPage() {
</div>
</Card>

{/* Chart Info Modal */}
{showChartInfo && (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
<div className="fixed inset-0 bg-black/70" onClick={() => setShowChartInfo(false)} />
<div className="relative bg-zinc-900 rounded-2xl border border-zinc-800 p-6 max-w-md w-full max-h-[80vh] overflow-y-auto">
<button
onClick={() => setShowChartInfo(false)}
className="absolute top-4 right-4 text-zinc-400 hover:text-white"
>
<X size={20} />
</button>

<h2 className="text-lg font-semibold mb-4">Chart-Linien erklärt</h2>

<div className="space-y-5">
{/* Aktuell */}
<div>
<div className="flex items-center gap-2 mb-2">
<div className="w-3 h-3 rounded-full bg-white"></div>
<span className="font-medium">Aktuell (Weiße Punkte)</span>
</div>
<p className="text-sm text-zinc-400">
Dein tatsächlich gemessenes Gewicht an jedem Tag.
</p>
</div>
{/* Chart Info Sheet */}
<InfoSheet
open={showChartInfo}
onOpenChange={setShowChartInfo}
title="Chart-Linien erklärt"
>
<div className="space-y-5">
{/* Aktuell */}
<div>
<div className="flex items-center gap-2 mb-2">
<div className="w-3 h-3 rounded-full bg-white"></div>
<span className="font-medium">Aktuell (Weiße Punkte)</span>
</div>
<p className="text-[15px] text-zinc-400">
Dein tatsächlich gemessenes Gewicht an jedem Tag.
</p>
</div>

{/* Trend */}
<div>
<div className="flex items-center gap-2 mb-2">
<div className="w-6 h-0.5 bg-amber-500"></div>
<span className="font-medium text-amber-500">Trend (Gelbe Linie)</span>
</div>
<p className="text-sm text-zinc-400 mb-2">
7-Tage gleitender Durchschnitt deines Gewichts. Glättet tägliche Schwankungen (Wasser, Verdauung) und zeigt die echte Richtung.
</p>
<div className="bg-zinc-800 rounded-lg p-3 text-xs font-mono">
Trend = Ø der letzten 7 Messungen
</div>
</div>
{/* Trend */}
<div>
<div className="flex items-center gap-2 mb-2">
<div className="w-6 h-0.5 bg-amber-500"></div>
<span className="font-medium text-amber-500">Trend (Gelbe Linie)</span>
</div>
<p className="text-[15px] text-zinc-400 mb-2">
7-Tage gleitender Durchschnitt deines Gewichts. Glättet tägliche Schwankungen (Wasser, Verdauung) und zeigt die echte Richtung.
</p>
<div className="bg-zinc-800 rounded-xl p-3 text-[13px] font-mono">
Trend = Ø der letzten 7 Messungen
</div>
</div>

{/* Erwartet */}
<div>
<div className="flex items-center gap-2 mb-2">
<div className="w-6 h-0.5 bg-emerald-500" style={{ backgroundImage: 'repeating-linear-gradient(90deg, #22c55e 0, #22c55e 4px, transparent 4px, transparent 8px)' }}></div>
<span className="font-medium text-emerald-500">Erwartet (Grün gestrichelt)</span>
</div>
<p className="text-sm text-zinc-400 mb-2">
Theoretischer Gewichtsverlust basierend auf deinem Kaloriendefizit.
</p>
<div className="bg-zinc-800 rounded-lg p-3 text-xs font-mono space-y-1">
<div>Defizit = TDEE − Kalorienaufnahme</div>
<div>Verlust/Tag = Defizit ÷ 7700 kcal</div>
<div className="text-zinc-500">(7700 kcal = 1 kg Körperfett)</div>
</div>
</div>
{/* Erwartet */}
<div>
<div className="flex items-center gap-2 mb-2">
<div className="w-6 h-0.5 bg-emerald-500" style={{ backgroundImage: 'repeating-linear-gradient(90deg, #22c55e 0, #22c55e 4px, transparent 4px, transparent 8px)' }}></div>
<span className="font-medium text-emerald-500">Erwartet (Grün gestrichelt)</span>
</div>
<p className="text-[15px] text-zinc-400 mb-2">
Theoretischer Gewichtsverlust basierend auf deinem Kaloriendefizit.
</p>
<div className="bg-zinc-800 rounded-xl p-3 text-[13px] font-mono space-y-1">
<div>Defizit = TDEE − Kalorienaufnahme</div>
<div>Verlust/Tag = Defizit ÷ 7700 kcal</div>
<div className="text-zinc-500">(7700 kcal = 1 kg Körperfett)</div>
</div>
</div>

{/* TDEE */}
<div className="border-t border-zinc-800 pt-4">
<div className="flex items-center gap-2 mb-2">
<Zap size={16} className="text-blue-400" />
<span className="font-medium text-blue-400">TDEE Berechnung</span>
</div>
<p className="text-sm text-zinc-400 mb-2">
Nach 21+ Tagen Tracking wird dein echter TDEE automatisch berechnet:
</p>
<div className="bg-zinc-800 rounded-lg p-3 text-xs font-mono">
<div>TDEE = Ø Kalorien + (Gewichtsverlust × 1100)</div>
<div className="text-zinc-500 mt-1">Wobei: 1100 = 7700 ÷ 7 Tage</div>
</div>
<p className="text-xs text-zinc-500 mt-2">
Aktueller TDEE: {tdeeTracking.calculatedTdee || data.profile.tdee} kcal
{tdeeTracking.calculatedTdee ? ' (berechnet)' : ' (manuell in Settings)'}
</p>
</div>
{/* TDEE */}
<div className="border-t border-zinc-800 pt-4">
<div className="flex items-center gap-2 mb-2">
<Zap size={16} className="text-blue-400" />
<span className="font-medium text-blue-400">TDEE Berechnung</span>
</div>
<p className="text-[15px] text-zinc-400 mb-2">
Nach 21+ Tagen Tracking wird dein echter TDEE automatisch berechnet:
</p>
<div className="bg-zinc-800 rounded-xl p-3 text-[13px] font-mono">
<div>TDEE = Ø Kalorien + (Gewichtsverlust × 1100)</div>
<div className="text-zinc-500 mt-1">Wobei: 1100 = 7700 ÷ 7 Tage</div>
</div>
<p className="text-[13px] text-zinc-500 mt-2">
Aktueller TDEE: {tdeeTracking.calculatedTdee || data.profile.tdee} kcal
{tdeeTracking.calculatedTdee ? ' (berechnet)' : ' (manuell in Settings)'}
</p>
</div>
</div>
)}
</InfoSheet>
</div>
);
}
72 changes: 72 additions & 0 deletions src/components/InfoSheet.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
'use client';

import { Drawer } from 'vaul';
import { useEffect, useState, ReactNode } from 'react';
import { X } from 'lucide-react';

interface InfoSheetProps {
open: boolean;
onOpenChange: (open: boolean) => void;
title: string;
children: ReactNode;
}

export default function InfoSheet({
open,
onOpenChange,
title,
children,
}: InfoSheetProps) {
const [isDesktop, setIsDesktop] = useState(false);

useEffect(() => {
const checkDesktop = () => setIsDesktop(window.innerWidth >= 768);
checkDesktop();
window.addEventListener('resize', checkDesktop);
return () => window.removeEventListener('resize', checkDesktop);
}, []);

if (isDesktop) {
if (!open) return null;

return (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
<div
className="fixed inset-0 bg-black/60"
onClick={() => onOpenChange(false)}
/>
<div className="relative bg-zinc-900 rounded-2xl border border-zinc-800 p-6 w-full max-w-md max-h-[80vh] overflow-y-auto shadow-xl">
<button
onClick={() => onOpenChange(false)}
className="absolute top-4 right-4 w-8 h-8 flex items-center justify-center text-zinc-400 hover:text-white rounded-lg hover:bg-zinc-800 transition-colors"
>
<X size={20} />
</button>
<h2 className="text-lg font-semibold text-white mb-4 pr-8">
{title}
</h2>
{children}
</div>
</div>
);
}

return (
<Drawer.Root open={open} onOpenChange={onOpenChange}>
<Drawer.Portal>
<Drawer.Overlay className="fixed inset-0 bg-black/60 z-50" />
<Drawer.Content className="fixed bottom-0 left-0 right-0 z-50 outline-none max-h-[85vh] flex flex-col">
<div className="bg-zinc-900 rounded-t-2xl border-t border-zinc-800 flex flex-col flex-1 overflow-hidden">
<div className="mx-auto w-12 h-1.5 flex-shrink-0 rounded-full bg-zinc-700 my-4" />
<div className="px-6 pb-8 overflow-y-auto flex-1">
<Drawer.Title className="text-lg font-semibold text-white mb-4">
{title}
</Drawer.Title>
{children}
</div>
</div>
</Drawer.Content>
</Drawer.Portal>
</Drawer.Root>
);
}