Skip to content

Commit

Permalink
working on macros bar
Browse files Browse the repository at this point in the history
  • Loading branch information
trevorpfiz committed Jan 17, 2024
1 parent dceec4f commit 7d51e67
Show file tree
Hide file tree
Showing 13 changed files with 776 additions and 8 deletions.
5 changes: 5 additions & 0 deletions apps/nextjs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
"with-env": "dotenv -e ../../.env --"
},
"dependencies": {
"@headlessui/tailwindcss": "^0.2.0",
"@heroicons/react": "^2.1.1",
"@nourish/api": "workspace:^0.1.0",
"@nourish/auth": "workspace:^0.1.0",
"@nourish/db": "workspace:^0.1.0",
Expand All @@ -27,10 +29,13 @@
"@trpc/client": "next",
"@trpc/react-query": "next",
"@trpc/server": "next",
"chart.js": "^4.4.1",
"date-fns": "^3.2.0",
"geist": "^1.2.0",
"lucide-react": "^0.309.0",
"next": "^14.0.4",
"react": "18.2.0",
"react-chartjs-2": "^5.2.0",
"react-dom": "18.2.0",
"react-icons": "^5.0.0",
"superjson": "2.2.1",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import { AuthShowcase } from "~/app/_components/auth-showcase";
import { MacrosBar } from "~/components/dashboard/macros-bar";
import { MacrosDonut } from "~/components/dashboard/macros-donut";
import { MacrosProgress } from "~/components/dashboard/macros-progress";
import { MealCard } from "~/components/dashboard/meal-card";
import { MicrosProgress } from "~/components/dashboard/micros-progress";
import { Tabs } from "~/components/dashboard/tabs";
import { TopNavbar } from "~/components/dashboard/top-navbar";

Expand All @@ -12,17 +17,19 @@ export default async function HomePage() {
<TopNavbar />

{/* Content */}
<main className="relative h-full w-full flex-1 overflow-auto px-4">
<main className="relative flex h-full w-full flex-1 flex-col items-center gap-2 overflow-auto px-4">
{/* Macros donut chart */}

<MacrosDonut />
{/* Macros progress charts */}

<MacrosProgress />
{/* Macros bar chart */}

<MacrosBar />
{/* Meal and snack cards */}

<MealCard />
{/* Micros progress charts */}
<MicrosProgress />

{/* TODO - remove */}
<AuthShowcase />
</main>

Expand Down
109 changes: 109 additions & 0 deletions apps/nextjs/src/components/dashboard/macros-bar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
"use client";

import { BarChart, Card, Subtitle, Title } from "@tremor/react";
import {
addMinutes,
closestIndexTo,
format,
parseISO,
startOfDay,
} from "date-fns";

// Define a type for meal data
interface MealData {
time: string; // ISO string for meal time
Fat: number;
Carbs: number;
Protein: number;
}

// Meal data with specific ISO times
const sampleMeals: MealData[] = [
{ time: "2023-11-09T07:01:00Z", Fat: 300, Carbs: 200, Protein: 300 }, // Breakfast
{ time: "2023-11-09T011:31:00Z", Fat: 100, Carbs: 500, Protein: 200 }, // Lunch
{ time: "2023-11-09T14:01:00Z", Fat: 20, Carbs: 35, Protein: 25 }, // Snack
{ time: "2023-11-09T18:01:00Z", Fat: 400, Carbs: 300, Protein: 500 }, // Dinner
// ... other meals
];

// Generate fixed times for every 30 minutes in a day
const generateFixedTimes = (baseDate: Date): Date[] => {
const startTime: Date = startOfDay(baseDate); // Start at midnight of the current day
const fixedTimes: Date[] = [];
for (let i = 0; i < 48; i++) {
// 24 hours * 2 slots per hour
fixedTimes.push(addMinutes(startTime, i * 30));
}
return fixedTimes;
};

// Function to round time to the nearest 30-minute window
const getNearestHalfHour = (date: Date): string => {
const minutes = date.getMinutes();
const roundedMinutes = minutes >= 30 ? 30 : 0;
const roundedDate = new Date(date);
roundedDate.setMinutes(roundedMinutes, 0, 0); // Set seconds and milliseconds to 0 for consistency

return format(roundedDate, "HH:mm");
};

const date: Date = new Date(); // Your sample date
const fixedHalfHours: Date[] = generateFixedTimes(date);

// Convert fixed half-hours to strings for chartData
const fixedTimeStrings: string[] = fixedHalfHours.map((time) =>
format(time, "HH:mm"),
);

// Initialize chart data with times as strings and zeroed macronutrients
const chartData: MealData[] = fixedTimeStrings.map((time) => ({
time: time, // Use the string representation of time
Fat: 0,
Carbs: 0,
Protein: 0,
}));

sampleMeals.forEach((meal) => {
const mealTime: Date = parseISO(meal.time);
const roundedTime: string = getNearestHalfHour(mealTime, fixedHalfHours); // Pass the array of fixed Date objects
const dataIndex: number = chartData.findIndex(
(data) => data.time === roundedTime,
);

if (dataIndex !== -1) {
chartData[dataIndex].Fat += meal.Fat;
chartData[dataIndex].Carbs += meal.Carbs;
chartData[dataIndex].Protein += meal.Protein;
} else {
// If there is no matching time slot, log an error or handle appropriately
console.error(
`Meal time ${roundedTime} does not match any fixed time slot.`,
);
}
});

const valueFormatter = (value) => `${value}`;

const MacrosBar = () => (
<Card className="px-1">
<Title>Macronutrient Ratio per Meal</Title>
<Subtitle>Daily intake ratios for fat, carbs, and protein.</Subtitle>
<BarChart
className="mt-6"
data={chartData}
index="time"
categories={["Fat", "Carbs", "Protein"]}
colors={["amber", "violet", "lime"]} // Gold for Fat, Turquoise for Carbs, YellowGreen for Protein
stack={true}
valueFormatter={valueFormatter}
yAxisWidth={48}
intervalType={"preserveStartEnd"} // This ensures that the first and last ticks are shown
showXAxis={true}
showYAxis={true}
showAnimation={true}
// Additional properties like animationDuration, showTooltip, etc., can be set as required.
/>
</Card>
);

export { MacrosBar };
90 changes: 90 additions & 0 deletions apps/nextjs/src/components/dashboard/macros-donut-cods.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
"use client";

import React, { useEffect, useRef } from "react";

const CircularProgressBar = ({ macros }) => {
const svgRef = useRef(null);
const totalCalories = macros.reduce((acc, macro) => acc + macro.calories, 0);

useEffect(() => {
const progressElements = svgRef.current.querySelectorAll(".progress");
let cumulativePercent = 0;

progressElements.forEach((progress, index) => {
const percentage = (macros[index].calories / totalCalories) * 100;
const radius = progress.r.baseVal.value;
const circumference = 2 * Math.PI * radius;

// Calculate the stroke dash offset for this segment
const offset = circumference - (circumference * percentage) / 100;

// The starting point for the next segment
cumulativePercent += percentage;

// Set the initial styles for the stroke dash array and offset
progress.style.strokeDasharray = `${circumference}`;
progress.style.strokeDashoffset = `${circumference}`;

// Set the transition property
progress.style.transition = `stroke-dashoffset ${macros[index].animateTime}s ease-in forwards`;

// Set the timeout to trigger the animation
setTimeout(() => {
progress.style.strokeDashoffset = `${offset}`;
}, 0);
});
}, [macros, totalCalories]);

return (
<div className="relative">
<svg
ref={svgRef}
className="h-80 w-80"
viewBox="0 0 200 200"
style={{ transform: "rotate(-90deg)" }}
>
{macros.map((macro, index) => (
<circle
key={macro.name}
className="progress"
cx="100"
cy="100"
r="70"
fill="none"
stroke={macro.color}
strokeWidth="15"
// Inline styles are set in the useEffect hook
/>
))}
</svg>
</div>
);
};

const MacrosComponent = () => {
const macros = [
{
name: "Fat",
calories: 500,
color: "#F59E0B", // amber
animateTime: 1, // Animation time in seconds
},
{
name: "Carbs",
calories: 500,
color: "#7C3AED", // violet
animateTime: 1,
},
{
name: "Protein",
calories: 1000,
color: "#10B981", // lime
animateTime: 1,
},
// You can add "Remaining" if you want to show it as well
];

return <CircularProgressBar macros={macros} />;
};

export { MacrosComponent };
Loading

0 comments on commit 7d51e67

Please sign in to comment.