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
69 changes: 34 additions & 35 deletions .github/workflows/codecov.yml
Original file line number Diff line number Diff line change
@@ -1,41 +1,40 @@
name: Codecov Coverage & Sentry Test Analytics

on:
push:
branches: ['**'] # Run on all branches
pull_request:
branches: ['**'] # Run on PRs to any branch
# on:
# push:
# branches: ['**'] # Run on all branches
# pull_request:
# branches: ['**'] # Run on PRs to any branch

jobs:
test:
runs-on: ubuntu-latest
defaults:
run:
working-directory: app # This ensures all commands run in the /app directory
steps:
- uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: 20
- name: Install dependencies
run: npm ci
- name: Run tests with coverage and JUnit output
run: |
# Install jest-junit if not already available
npm install --save-dev jest-junit
# Run tests with both coverage and JUnit output for Sentry Test Analytics
npx jest --coverage --testResultsProcessor=jest-junit --outputFile=test-results.junit.xml
env:
NODE_ENV: test
continue-on-error: true # Allow flaky tests to not fail the build
- name: Upload test results to Sentry
if: ${{ !cancelled() }}
uses: getsentry/prevent-action
with:
token: ${{ secrets.SENTRY_PREVENT_TOKEN }}
report-type: test-results
files: ./test-results.junit.xml
# permissions:
# id-token: write

# jobs:
# test:
# runs-on: ubuntu-latest
# defaults:
# run:
# working-directory: app # This ensures all commands run in the /app directory
# steps:
# - uses: actions/checkout@v4
# - name: Set up Node.js
# uses: actions/setup-node@v4
# with:
# node-version: 20
# - name: Install dependencies
# run: npm ci
# - name: Run tests with coverage and JUnit output
# run: |
# # Install jest-junit if not already available
# npm install --save-dev jest-junit
# # Run tests with both coverage and JUnit output for Sentry Test Analytics
# npx jest --coverage --testResultsProcessor=jest-junit --outputFile=test-results.junit.xml
# env:
# NODE_ENV: test
# continue-on-error: true # Allow flaky tests to not fail the build
# - name: Upload test results to Sentry
# if: ${{ !cancelled() }}
# uses: getsentry/prevent-action
# - name: Upload coverage reports to Codecov
# uses: codecov/codecov-action@v5
# with:
Expand Down
45 changes: 45 additions & 0 deletions .github/workflows/sentry-test-analytics.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
name: Sentry Test Analytics

on:
push:
branches: ['**'] # Run on all branches
pull_request:
branches: ['**'] # Run on PRs to any branch

permissions:
id-token: write

jobs:
test:
runs-on: ubuntu-latest
defaults:
run:
working-directory: app # This ensures all commands run in the /app directory

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
cache-dependency-path: app/package-lock.json

- name: Install dependencies
run: npm ci

- name: Run tests with JUnit output
run: |
# Install jest-junit if not already available
npm install --save-dev jest-junit
# Run tests with JUnit output for Sentry Test Analytics
npx jest --testResultsProcessor=jest-junit --outputFile=test-results.junit.xml
env:
NODE_ENV: test
continue-on-error: true # Allow flaky tests to not fail the build

- name: Upload test results to Sentry
if: ${{ !cancelled() }}
uses: getsentry/prevent-action
126 changes: 126 additions & 0 deletions app/src/app/api/streak/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import { NextRequest, NextResponse } from 'next/server';
import { getServerSession } from 'next-auth/next';
import { authOptions } from '@/lib/auth.config';
import { PrismaClient } from '@prisma/client';

const prisma = new PrismaClient();

// GET: Calculate and return streak data
export async function GET() {
const session = await getServerSession(authOptions);

if (!session?.user?.email) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}

const user = await prisma.user.findUnique({
where: { email: session.user.email },
});

if (!user) {
return NextResponse.json({ error: 'User not found' }, { status: 404 });
}

try {
// Get current week's activities
const now = new Date();
const startOfWeek = new Date(now);
startOfWeek.setDate(now.getDate() - now.getDay()); // Start of current week (Sunday)
startOfWeek.setHours(0, 0, 0, 0);

const endOfWeek = new Date(startOfWeek);
endOfWeek.setDate(startOfWeek.getDate() + 6); // End of current week (Saturday)
endOfWeek.setHours(23, 59, 59, 999);

// Get activities for current week
const weekActivities = await prisma.activity.findMany({
where: {
userId: user.id,
date: {
gte: startOfWeek,
lte: endOfWeek,
},
},
orderBy: {
date: 'asc',
},
});

// Calculate unique days with activities this week
const uniqueDays = new Set();
weekActivities.forEach(activity => {
const activityDate = new Date(activity.date);
const dateString = activityDate.toISOString().split('T')[0]; // YYYY-MM-DD format
uniqueDays.add(dateString);
Comment on lines +52 to +54
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potential bug: The streak API converts activity dates to UTC using toISOString(), which is inconsistent with other APIs that use local time, potentially causing incorrect streak calculations.
  • Description: The new streak API at /api/streak/route.ts converts activity dates to a UTC date string using activityDate.toISOString().split('T')[0]. This is inconsistent with the established pattern in other endpoints, such as /api/activities/route.ts, which parse dates into local time. This discrepancy can lead to incorrect streak calculations. For example, an activity logged by a user in a timezone with a significant UTC offset late on a Sunday night could be converted to Monday in UTC, causing it to be excluded from the current week's streak calculation and misaligning the user's activity log.

  • Suggested fix: Adopt the established date parsing logic from other API routes. Instead of converting to UTC with toISOString(), ensure the date from the database is treated as local time, similar to the logic in /api/activities/route.ts and /api/meals/route.ts, to ensure consistent date handling across the application.
    severity: 0.75, confidence: 0.95

Did we get this right? 👍 / 👎 to inform future reviews.

});

const daysWithActivity = uniqueDays.size;

// Calculate streak data
const streakData = {
currentWeek: {
daysWithActivity,
totalActivities: weekActivities.length,
totalDuration: weekActivities.reduce((sum, activity) => sum + activity.duration, 0),
totalCalories: weekActivities.reduce((sum, activity) => sum + (activity.calories || 0), 0),
hasStreak: daysWithActivity >= 4, // Streak threshold: 4+ days
streakMessage: daysWithActivity >= 4
? `🔥 Amazing! You've worked out ${daysWithActivity} days this week!`
: daysWithActivity >= 2
? `💪 Great progress! You've worked out ${daysWithActivity} days this week. Keep it up!`
: daysWithActivity >= 1
? `🎯 Good start! You've worked out ${daysWithActivity} day this week.`
: `🚀 Ready to start your fitness journey? Log your first activity!`,
activitiesByDay: weekActivities.reduce((acc, activity) => {
const dateString = new Date(activity.date).toISOString().split('T')[0];
if (!acc[dateString]) {
acc[dateString] = [];
}
acc[dateString].push(activity);
return acc;
}, {} as Record<string, typeof weekActivities>),
},
// Calculate last week for comparison
lastWeek: await calculateLastWeekStreak(user.id, startOfWeek),
};

return NextResponse.json({ streak: streakData });
} catch (error) {
console.error('Error calculating streak:', error);
return NextResponse.json({ error: 'Failed to calculate streak' }, { status: 500 });
}
}

// Helper function to calculate last week's streak
async function calculateLastWeekStreak(userId: string, currentWeekStart: Date) {
const lastWeekStart = new Date(currentWeekStart);
lastWeekStart.setDate(currentWeekStart.getDate() - 7);

const lastWeekEnd = new Date(lastWeekStart);
lastWeekEnd.setDate(lastWeekStart.getDate() + 6);
lastWeekEnd.setHours(23, 59, 59, 999);

const lastWeekActivities = await prisma.activity.findMany({
where: {
userId,
date: {
gte: lastWeekStart,
lte: lastWeekEnd,
},
},
});

const uniqueDays = new Set();
lastWeekActivities.forEach(activity => {
const activityDate = new Date(activity.date);
const dateString = activityDate.toISOString().split('T')[0];
uniqueDays.add(dateString);
});

return {
daysWithActivity: uniqueDays.size,
totalActivities: lastWeekActivities.length,
totalDuration: lastWeekActivities.reduce((sum, activity) => sum + activity.duration, 0),
totalCalories: lastWeekActivities.reduce((sum, activity) => sum + (activity.calories || 0), 0),
};
}
43 changes: 32 additions & 11 deletions app/src/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import Card from "@/components/Card";
import Link from "next/link";
import Button from "@/components/Button";
import Logo from "@/components/Logo";
import StreakBanner from "@/components/StreakBanner";
import { Line, Bar } from "react-chartjs-2";
import {
Chart as ChartJS,
Expand Down Expand Up @@ -79,27 +80,29 @@ interface DashboardData {
};
}

interface DashboardData {
thisWeek: {
totalCalories: number;
avgDailyCalories: number;
totalDuration: number;
interface StreakData {
currentWeek: {
daysWithActivity: number;
daily: DailyStats[];
totalActivities: number;
totalDuration: number;
totalCalories: number;
hasStreak: boolean;
streakMessage: string;
activitiesByDay: Record<string, Activity[]>;
};
lastWeek: {
totalCalories: number;
avgDailyCalories: number;
totalDuration: number;
daysWithActivity: number;
daily: DailyStats[];
totalActivities: number;
totalDuration: number;
totalCalories: number;
};
}

export default function HomePage() {
const [activities, setActivities] = useState<Activity[]>([]);
const [meals, setMeals] = useState<Meal[]>([]);
const [dashboardData, setDashboardData] = useState<DashboardData | null>(null);
const [streakData, setStreakData] = useState<StreakData | null>(null);
const [loading, setLoading] = useState(false);
const [isAuthenticated, setIsAuthenticated] = useState<boolean | null>(null);
const [activeChart, setActiveChart] = useState<'activity' | 'burned' | 'consumed'>('activity');
Expand Down Expand Up @@ -165,10 +168,11 @@ export default function HomePage() {
async function fetchData() {
setLoading(true);
try {
const [activitiesRes, mealsRes, dashboardRes] = await Promise.all([
const [activitiesRes, mealsRes, dashboardRes, streakRes] = await Promise.all([
fetch("/api/activities", { credentials: 'include' }),
fetch("/api/meals", { credentials: 'include' }),
fetch("/api/dashboard", { credentials: 'include' }),
fetch("/api/streak", { credentials: 'include' }),
]);

if (activitiesRes.ok) {
Expand Down Expand Up @@ -197,6 +201,11 @@ export default function HomePage() {
const data = await dashboardRes.json();
setDashboardData(data);
}

if (streakRes.ok) {
const data = await streakRes.json();
setStreakData(data.streak);
}
} catch (error) {
console.error('Error fetching data:', error);
} finally {
Expand Down Expand Up @@ -469,6 +478,18 @@ export default function HomePage() {
{/* Dashboard - Only show when authenticated */}
{isAuthenticated && (
<>
{/* Streak Banner */}
{streakData && (
<StreakBanner
daysWithActivity={streakData.currentWeek.daysWithActivity}
streakMessage={streakData.currentWeek.streakMessage}
hasStreak={streakData.currentWeek.hasStreak}
totalActivities={streakData.currentWeek.totalActivities}
totalDuration={streakData.currentWeek.totalDuration}
totalCalories={streakData.currentWeek.totalCalories}
/>
)}

{/* Summary Stats */}
<Card className="p-6">
<h2 className="text-xl font-bold mb-4 text-fitfest-text dark:text-fitfest-subtle">
Expand Down
Loading