Skip to content
Merged
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
290 changes: 210 additions & 80 deletions src/app/about/page.jsx
Original file line number Diff line number Diff line change
@@ -1,178 +1,308 @@
'use client'

import { useState, useEffect } from 'react';
import Image from 'next/image';
import { useState, useEffect, useRef } from 'react';
import { Container } from '@/components/shared/Container';
import { Banner } from '@/components/shared/Banner';
import { Timeline } from '@/components/about/Timeline';
import { Team } from '@/components/about/Team';
import React from 'react';
import { Line } from 'react-chartjs-2';
import { Chart as ChartJS, LineElement, CategoryScale, LinearScale, PointElement, Tooltip, Filler } from 'chart.js';
import { motion } from 'framer-motion';
import {
Chart as ChartJS,
LineElement,
CategoryScale,
LinearScale,
PointElement,
Tooltip,
Filler,
Legend,
} from 'chart.js';
import { motion, useInView } from 'framer-motion';

ChartJS.register(LineElement, CategoryScale, LinearScale, PointElement, Tooltip, Filler);
ChartJS.register(LineElement, CategoryScale, LinearScale, PointElement, Tooltip, Filler, Legend);

// ── animated counter ──────────────────────────────────────────────────────────
function AnimatedNumber({ target, duration = 1800 }) {
const [display, setDisplay] = useState(0);
const ref = useRef(null);
const inView = useInView(ref, { once: true });

useEffect(() => {
if (!inView) return;
const numeric = parseInt(String(target).replace(/\D/g, ''), 10) || 0;
let start = null;
const step = (ts) => {
if (!start) start = ts;
const progress = Math.min((ts - start) / duration, 1);
const ease = 1 - Math.pow(1 - progress, 3);
setDisplay(Math.floor(ease * numeric));
if (progress < 1) requestAnimationFrame(step);
};
requestAnimationFrame(step);
}, [inView, target, duration]);
Comment on lines +29 to +41
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Missing cleanup for requestAnimationFrame can cause state update on unmounted component.

If the component unmounts during the animation, setDisplay will still be called, potentially causing a React warning. Add a cleanup mechanism using a ref or abort flag.

🛡️ Proposed fix
   useEffect(() => {
     if (!inView) return;
     const numeric = parseInt(String(target).replace(/\D/g, ''), 10) || 0;
     let start = null;
+    let rafId;
+    let cancelled = false;
     const step = (ts) => {
+      if (cancelled) return;
       if (!start) start = ts;
       const progress = Math.min((ts - start) / duration, 1);
       const ease = 1 - Math.pow(1 - progress, 3);
       setDisplay(Math.floor(ease * numeric));
-      if (progress < 1) requestAnimationFrame(step);
+      if (progress < 1) rafId = requestAnimationFrame(step);
     };
-    requestAnimationFrame(step);
+    rafId = requestAnimationFrame(step);
+    return () => {
+      cancelled = true;
+      cancelAnimationFrame(rafId);
+    };
   }, [inView, target, duration]);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
useEffect(() => {
if (!inView) return;
const numeric = parseInt(String(target).replace(/\D/g, ''), 10) || 0;
let start = null;
const step = (ts) => {
if (!start) start = ts;
const progress = Math.min((ts - start) / duration, 1);
const ease = 1 - Math.pow(1 - progress, 3);
setDisplay(Math.floor(ease * numeric));
if (progress < 1) requestAnimationFrame(step);
};
requestAnimationFrame(step);
}, [inView, target, duration]);
useEffect(() => {
if (!inView) return;
const numeric = parseInt(String(target).replace(/\D/g, ''), 10) || 0;
let start = null;
let rafId;
let cancelled = false;
const step = (ts) => {
if (cancelled) return;
if (!start) start = ts;
const progress = Math.min((ts - start) / duration, 1);
const ease = 1 - Math.pow(1 - progress, 3);
setDisplay(Math.floor(ease * numeric));
if (progress < 1) rafId = requestAnimationFrame(step);
};
rafId = requestAnimationFrame(step);
return () => {
cancelled = true;
cancelAnimationFrame(rafId);
};
}, [inView, target, duration]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/about/page.jsx` around lines 29 - 41, The useEffect animation using
requestAnimationFrame (function step) can continue after unmount and call
setDisplay; update the effect to store the request id (e.g., let rafId) and an
"aborted" or "mounted" flag (ref/variable) and in the cleanup return function
call cancelAnimationFrame(rafId) and set the flag so step exits early (avoid
calling setDisplay when aborted/unmounted); reference the existing useEffect,
step, requestAnimationFrame, setDisplay, inView, target, and duration to locate
and fix the logic.


const suffix = String(target).replace(/[0-9]/g, '');
return <span ref={ref}>{display}{suffix}</span>;
}

export default function About() {
const [stats, setStats] = useState({
years: 10,
projects: 203,
contributors: 7600,
graphData: {
labels: ['2016', '2017', '2018', '2019', '2020', '2021', '2022', '2023', '2024'],
data: [4, 8, 12, 9, 9, 11, 8, 6, 18, 22]
}
graphData: {
labels: ['2016', '2017', '2018', '2019', '2020', '2021', '2022', '2023', '2024'],
data: [4, 8, 12, 9, 9, 11, 8, 6, 18, 22],
},
Comment on lines +52 to +55
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Array length mismatch in initial state: 9 labels vs 10 data points.

The initial graphData has 9 labels (2016–2024) but 10 data values. While this fallback state appears unused since the chart uses hardcoded rawData/labels at lines 113-114, keeping it consistent avoids confusion.

🔧 Suggested fix
     graphData: {
-      labels: ['2016', '2017', '2018', '2019', '2020', '2021', '2022', '2023', '2024'],
+      labels: ['2016', '2017', '2018', '2019', '2020', '2021', '2022', '2023', '2024', '2025'],
       data: [4, 8, 12, 9, 9, 11, 8, 6, 18, 22],
     },
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
graphData: {
labels: ['2016', '2017', '2018', '2019', '2020', '2021', '2022', '2023', '2024'],
data: [4, 8, 12, 9, 9, 11, 8, 6, 18, 22],
},
graphData: {
labels: ['2016', '2017', '2018', '2019', '2020', '2021', '2022', '2023', '2024', '2025'],
data: [4, 8, 12, 9, 9, 11, 8, 6, 18, 22],
},
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/about/page.jsx` around lines 52 - 55, The initial graphData fallback
has mismatched arrays (graphData.labels length 9 vs graphData.data length 10);
update the graphData object so labels and data are the same length by either
adding the missing year to graphData.labels (e.g., include the extra year that
corresponds to the extra value) or removing the extra numeric entry from
graphData.data so both arrays align; edit the graphData constant in page.jsx
(the graphData -> labels and data fields) to ensure consistent lengths.

});

// Detect dark mode preference on page load and fetch stats
const [chartKey, setChartKey] = useState(0);
const [isDark, setIsDark] = useState(false);
const chartRef = useRef(null);
const graphRef = useRef(null);
const graphInView = useInView(graphRef, { once: true, margin: '-80px' });

// re-trigger chart animation each time it enters view
useEffect(() => {
if (!graphInView) return;
setChartKey((k) => k + 1);
drawProgress.current = 0;
const start = performance.now();
const duration = 4000;
const easeInOutQuart = (t) => t < 0.5 ? 8 * t * t * t * t : 1 - Math.pow(-2 * t + 2, 4) / 2;
const animate = (now) => {
const t = Math.min((now - start) / duration, 1);
drawProgress.current = easeInOutQuart(t);
chartRef.current?.update('none');
if (t < 1) requestAnimationFrame(animate);
};
requestAnimationFrame(animate);
}, [graphInView]);
Comment on lines +64 to +79
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Missing cleanup for chart animation requestAnimationFrame.

Similar to AnimatedNumber, if the component unmounts during animation, the callback will continue executing. Also, drawProgress is referenced here but declared later at line 144, which is valid JavaScript but reduces readability.

🛡️ Proposed fix
   useEffect(() => {
     if (!graphInView) return;
     setChartKey((k) => k + 1);
     drawProgress.current = 0;
     const start = performance.now();
     const duration = 4000;
     const easeInOutQuart = (t) => t < 0.5 ? 8 * t * t * t * t : 1 - Math.pow(-2 * t + 2, 4) / 2;
+    let rafId;
+    let cancelled = false;
     const animate = (now) => {
+      if (cancelled) return;
       const t = Math.min((now - start) / duration, 1);
       drawProgress.current = easeInOutQuart(t);
       chartRef.current?.update('none');
-      if (t < 1) requestAnimationFrame(animate);
+      if (t < 1) rafId = requestAnimationFrame(animate);
     };
-    requestAnimationFrame(animate);
+    rafId = requestAnimationFrame(animate);
+    return () => {
+      cancelled = true;
+      cancelAnimationFrame(rafId);
+    };
   }, [graphInView]);

Consider moving the drawProgress ref declaration (line 144) closer to this effect for better code locality.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// re-trigger chart animation each time it enters view
useEffect(() => {
if (!graphInView) return;
setChartKey((k) => k + 1);
drawProgress.current = 0;
const start = performance.now();
const duration = 4000;
const easeInOutQuart = (t) => t < 0.5 ? 8 * t * t * t * t : 1 - Math.pow(-2 * t + 2, 4) / 2;
const animate = (now) => {
const t = Math.min((now - start) / duration, 1);
drawProgress.current = easeInOutQuart(t);
chartRef.current?.update('none');
if (t < 1) requestAnimationFrame(animate);
};
requestAnimationFrame(animate);
}, [graphInView]);
// re-trigger chart animation each time it enters view
useEffect(() => {
if (!graphInView) return;
setChartKey((k) => k + 1);
drawProgress.current = 0;
const start = performance.now();
const duration = 4000;
const easeInOutQuart = (t) => t < 0.5 ? 8 * t * t * t * t : 1 - Math.pow(-2 * t + 2, 4) / 2;
let rafId;
let cancelled = false;
const animate = (now) => {
if (cancelled) return;
const t = Math.min((now - start) / duration, 1);
drawProgress.current = easeInOutQuart(t);
chartRef.current?.update('none');
if (t < 1) rafId = requestAnimationFrame(animate);
};
rafId = requestAnimationFrame(animate);
return () => {
cancelled = true;
cancelAnimationFrame(rafId);
};
}, [graphInView]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/about/page.jsx` around lines 64 - 79, The effect that re-triggers
chart animation (useEffect) starts a requestAnimationFrame loop via animate but
does not cancel it on unmount or when graphInView changes; move the drawProgress
ref declaration (drawProgress) next to this useEffect for locality and add a
cleanup that cancels the scheduled animationFrame (store the id from
requestAnimationFrame and call cancelAnimationFrame in the returned cleanup).
Ensure chartRef.update('none') and setChartKey usages remain unchanged but the
animate loop is properly torn down to avoid callbacks running after unmount or
dependency changes.


// track dark mode for tick label colors
useEffect(() => {
const html = document.documentElement;
const update = () => setIsDark(html.classList.contains('dark'));
update();
const observer = new MutationObserver(update);
observer.observe(html, { attributes: true, attributeFilter: ['class'] });
return () => observer.disconnect();
}, []);

useEffect(() => {
const fetchStats = async () => {
try {
const res = await fetch('/api/stats');
const data = await res.json();
if (!data.error) {
setStats(data);
}
} catch (error) {
console.error('Failed to fetch stats:', error);
const json = await res.json();
if (!json.error) setStats(json);
} catch (e) {
console.error('Failed to fetch stats:', e);
}
};

fetchStats();
}, []);

// ── gradient fill factory ────────────────────────────────────────────────
const getGradient = (ctx, chartArea) => {
const gradient = ctx.createLinearGradient(0, chartArea.top, 0, chartArea.bottom);
gradient.addColorStop(0, 'rgba(0,132,61,0.35)');
gradient.addColorStop(0.5, 'rgba(0,132,61,0.12)');
gradient.addColorStop(1, 'rgba(0,132,61,0.0)');
return gradient;
};

const rawData = [4, 8, 12, 9, 9, 11, 8, 6, 18, 22];
const labels = ['2016', '2017', '2018', '2019', '2020', '2021', '2022', '2023', '2024', '2025'];
Comment on lines +113 to +114
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Fetched graphData from API is unused; chart uses hardcoded data instead.

The API response contains dynamic graphData with labels and data (see lines 95-96), but the chart uses hardcoded rawData and labels constants. This means the chart won't reflect actual project counts from the API.

🔧 Suggested fix to use API data
-  const rawData = [4, 8, 12, 9, 9, 11, 8, 6, 18, 22];
-  const labels  = ['2016', '2017', '2018', '2019', '2020', '2021', '2022', '2023', '2024', '2025'];
+  const { labels, data: rawData } = stats.graphData;

   const data = {
     labels,
     datasets: [
       {
         label: 'Completed Projects',
         data: rawData,
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const rawData = [4, 8, 12, 9, 9, 11, 8, 6, 18, 22];
const labels = ['2016', '2017', '2018', '2019', '2020', '2021', '2022', '2023', '2024', '2025'];
const { labels, data: rawData } = stats.graphData;
const data = {
labels,
datasets: [
{
label: 'Completed Projects',
data: rawData,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/about/page.jsx` around lines 113 - 114, Replace the hardcoded chart
constants so the chart uses the fetched API response: stop using rawData and the
hardcoded labels and instead read graphData.labels and graphData.data (or
graphData?.labels / graphData?.data) when rendering the chart; update the
variables referenced in the chart component (rawData, labels) to use graphData
values with a safe fallback (e.g., empty arrays) so chart rendering won’t break
if graphData is undefined; ensure the component that originally declared
rawData/labels now maps to graphData from your fetch (refer to the graphData
variable and wherever rawData/labels are passed into the Chart).


const data = {
labels: ['2016', '2017', '2018', '2019', '2020', '2021', '2022', '2023', '2024', '2025'], // Include '0' on the x-axis
labels,
datasets: [
{
label: 'Number of Completed Projects',
data: [4, 8, 12, 9, 9, 11, 8, 6, 18, 22], // Start data points from '2017', leave '0' as null
fill: false,
borderColor: '#32a852',
tension: 0.4,
label: 'Completed Projects',
data: rawData,
fill: true,
backgroundColor: (ctx) => {
const chart = ctx.chart;
const { ctx: c, chartArea } = chart;
if (!chartArea) return 'transparent';
return getGradient(c, chartArea);
},
borderColor: '#00843D',
borderWidth: 4,
tension: 0.45,
pointRadius: 6,
pointHoverRadius: 9,
pointBackgroundColor: '#fff',
pointBorderColor: '#00843D',
pointBorderWidth: 2.5,
pointHoverBackgroundColor: '#00843D',
pointHoverBorderColor: '#fff',
pointHoverBorderWidth: 3,
},
],
};

const drawProgress = useRef(0);

const leftToRightPlugin = {
id: 'leftToRight',
beforeDatasetsDraw(chart) {
const { ctx, chartArea } = chart;
if (!chartArea) return;
const { left, right, top, bottom } = chartArea;
const clipX = left + (right - left) * drawProgress.current;
ctx.save();
ctx.beginPath();
ctx.rect(left, top, clipX - left, bottom - top);
ctx.clip();
},
afterDatasetsDraw(chart) {
chart.ctx.restore();
},
};

const options = {
responsive: true,
maintainAspectRatio: false,
animation: false,
interaction: { mode: 'index', intersect: false },
plugins: {
legend: {
display: false,
legend: { display: false },
tooltip: {
backgroundColor: 'rgba(24,24,27,0.92)',
titleColor: '#00843D',
bodyColor: '#e4e4e7',
borderColor: '#00843D',
borderWidth: 1,
padding: 12,
cornerRadius: 10,
displayColors: false,
titleFont: { size: 13, weight: 'bold', family: 'monospace' },
bodyFont: { size: 14, family: 'monospace' },
callbacks: {
title: ([item]) => `Year: ${item.label}`,
label: (item) => ` Projects: ${item.parsed.y}`,
},
},
},
scales: {
x: {
type: 'category',
grid: {
display: true,
color: '#FFCC00',
},
ticks: {
callback: (value, index) => data.labels[index], // Match x-axis labels
},
grid: { color: 'rgba(254,212,30,0.25)', lineWidth: 3, drawBorder: false },
ticks: { color: isDark ? '#ffffff' : '#18181b', font: { size: 12, weight: 'normal', family: 'monospace' } },
border: { display: false },
},
y: {
beginAtZero: true, // Start y-axis from 0
beginAtZero: true,
ticks: {
stepSize: 5, // Increment y-axis labels by 5
},
grid: {
display: true,
color: '#FFCC00',
stepSize: 5,
color: isDark ? '#ffffff' : '#18181b',
font: { size: 12, weight: 'normal', family: 'monospace' },
},
grid: { color: 'rgba(254,212,30,0.25)', lineWidth: 3, drawBorder: false },
border: { display: false },
},
},
};

return (
return (
<>
<Container className="mt-4 sm:mt-16 mb-20">
<div className="max-w-4xl mx-auto px-4 text-center">

{/* intro text */}
<div className="my-8">
<motion.p
<motion.p
className="text-base md:text-lg leading-relaxed text-zinc-600 dark:text-zinc-400 font-mono mt-5 mb-10 text-center"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.8, delay: 0.2 }}
>
<span className="text-[#32a852] font-bold">AOSSIE</span> (Australian Open
Source Software Innovation and Education) is a not-for-profit
organization dedicated to project-based innovation-focused and
organization dedicated to project-based innovation-focused and
research-intensive education. Our projects are free and open-source.
</motion.p>
</div>

<div className="my-8 space-y-12">
<motion.div
className="w-full h-[300px] md:h-[400px]"
initial={{ opacity: 0, scale: 0.95 }}
whileInView={{ opacity: 1, scale: 1 }}
viewport={{ once: true }}
transition={{ duration: 0.5 }}

{/* ── graph ──────────────────────────────────────────────────── */}
<motion.div
ref={graphRef}
initial={{ opacity: 0, y: 40 }}
animate={graphInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.7, ease: 'easeOut' }}
className="w-full"
>
<Line data={data} options={options} />

{/* chart */}
<div className="w-full h-[280px] md:h-[360px]">
<Line key={chartKey} ref={chartRef} data={data} options={options} plugins={[leftToRightPlugin]} />
</div>

<p className="mt-3 text-center text-xs font-mono font-bold text-zinc-600 dark:text-zinc-300 tracking-widest uppercase">
Year
</p>
</motion.div>

<motion.div

{/* ── stats cards ────────────────────────────────────────────── */}
<motion.div
className="flex flex-wrap justify-around gap-6 mt-8"
initial="hidden"
whileInView="show"
viewport={{ once: true }}
variants={{
hidden: { opacity: 0 },
show: {
opacity: 1,
transition: {
staggerChildren: 0.2
}
}
show: { opacity: 1, transition: { staggerChildren: 0.15 } },
}}
>
{/* Stats Cards */}
{[
{ value: stats.years, label: 'years' },
{ value: stats.projects, label: 'projects' },
{ value: 203, label: 'repos' },
{ value: '88', label: 'mentors' },
{ value: `${stats.contributors}+`, label: 'contributors' },
{ value: '7500+', label: 'community members' }
].map((item, index) => (
{[
{ value: stats.years, label: 'years' },
{ value: stats.projects, label: 'projects' },
{ value: 203, label: 'repos' },
{ value: '88', label: 'mentors' },
{ value: `${stats.contributors}+`, label: 'contributors' },
{ value: '7500+', label: 'community members' },
].map((item, index) => (
<motion.div
key={index}
variants={{
hidden: { opacity: 0, y: 20 },
show: { opacity: 1, y: 0 }
}}
className="bg-white dark:bg-zinc-800 p-6 rounded-xl shadow-lg w-full sm:w-48 lg:w-56 cursor-pointer transform hover:scale-105 transition-transform duration-300"
variants={{ hidden: { opacity: 0, y: 20 }, show: { opacity: 1, y: 0 } }}
whileHover={{ scale: 1.06, y: -4 }}
className="bg-white dark:bg-zinc-800 p-6 rounded-xl shadow-lg
border border-zinc-100 dark:border-zinc-700
w-full sm:w-48 lg:w-56 cursor-pointer
transition-shadow duration-300
hover:shadow-[#00843D]/20 dark:hover:shadow-[#FED41E]/20"
>
<div className="text-4xl font-bold text-[#32a852]">{item.value}</div>
<div className="text-lg text-zinc-600 dark:text-zinc-400 mt-1">{item.label}</div>
<div className="text-4xl font-bold font-mono text-[#00843D] dark:text-[#FED41E]">
<AnimatedNumber target={item.value} />
</div>
<div className="text-sm text-zinc-500 dark:text-zinc-400 mt-1 font-mono uppercase tracking-wider">
{item.label}
</div>
</motion.div>
))}
))}
</motion.div>
</div>
</div>

<motion.div
<motion.div
className="mt-20"
initial={{ opacity: 0 }}
whileInView={{ opacity: 1 }}
viewport={{ once: true }}
transition={{ duration: 0.8 }}
>
<Timeline />
<Timeline />
</motion.div>
<motion.div

<motion.div
className="mt-24 mb-24"
initial={{ opacity: 0 }}
whileInView={{ opacity: 1 }}
viewport={{ once: true }}
transition={{ duration: 0.8 }}
>
<Team />
<Team />
</motion.div>
</Container>
</>
Expand Down