feat: update ProjectE application

This commit is contained in:
2026-07-18 19:05:52 -04:00
parent 8f55626e03
commit 4c1cc50231
68 changed files with 1586 additions and 697 deletions
+48 -9
View File
@@ -8,6 +8,8 @@ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { HabitCard } from '@/components/habits/habit-card';
import { HabitCompletionDialog } from '@/components/habits/habit-completion-dialog';
import type { Habit } from '@project-e/shared';
import { CreateItemDialog } from '@/components/create-item-dialog';
import { toast } from 'sonner';
// Lazy load react-calendar-heatmap (~15KB)
const HabitHeatmap = dynamic(
@@ -28,22 +30,36 @@ interface HabitWithMeta extends Habit {
export default function HabitsPage() {
const [habits, setHabits] = useState<HabitWithMeta[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [selectedHabit, setSelectedHabit] = useState<HabitWithMeta | null>(null);
const [completionDialogOpen, setCompletionDialogOpen] = useState(false);
const [createOpen, setCreateOpen] = useState(false);
useEffect(() => {
fetchHabits();
}, []);
useEffect(() => {
if (new URLSearchParams(window.location.search).get('new') === 'true') setCreateOpen(true);
}, []);
function handleCreateOpenChange(open: boolean) {
setCreateOpen(open);
if (!open && new URLSearchParams(window.location.search).get('new') === 'true') {
window.history.replaceState(null, '', '/habits');
}
}
async function fetchHabits() {
try {
setError(null);
const response = await fetch('/api/habits');
if (response.ok) {
const data = await response.json();
setHabits(data.items || []);
}
if (!response.ok) throw new Error('Unable to load habits.');
const data = await response.json();
setHabits(data.items || []);
} catch (error) {
console.error('Failed to fetch habits:', error);
setError('Unable to load habits. Please try again.');
} finally {
setLoading(false);
}
@@ -63,15 +79,18 @@ export default function HabitsPage() {
data: { mood?: number; value?: number; notes?: string }
) {
try {
await fetch(`/api/habits/${habitId}/logs`, {
const response = await fetch(`/api/habits/${habitId}/logs`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
});
if (!response.ok) throw new Error('Unable to save habit completion.');
fetchHabits();
setCompletionDialogOpen(false);
toast.success('Habit completed.');
} catch (error) {
console.error('Failed to log habit:', error);
toast.error('Unable to save habit completion. Please try again.');
}
}
@@ -80,7 +99,7 @@ export default function HabitsPage() {
habits.length > 0 ? Math.round((completedCount / habits.length) * 100) : 0;
if (loading) {
return <p className="text-muted-foreground">Loading habits...</p>;
return <p role="status" className="text-muted-foreground">Loading habits...</p>;
}
return (
@@ -92,12 +111,19 @@ export default function HabitsPage() {
Small actions, visible momentum.
</p>
</div>
<Button>
<Plus className="mr-2 h-4 w-4" />
<Button onClick={() => setCreateOpen(true)}>
<Plus className="mr-2 h-4 w-4" aria-hidden="true" />
New habit
</Button>
</div>
{error && (
<div role="alert" className="mb-6 flex items-center justify-between gap-4 rounded-lg border border-destructive/30 bg-destructive/10 p-4 text-sm text-destructive">
<span>{error}</span>
<Button variant="outline" size="sm" onClick={fetchHabits}>Retry</Button>
</div>
)}
{/* Summary banner */}
<Card className="mb-6">
<CardContent className="flex items-center justify-between p-6">
@@ -116,7 +142,14 @@ export default function HabitsPage() {
{/* Habit cards grid */}
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
{habits.map((habit) => (
{habits.length === 0 && !error ? (
<Card className="md:col-span-2 lg:col-span-3">
<CardContent className="py-10 text-center">
<p className="text-muted-foreground">No habits yet. Start with one small action.</p>
<Button className="mt-4" onClick={() => setCreateOpen(true)}>Create a habit</Button>
</CardContent>
</Card>
) : habits.map((habit) => (
<HabitCard
key={habit.id}
habit={habit}
@@ -153,6 +186,12 @@ export default function HabitsPage() {
onSubmit={(data) => logHabitCompletion(selectedHabit.id, data)}
/>
)}
<CreateItemDialog
type="habit"
open={createOpen}
onOpenChange={handleCreateOpenChange}
onCreated={fetchHabits}
/>
</div>
);
}