Skip to content
77 changes: 65 additions & 12 deletions src/components/dashboard/Dashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@ import FootprintChart from "@/components/charts/FootprintChart";
import ComparisonSection from "@/components/dashboard/ComparisonSection";
import ShareButton from "@/components/ui/ShareButton";
import { getUserFootprints } from "@/lib/firebase/firestore";
import { deleteActivitys } from "@/lib/firebase/firestore";
import { exportToCSV, ActivityHistoryEntry } from "@/utils/exportCSV";
import { ArrowDownTrayIcon } from "@heroicons/react/24/outline";
import ConfirmDialog from "../ui/ConfirmDialog";
import Spinner from "@/components/ui/Spinner";

type SortOption = "newest" | "oldest" | "highest_impact" | "lowest_impact";
Expand Down Expand Up @@ -88,6 +90,7 @@ interface DashboardProps {
onNavigate: (page: PageType) => void;
sortPreference: SortOption;
onSortChange: (sort: SortOption) => void;
onDeleteActivity: (id:string, dateString: string, activities: any, totalCO2: number) => void;
}

export default function Dashboard({
Expand All @@ -96,12 +99,15 @@ export default function Dashboard({
onNavigate,
sortPreference,
onSortChange,
onDeleteActivity
}: DashboardProps) {
const { user } = useAuth();
const [dashboardData, setDashboardData] = useState<DashboardData | null>(
null
);
const [loading, setLoading] = useState(true);
const [confirmOpen, setConfirmOpen] = useState(false);
const [toDeleteEntry, setToDeleteEntry] = useState<any | null>(null);
const [exportStatus, setExportStatus] = useState<{
show: boolean;
success: boolean;
Expand Down Expand Up @@ -214,6 +220,33 @@ export default function Dashboard({
}
}, [propDashboardData]);

const handleDeleteClick = (entry: any) => {
setToDeleteEntry(entry);
setConfirmOpen(true);
};

const handleConfirmDelete = async () => {
if (!toDeleteEntry) return;
setConfirmOpen(false);

try {
if (onDeleteActivity) {
await onDeleteActivity(toDeleteEntry.id, toDeleteEntry.timestamp.toString(), toDeleteEntry.activities, toDeleteEntry.result.totalCO2);
} else {
// fallback: try direct firestore helper
await deleteActivitys?.(toDeleteEntry.id);
}
} catch (err) {
console.error("Error deleting activity", err);
} finally {
setToDeleteEntry(null);
}
};

const handleCancelDelete = () => {
setConfirmOpen(false);
setToDeleteEntry(null);
}
// Handle CSV export
const handleExportCSV = () => {
const result = exportToCSV(activityHistory as ActivityHistoryEntry[]);
Expand Down Expand Up @@ -411,21 +444,41 @@ export default function Dashboard({
+{formatCO2Amount(entry.result.totalCO2)}
</span>
</div>
<div className="flex flex-wrap gap-2">
{Object.entries(entry.activities).map(
([activity, value]) =>
(value as number) > 0 ? (
<span
key={activity}
className="text-xs bg-blue-100 text-blue-700 px-2 py-1 rounded-full"
>
{activity}: {value as number}
</span>
) : null
)}

<div className="flex items-end justify-between">
<div className="flex flex-wrap gap-2">
{Object.entries(entry.activities).map(
([activity, value]) =>
(value as number) > 0 ? (
<span
key={activity}
className="text-xs bg-blue-100 text-blue-700 px-2 py-1 rounded-full"
>
{activity}: {value as number}
</span>
) : null
)}
</div>

{/* delete icon */}
<button
onClick={() => {handleDeleteClick(entry);}}
type="button"
className="ml-4 text-sm text-red-600 bg-red-50 px-2 py-1 rounded-md select-none cursor-pointer z-50"
>
πŸ—‘οΈ
</button>
</div>
</div>
))}
<ConfirmDialog
isOpen={confirmOpen}
title="Delete activity"
message="Delete this activity? This action cannot be undone."
confirmText="Delete"
onConfirm={handleConfirmDelete}
onCancel={handleCancelDelete}
/>
</div>
) : (
// EMPTY STATE FOR ACTIVITY HISTORY
Expand Down
59 changes: 58 additions & 1 deletion src/components/layout/AppLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import GoalsPanel from "@/components/gamification/GoalsPanel";
import BadgeDisplay from "@/components/gamification/BadgeDisplay";
import { ActivityInput } from "@/types";
import { calculateCarbonFootprint } from "@/lib/calculations/carbonFootprint";
import { saveCarbonFootprint, saveActivity } from "@/lib/firebase/firestore";
import { saveCarbonFootprint, saveActivity, deleteActivitys } from "@/lib/firebase/firestore";
import { ShortcutsModal } from "../ui/ShortcutsModal";
import { useKeyboardShortcuts } from "@/hooks/useKeyboardShortcuts";
import QuickActionsFAB from "@/components/ui/QuickActionsFAB";
Expand All @@ -28,6 +28,7 @@ export default function AppLayout() {
const [currentPage, setCurrentPage] = useState<PageType>("dashboard");
const [todayFootprint, setTodayFootprint] = useState(0);
const [successToast, setSuccessToast] = useState<string | null>(null);
const [failToast, setFailToast] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState(false);
const [showSuccess, setShowSuccess] = useState(false);
const [dashboardData, setDashboardData] = useState<any>(null);
Expand Down Expand Up @@ -65,6 +66,52 @@ useEffect(() => {
localStorage.setItem(LOCAL_STORAGE_KEY, newSort);
};

const handleDeleteActivity = async (
id:string,
dateString: string,
activities: any,
totalCO2: number,
customToastMessage?: string
) => {

const newTodayFootprint = todayFootprint - totalCO2;
setTodayFootprint(newTodayFootprint);

// In development mode, simulate saving with shorter delay
if (process.env.NODE_ENV === "development") {
setActivityHistory((prev) => prev.filter((activity) => activity.id !== id));
setSuccessToast(customToastMessage || "Activities saved successfully!");
setTimeout(() => setSuccessToast(null), 3000);
return;
}

try{
// Loop through activities and delete each from Firestore
if (activities) {
const deletionPromises = Object.entries(activities)
.filter(([_, value]) => value as number > 0)
.map(([activityType, _]) =>
deleteActivitys({
rawDateString: dateString,
userId: user!.id,
activityType: activityType
})
);

// Wait for all deletions to finish
await Promise.all(deletionPromises);

// Now update state and show toast
setActivityHistory((prev) => prev.filter((activity) => activity.id !== id));
setSuccessToast(customToastMessage || "Activity deleted successfully!");
setTimeout(() => setSuccessToast(null), 3000);
}
}catch(error){
setTodayFootprint((prev) => prev + totalCO2);
setFailToast(customToastMessage || "An error occurred while deleting activity.");
setTimeout(() => setFailToast(null), 3000);
}
}
const getSortedActivityHistory = () => {
// We create a shallow copy to ensure we don't mutate the original state
const sortedList = [...activityHistory];
Expand Down Expand Up @@ -269,6 +316,7 @@ useEffect(() => {
sortPreference={sortPreference}
onSortChange={handleSortChange}
onNavigate={setCurrentPage}
onDeleteActivity={handleDeleteActivity}
/>
);

Expand Down Expand Up @@ -405,6 +453,15 @@ useEffect(() => {
</div>
)}

{/* Failed Toast */}
{failToast && (
<div className="fixed top-4 right-4 bg-red-500 text-white px-6 py-3 rounded-lg shadow-lg z-50 animate-pulse">
<div className="flex items-center space-x-2">
<span className="text-lg">❌</span>
<span className="font-medium">{failToast}</span>
</div>
</div>
)}
{/* Mobile spacing for bottom navigation */}
<div className="h-16 lg:hidden"></div>

Expand Down
2 changes: 1 addition & 1 deletion src/components/ui/ConfirmDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -151,4 +151,4 @@ export default function ConfirmDialog({
</div>
</div>
);
}
}
31 changes: 31 additions & 0 deletions src/lib/firebase/firestore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,37 @@ export const saveActivity = async (activity: Omit<Activity, 'id'>) => {
});
};

export const deleteActivitys = async (args: {
rawDateString: string;
userId: string;
activityType: string;
}) => {
const { rawDateString, userId, activityType} = args;
// convert incoming date string back into Timestamp to match stored data
const date =new Date(rawDateString);
const start = Timestamp.fromDate(new Date(date)); // start time
const end = Timestamp.fromDate(new Date(date.getTime() + 1000)); // +1 second window

// Query for matching documents (change this in the future if an unique is implemented)
const q = query(
collection(db, "activities"),
where("date", ">=", start),
where("date", "<", end),
where("type", "==", activityType),
where("userId", "==", userId)
);

const snap = await getDocs(q);

const deletes = snap.docs.map(doc => deleteDoc(doc.ref));
await Promise.all(deletes);

if (deletes.length === 0) {
console.error("No matching activity found in DB to delete.");
throw new Error("No matching activity found to delete.");
}
};

export const getUserActivities = async (userId: string, days: number = 30): Promise<Activity[]> => {
const activitiesRef = collection(db, 'activities');
const startDate = new Date();
Expand Down