tududi/frontend/components/Habits/Habits.tsx
Chris 269197e3db
Feat: habits (#707)
* Scaffold habits

* Fix today issues

* Fix buttons in taskitem

* Fix mobile layout

* Fix creation process

* Add to sidebar

* fixup! Add to sidebar

* fixup! fixup! Add to sidebar
2025-12-13 08:47:52 +02:00

215 lines
9.6 KiB
TypeScript

import React, { useEffect, useMemo } from 'react';
import { useNavigate } from 'react-router-dom';
import { useStore } from '../../store/useStore';
import { Task } from '../../entities/Task';
import HabitCard from './HabitCard';
import {
PlusIcon,
FireIcon,
CheckCircleIcon,
ChartBarIcon,
} from '@heroicons/react/24/outline';
import { useTranslation } from 'react-i18next';
const Habits: React.FC = () => {
const { t } = useTranslation();
const navigate = useNavigate();
const { habits, isLoading, loadHabits, logCompletion } = useStore(
(state) => state.habitsStore
);
useEffect(() => {
loadHabits();
}, [loadHabits]);
const handleCreateHabit = () => {
navigate('/habit/new');
};
const handleViewHabit = (habit: Task) => {
if (habit.uid) {
navigate(`/habit/${habit.uid}`);
}
};
const handleComplete = async (habitUid: string) => {
try {
await logCompletion(habitUid);
} catch (error) {
console.error('Failed to log completion:', error);
}
};
// Calculate dashboard statistics
const dashboardStats = useMemo(() => {
const totalHabits = habits.length;
const totalCompletions = habits.reduce(
(sum, h) => sum + (h.habit_total_completions || 0),
0
);
const totalCurrentStreak = habits.reduce(
(sum, h) => sum + (h.habit_current_streak || 0),
0
);
const totalBestStreak = habits.reduce(
(max, h) => Math.max(max, h.habit_best_streak || 0),
0
);
const activeStreaks = habits.filter(
(h) => (h.habit_current_streak || 0) > 0
).length;
return {
totalHabits,
totalCompletions,
totalCurrentStreak,
totalBestStreak,
activeStreaks,
};
}, [habits]);
if (isLoading) {
return (
<div className="flex justify-center items-center h-64">
<div className="text-lg dark:text-white">
{t('common.loading', 'Loading...')}
</div>
</div>
);
}
return (
<div className="w-full pt-4 pb-8 px-2 sm:px-4 lg:px-6">
<div className="w-full">
<div className="flex items-center justify-between gap-2 mb-8">
<h2 className="text-2xl font-light dark:text-white">
{t('habits.title', 'Habits')}
</h2>
<button
onClick={handleCreateHabit}
className="flex items-center gap-2 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition"
>
<PlusIcon className="w-5 h-5" />
{t('habits.new', 'New Habit')}
</button>
</div>
{habits.length === 0 ? (
<div className="text-center py-12 text-gray-500 dark:text-gray-400">
<p>
{t(
'habits.empty',
'No habits yet. Create your first habit to get started!'
)}
</p>
</div>
) : (
<>
{/* Dashboard */}
<div className="mb-8">
<h2 className="text-xl font-semibold dark:text-white mb-4 flex items-center gap-2">
<ChartBarIcon className="h-6 w-6" />
{t('habits.overview', 'Overview')}
</h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
{/* Total Habits */}
<div className="bg-white dark:bg-gray-900 rounded-lg shadow p-4">
<div className="flex items-center justify-between mb-2">
<h3 className="text-sm font-medium text-gray-600 dark:text-gray-400">
{t(
'habits.totalHabits',
'Total Habits'
)}
</h3>
<FireIcon className="h-5 w-5 text-orange-500" />
</div>
<p className="text-3xl font-bold text-gray-900 dark:text-white">
{dashboardStats.totalHabits}
</p>
</div>
{/* Active Streaks */}
<div className="bg-white dark:bg-gray-900 rounded-lg shadow p-4">
<div className="flex items-center justify-between mb-2">
<h3 className="text-sm font-medium text-gray-600 dark:text-gray-400">
{t(
'habits.activeStreaks',
'Active Streaks'
)}
</h3>
<FireIcon className="h-5 w-5 text-orange-500" />
</div>
<p className="text-3xl font-bold text-gray-900 dark:text-white">
{dashboardStats.activeStreaks}
</p>
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
{t('habits.total', 'Total')}:{' '}
{dashboardStats.totalCurrentStreak}{' '}
{t('habits.days', 'days')}
</p>
</div>
{/* Best Streak */}
<div className="bg-white dark:bg-gray-900 rounded-lg shadow p-4">
<div className="flex items-center justify-between mb-2">
<h3 className="text-sm font-medium text-gray-600 dark:text-gray-400">
{t(
'habits.bestStreak',
'Best Streak'
)}
</h3>
<FireIcon className="h-5 w-5 text-yellow-500" />
</div>
<p className="text-3xl font-bold text-gray-900 dark:text-white">
{dashboardStats.totalBestStreak}
</p>
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
{t('habits.days', 'days')}
</p>
</div>
{/* Total Completions */}
<div className="bg-white dark:bg-gray-900 rounded-lg shadow p-4">
<div className="flex items-center justify-between mb-2">
<h3 className="text-sm font-medium text-gray-600 dark:text-gray-400">
{t(
'habits.totalCompletions',
'Total Completions'
)}
</h3>
<CheckCircleIcon className="h-5 w-5 text-green-500" />
</div>
<p className="text-3xl font-bold text-gray-900 dark:text-white">
{dashboardStats.totalCompletions}
</p>
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
{t('habits.allTime', 'all time')}
</p>
</div>
</div>
</div>
{/* Habits Grid */}
<div>
<h2 className="text-xl font-semibold dark:text-white mb-4">
{t('habits.yourHabits', 'Your Habits')}
</h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{habits.map((habit) => (
<HabitCard
key={habit.uid}
habit={habit}
onComplete={handleComplete}
onEdit={handleViewHabit}
/>
))}
</div>
</div>
</>
)}
</div>
</div>
);
};
export default Habits;