Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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
1 change: 0 additions & 1 deletion src/app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import Link from "next/link";

export default function HomePage() {
return (
<main className="min-h-screen flex flex-col items-center justify-center px-4">
Expand Down
103 changes: 71 additions & 32 deletions src/components/GoalTracker.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"use client";

import { useEffect, useState } from "react";
import Toast from "./Toast";

interface Goal {
id: string;
Expand All @@ -13,18 +14,36 @@ export default function GoalTracker() {
const [goals, setGoals] = useState<Goal[]>([]);
const [loading, setLoading] = useState(true);

const [toastMessage, setToastMessage] = useState("");
const [notifiedGoals, setNotifiedGoals] = useState<string[]>([]);

useEffect(() => {
fetch("/api/goals")
.then((r) => r.json())
.then((data: { goals: Goal[] }) => setGoals(data.goals ?? []))
.catch(() => {})
.then((data: { goals: Goal[] }) => {
setGoals(data.goals ?? []);
})
.catch(() => { })
.finally(() => setLoading(false));
}, []);

useEffect(() => {
goals.forEach((goal) => {
const isComplete = goal.current >= goal.target;
const alreadyNotified = notifiedGoals.includes(goal.id);

if (isComplete && !alreadyNotified) {
setToastMessage(`🎉 Goal complete: ${goal.label}!`);
setNotifiedGoals((prev) => [...prev, goal.id]);
}
});
}, [goals, notifiedGoals]);

if (loading) {
return (
<div className="h-full rounded-xl border border-[var(--border)] bg-[var(--card)] p-6">
<div className="mb-4 h-5 w-32 rounded bg-[var(--card-muted)] animate-pulse" />

{[1, 2, 3].map((i) => (
<div key={i} className="mb-4">
<div className="mb-2 h-3 rounded bg-[var(--card-muted)] animate-pulse" />
Expand All @@ -36,35 +55,55 @@ export default function GoalTracker() {
}

return (
<div className="h-full rounded-xl border border-[var(--border)] bg-[var(--card)] p-6 shadow-sm">
<h2 className="mb-4 text-lg font-semibold text-[var(--card-foreground)]">Weekly Goals</h2>
{goals.length === 0 ? (
<p className="text-sm text-[var(--muted-foreground)]">
No goals yet. Create one via the API or future UI.
</p>
) : (
<ul className="space-y-4">
{goals.map((goal) => {
const pct = Math.min((goal.current / goal.target) * 100, 100);
return (
<li key={goal.id}>
<div className="flex justify-between text-sm mb-1">
<span className="text-[var(--card-foreground)]">{goal.label}</span>
<span className="text-[var(--muted-foreground)]">
{goal.current}/{goal.target}
</span>
</div>
<div className="h-2 overflow-hidden rounded-full bg-[var(--control)]">
<div
className="h-full rounded-full bg-[var(--accent)] transition-all"
style={{ width: `${pct}%` }}
/>
</div>
</li>
);
})}
</ul>
<>
{toastMessage && (
<Toast
message={toastMessage}
onClose={() => setToastMessage("")}
/>
)}
</div>

<div className="h-full rounded-xl border border-[var(--border)] bg-[var(--card)] p-6 shadow-sm">
<h2 className="mb-4 text-lg font-semibold text-[var(--card-foreground)]">
Weekly Goals
</h2>

{goals.length === 0 ? (
<p className="text-sm text-[var(--muted-foreground)]">
No goals yet. Create one via the API or future UI.
</p>
) : (
<ul className="space-y-4">
{goals.map((goal) => {
const pct = Math.min(
(goal.current / goal.target) * 100,
100
);

return (
<li key={goal.id}>
<div className="flex justify-between text-sm mb-1">
<span className="text-[var(--card-foreground)]">
{goal.label}
</span>

<span className="text-[var(--muted-foreground)]">
{goal.current}/{goal.target}
</span>
</div>

<div className="h-2 overflow-hidden rounded-full bg-[var(--control)]">
<div
className="h-full rounded-full bg-[var(--accent)] transition-all"
style={{ width: `${pct}%` }}
/>
</div>
</li>
);
})}
</ul>
)}
</div>
</>
);
}
}
28 changes: 28 additions & 0 deletions src/components/Toast.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
"use client";

import { useEffect } from "react";

interface ToastProps {
message: string;
onClose: () => void;
}

export default function Toast({ message, onClose }: ToastProps) {
useEffect(() => {
const timer = setTimeout(() => {
onClose();
}, 3000);

return () => clearTimeout(timer);
}, [onClose]);

return (
<div className="fixed inset-0 flex justify-center items-start pt-6 pointer-events-none z-50">
<div className="rounded-lg border border-[var(--border)] bg-[var(--card)] px-5 py-3 shadow-lg animate-bounce">
<p className="text-sm font-medium text-[var(--card-foreground)]">
{message}
</p>
</div>
</div>
);
}