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
31 changes: 23 additions & 8 deletions middlewareNode/src/routes/activities.js
Original file line number Diff line number Diff line change
Expand Up @@ -140,22 +140,37 @@ router.put("/:username/activity", requireAuth, requireActivityWriteAccess, async
return res.status(404).json({error:'User not found'});
}
const activities = db.collection("activities");
const activityIncomplete = await activities.findOne(
{ userId, "activities.name": activityName },
{ activities: {$elemMatch: { name: activityName }}, _id:0},
);
if(activityIncomplete) {
console.log('incomplete activity: ', activityName);
}

await activities.updateOne(
{ userId, "activities.name": activityName },
{ $set: { "activities.$.completed": true } }
);

// If every daily activity is now complete, record today's date
// (once) in completedDates — used for streak/badge stats.
const doc = await activities.findOne({ userId });
const allCompleted = doc?.activities?.length > 0 &&
doc.activities.every((a) => a.completed === true);

if (allCompleted) {
const today = new Date();
today.setUTCHours(0, 0, 0, 0);
const alreadyRecorded = (doc.completedDates || []).some(
(d) => new Date(d).getTime() === today.getTime()
);
if (!alreadyRecorded) {
await activities.updateOne(
{ userId },
{ $push: { completedDates: today } }
);
}
}

return res.status(200).json({message:'success'});
} catch (err) {
console.error('Error updating activities: ', err);
return res.status(500).json({error: 'Server error'});
}
});

module.exports = router;
module.exports = router;
Original file line number Diff line number Diff line change
Expand Up @@ -188,19 +188,41 @@
}

// ==========================
// Calendar Image Section
// Calendar Grid Section
// ==========================
.calendar-image-wrapper {
display: flex;
justify-content: center;
margin-top: 2rem;

.calendar-image {
.calendar-grid {
width: 55%;
max-width: 37.5rem;
height: auto;
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 0.5rem;
background-color: #fff;
border-radius: 0.75rem;
box-shadow: 0 0.25rem 1rem rgba(0, 0, 0, 0.15);
padding: 1rem;

.calendar-day {
aspect-ratio: 1 / 1;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
font-family: 'Lato', sans-serif;
font-weight: 600;
font-size: 0.9rem;
color: #666;
background-color: #F0F0F0;

&.completed {
background-color: #A8D96C;
color: #fff;
}
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,38 +3,70 @@
*
* Displays a modal showing the user's daily activity streak progress.
* Features animated characters (Stemette and Stemmy) with encouraging messages,
* a visual streak tracker with clock icon, and a weekly calendar view.
* a visual streak tracker with clock icon, and a real calendar view fetched
* from the streak API.
*
* Features:
* - Animated character mascots with speech bubbles
* - Visual streak progress indicator
* - Weekly calendar tracking (placeholder for now)
* - Visual streak progress indicator, fetched from GET /streak
* - Monthly calendar tracking completed days, fetched from GET /streak/calendar
* - Click outside to close functionality
*/

import React from "react";
import React, { useEffect, useState } from "react";
import { useCookies } from "react-cookie";
import "./StreakModal.scss";
import { ReactComponent as Polygon } from "../../../../assets/images/StreakProgressAssets/polygon.svg";
import { ReactComponent as Polygon_2 } from "../../../../assets/images/StreakProgressAssets/polygon_2.svg";
import streakClock from "../../../../assets/images/StreakProgressAssets/streak_progress_clock.png";
import { ReactComponent as Stemette } from "../../../../assets/images/StreakProgressAssets/stemette.svg";
import { ReactComponent as Stemmy } from "../../../../assets/images/StreakProgressAssets/stemmy.svg";

// Calendar placeholder. Delete once an actual calendar is implemented
import calendarIcon from "../../../../assets/images/StreakProgressAssets/Calendar.png";
import { environment } from "../../../../environments/environment";

/**
* StreakModal component - displays user's streak progress
* @param {Function} onClose - Callback to close the modal
* @param {string} username - Username to fetch streak data for
*/
const StreakModal = ({ onClose }: { onClose: () => void }) => {
const StreakModal = ({ onClose, username }: { onClose: () => void; username: string }) => {
const [cookies] = useCookies(["login"]);
const [currentStreak, setCurrentStreak] = useState<number>(0);
const [calendarDays, setCalendarDays] = useState<{ date: string; completed: boolean }[]>([]);
const [loading, setLoading] = useState(true);

// Overlay click handler - closes modal only if clicking outside modal content
const handleOverlayClick = (e: React.MouseEvent<HTMLDivElement>) => {
if (e.target === e.currentTarget) {
onClose();
}
};

useEffect(() => {
const fetchStreakData = async () => {
try {
const streakRes = await fetch(
`${environment.urls.middlewareURL}/streak?username=${username}`,
{ headers: { Authorization: `Bearer ${cookies.login}` } }
);
const streakData = await streakRes.json();
setCurrentStreak(streakData.currentStreak ?? 0);

const month = new Date().toISOString().slice(0, 7); // "YYYY-MM"
const calRes = await fetch(
`${environment.urls.middlewareURL}/streak/calendar?username=${username}&month=${month}`,
{ headers: { Authorization: `Bearer ${cookies.login}` } }
);
const calData = await calRes.json();
setCalendarDays(calData.days || []);
} catch (err) {
console.error("Failed to fetch streak data:", err);
} finally {
setLoading(false);
}
};
fetchStreakData();
}, [username, cookies.login]);

return (
<div className="streak-modal-overlay" onClick={handleOverlayClick}>
<div className="modal-content">
Expand Down Expand Up @@ -66,19 +98,31 @@ const StreakModal = ({ onClose }: { onClose: () => void }) => {
<img src={streakClock} alt="Streak Clock" className="streak-clock" />

<div className="streak-text streak-left">
<p className="big">9</p>
<p className="big">{loading ? "…" : currentStreak}</p>
<p className="small">Day Streak</p>
</div>

<div className="streak-text streak-right">
<p className="small">Today is</p>
<p className="big">6/9</p>
<p className="big">
{new Date().toLocaleDateString("en-US", { month: "numeric", day: "numeric" })}
</p>
</div>
</div>

{/* Calendar placeholder. Delete once an actual calendar is implemented */}
{/* Monthly calendar, fetched from GET /streak/calendar */}
<div className="calendar-image-wrapper">
<img src={calendarIcon} alt="Calendar Icon" className="calendar-image" />
<div className="calendar-grid">
{calendarDays.map((day) => (
<div
key={day.date}
className={`calendar-day ${day.completed ? "completed" : ""}`}
title={day.date}
>
{new Date(day.date).getDate()}
</div>
))}
</div>
</div>
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -689,7 +689,7 @@ const NewStudentProfile = ({ userPortraitSrc }: any) => {
</section>

{/* Modals */}
{activeModal === "streak" && <StreakModal onClose={() => setActiveModal(null)} />}
{activeModal === "streak" && <StreakModal onClose={() => setActiveModal(null)} username={username} />}
{activeModal === "activities" && <ActivitiesModal onClose={() => setActiveModal(null)} username={username} />}
{activeModal === "badges" && <BadgesModal onClose={() => setActiveModal(null)} username={username} />}
{activeModal === "leaderboard" && <LeaderboardModal onClose={() => setActiveModal(null)} />}
Expand Down
Loading