Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
d9cee62
Progress updates revamp
TylerDiorio Jul 23, 2025
231a7b5
Include 1st month open as possible for updates
TylerDiorio Jul 23, 2025
a15a694
Better UI for # updates per month
TylerDiorio Jul 23, 2025
de9ec70
Soften color of # updates & fix width of month box
TylerDiorio Jul 23, 2025
f4f1198
use grays to color # updates
TylerDiorio Jul 23, 2025
287eacc
try green
TylerDiorio Jul 23, 2025
3af85a2
try darker
TylerDiorio Jul 23, 2025
3f31159
nav to updates when clicking a month
TylerDiorio Jul 23, 2025
f0dffb6
Revert "nav to updates when clicking a month"
TylerDiorio Jul 23, 2025
2e156e9
Fix sonarcloud issues
TylerDiorio Jul 23, 2025
8eda8f7
Lighten color on # updates
TylerDiorio Jul 23, 2025
9a731f9
try alt design on # updates
TylerDiorio Jul 24, 2025
56d96fb
use x3 and bigger month text
TylerDiorio Jul 24, 2025
0182749
lighten x3
TylerDiorio Jul 24, 2025
f1199d8
green 600
TylerDiorio Jul 24, 2025
292a466
refactor: Centralize timeline start date logic into helper function
TylerDiorio Jul 29, 2025
09b894c
Merge branch 'main' into improve/progress-updates
TylerDiorio Jul 29, 2025
b754f44
Merge branch 'main' into improve/progress-updates
TylerDiorio Jul 31, 2025
0aa161a
Merge branch 'main' into improve/progress-updates
TylerDiorio Aug 4, 2025
fbadd14
Merge branch 'main' into improve/progress-updates
TylerDiorio Aug 11, 2025
b9a1c90
Remove update %, change green -> gray, underline = active
TylerDiorio Aug 11, 2025
4ba5df0
Update ProgressUpdates.tsx
TylerDiorio Aug 11, 2025
aec3136
entire month/year button is clickable, not just underline
TylerDiorio Aug 11, 2025
9a9a7ec
try vertical
TylerDiorio Aug 11, 2025
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
74 changes: 51 additions & 23 deletions components/Fund/lib/FundUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,35 +4,63 @@ interface Update {
content?: any;
}

interface FundraisingMetadata {
startDate?: string;
endDate?: string;
}

interface WorkData {
createdDate: string;
}

/**
* Calculate the update rate as a percentage of months with updates in the last 12 months
* Only the first update in each month counts towards the rate
* @param updates - Array of updates with createdDate
* @returns Percentage (0-100) representing how many months had updates
* Determine the start date for when updates can begin posting
* @param fundraising - Fundraising metadata object
* @param work - Work object with creation date
* @returns ISO date string for when updates can start
*/
export const calculateUpdateRate = (updates: Update[]): number => {
if (updates.length === 0) {
return 0;
export const getUpdatesStartDate = (
fundraising?: FundraisingMetadata | null,
work?: WorkData
): string => {
// Always use fundraise start date if available
if (fundraising?.startDate) {
return fundraising.startDate;
}
// Fallback to endDate minus 1 month if we have endDate
if (fundraising?.endDate) {
const endDate = new Date(fundraising.endDate);
endDate.setMonth(endDate.getMonth() - 1);
return endDate.toISOString();
}
// Final fallback to work creation date
return work?.createdDate || new Date().toISOString();
};

/**
* Determines the normalized start date for a timeline.
* @param startDate - Optional preferred start date string.
* @param updates - Array of updates, used as a fallback.
* @returns A normalized Date object for the start of the timeline.
*/
export const getTimelineStartDate = (startDate?: string, updates: Update[] = []): Date => {
const now = new Date();
const updatesInLast12Months = updates.filter((update) => {
const updateDate = new Date(update.createdDate);
const monthsDiff =
(now.getFullYear() - updateDate.getFullYear()) * 12 +
(now.getMonth() - updateDate.getMonth());
return monthsDiff <= 12;
});

// Group updates by month-year and only count unique months
const uniqueMonths = new Set<string>();
if (startDate) {
const startDateObj = new Date(startDate);
// Normalize to the beginning of the start month to ensure we include the full month
return new Date(startDateObj.getFullYear(), startDateObj.getMonth(), 1);
}

updatesInLast12Months.forEach((update) => {
const updateDate = new Date(update.createdDate);
const monthYear = `${updateDate.getFullYear()}-${updateDate.getMonth()}`;
uniqueMonths.add(monthYear);
});
if (updates.length > 0) {
// Find the earliest update
const earliestUpdate = updates.reduce((earliest, update) => {
const updateDate = new Date(update.createdDate);
return updateDate < earliest ? updateDate : earliest;
}, new Date(updates[0].createdDate));
return new Date(earliestUpdate.getFullYear(), earliestUpdate.getMonth(), 1);
}

// Calculate percentage based on unique months with updates out of 12 months
return Math.round((uniqueMonths.size / 12) * 100);
// Default to 3 months ago
return new Date(now.getFullYear(), now.getMonth() - 2, 1);
};
134 changes: 52 additions & 82 deletions components/ui/ProgressUpdates.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
'use client';

import React from 'react';
import { useRouter, usePathname } from 'next/navigation';
import { getTimelineStartDate } from '@/components/Fund/lib/FundUtils';

interface Update {
id: number;
Expand All @@ -8,22 +12,38 @@ interface Update {

interface ProgressUpdatesProps {
updates: Update[];
startDate?: string; // When updates can start being posted (e.g., fundraise start date)
className?: string;
}

export const ProgressUpdates: React.FC<ProgressUpdatesProps> = ({
updates = [],
startDate,
className = '',
}) => {
// Generate exactly 12 months starting from current date
const router = useRouter();
const pathname = usePathname();

const navigateToUpdatesTab = () => {
if (!pathname) return;
const basePath = pathname.replace(
/\/(updates|conversation|applications|reviews|bounties|history)$/i,
''
);
const target = `${basePath}/updates`;
router.push(target);
};
// Generate timeline from startDate to current date
const generateTimeline = () => {
const timeline = [];
const now = new Date();
const startDate = new Date(now.getFullYear(), now.getMonth(), 1); // Start from current month
const current = new Date(startDate);
let monthCount = 0;

while (monthCount < 12) {
const start = getTimelineStartDate(startDate, updates);
const current = new Date(start);
const currentMonthStart = new Date(now.getFullYear(), now.getMonth(), 1);

// Generate months from start to current month (inclusive)
while (current <= currentMonthStart) {
const monthYear = `${current.getFullYear()}-${String(current.getMonth() + 1).padStart(2, '0')}`;
const monthName = current.toLocaleDateString('en-US', { month: 'short' });
const year = current.getFullYear();
Expand All @@ -45,102 +65,52 @@ export const ProgressUpdates: React.FC<ProgressUpdatesProps> = ({
});

current.setMonth(current.getMonth() + 1);
monthCount++;
}

return timeline;
};

const timeline = generateTimeline();

// Don't render if timeline is empty
if (timeline.length === 0) {
return null;
}

return (
<div className={className}>
{/* Monthly Timeline */}
<div className="flex flex-wrap gap-1 mb-3">
<div className="flex flex-col gap-1 mb-3">
{timeline.map((month) => {
const now = new Date();
const monthDate = new Date(month.year, new Date(`${month.monthName} 1, 2000`).getMonth());
const currentMonthStart = new Date(now.getFullYear(), now.getMonth(), 1);
const isInPast = monthDate < currentMonthStart;
const isPastWithoutUpdates = isInPast && !month.hasUpdate;
const monthText = `${month.monthName} ${String(month.year).slice(-2)}`;

return (
<div
key={month.monthYear}
className={`
relative p-1.5 rounded-md border text-center transition-all flex-shrink-0 w-11
${
month.hasUpdate
? 'bg-green-50 border-green-200 text-green-700 hover:bg-green-100'
: 'bg-white border-gray-200 text-gray-600 hover:bg-gray-50'
}
`}
title={
month.hasUpdate
? `${month.monthName} ${month.year} - ${month.updateCount} update${month.updateCount > 1 ? 's' : ''}`
: `${month.monthName} ${month.year} - No updates`
}
>
{/* Diagonal lines for past months without updates */}
{isPastWithoutUpdates && (
<div
className="absolute inset-0 pointer-events-none opacity-30"
style={{
backgroundImage: `repeating-linear-gradient(
-45deg,
transparent 0px,
transparent 3px,
#d1d5db 3px,
#d1d5db 6px,
transparent 6px,
transparent 9px
)`,
}}
/>
<div key={month.monthYear} className="whitespace-nowrap">
{month.hasUpdate ? (
<button
type="button"
onClick={navigateToUpdatesTab}
className="text-sm font-medium underline text-gray-700 hover:text-gray-900"
aria-label={`View updates for ${month.monthName} ${month.year}`}
title={`${month.monthName} ${month.year} - ${month.updateCount} update${month.updateCount > 1 ? 's' : ''}`}
>
{monthText}
</button>
) : (
<span
className="text-sm font-medium text-gray-500"
title={`${month.monthName} ${month.year} - No updates`}
>
{monthText}
</span>
)}

<div className="text-xs font-medium">{month.monthName}</div>
<div className="text-xs text-gray-500 leading-none">
{month.year.toString().slice(-2)}
</div>

{/* Update Count Badge - Top Right Corner */}
{month.hasUpdate && (
<div className="absolute -top-1 -right-1 bg-green-600 text-white text-xs rounded-full min-w-[14px] h-3.5 flex items-center justify-center leading-none px-1 z-10">
{month.updateCount}
</div>
{month.updateCount > 1 && (
<span className="text-xs ml-2 text-gray-500">x {month.updateCount}</span>
)}
</div>
);
})}
</div>

{/* Compact Legend */}
<div className="flex items-center gap-4 text-xs text-gray-600">
<div className="flex items-center gap-1">
<div className="w-3 h-3 bg-green-50 border border-green-200 rounded"></div>
<span>Has updates</span>
</div>
<div className="flex items-center gap-1">
<div
className="relative w-3 h-3 bg-white border border-gray-200 rounded"
style={{
backgroundImage: `repeating-linear-gradient(
-45deg,
transparent 0px,
transparent 3px,
#d1d5db 3px,
#d1d5db 6px,
transparent 6px,
transparent 9px
)`,
backgroundBlendMode: 'multiply',
opacity: 0.6,
}}
></div>
<span>No updates</span>
</div>
</div>
</div>
);
};
30 changes: 0 additions & 30 deletions components/ui/badges/UpdateRateBadge.tsx

This file was deleted.

7 changes: 2 additions & 5 deletions components/work/FundDocument.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,9 @@ import { PostBlockEditor } from './PostBlockEditor';
import { FundraiseProgress } from '@/components/Fund/FundraiseProgress';
import { ProgressUpdates } from '@/components/ui/ProgressUpdates';
import { useStorageKey } from '@/utils/storageKeys';
import { calculateUpdateRate } from '@/components/Fund/lib/FundUtils';
import { getUpdatesStartDate } from '@/components/Fund/lib/FundUtils';
import { FundingRightSidebar } from './FundingRightSidebar';
import { useUser } from '@/contexts/UserContext';
import { UpdateRateBadge } from '@/components/ui/badges/UpdateRateBadge';
import { EarningOpportunityBanner } from '@/components/banners/EarningOpportunityBanner';
import { useShareModalContext } from '@/contexts/ShareContext';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
Expand Down Expand Up @@ -121,16 +120,14 @@ export const FundDocument = ({
Track updates made by authors about their research progress.
</p>
</div>
<div className="flex items-center gap-2 ml-4">
<UpdateRateBadge updateRate={calculateUpdateRate(authorUpdates)} />
</div>
</div>
<ProgressUpdates
updates={authorUpdates.map((update) => ({
id: update.id,
createdDate: update.createdDate,
content: update.content,
}))}
startDate={getUpdatesStartDate(metadata.fundraising, work)}
/>
</div>

Expand Down
3 changes: 2 additions & 1 deletion components/work/FundingRightSidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { NonprofitSection } from './components/NonprofitSection';
import { FundersSection } from './components/FundersSection';
import { ApplicantsSection } from './components/ApplicantsSection';
import { UpdatesSection } from './components/UpdatesSection';
import { getUpdatesStartDate } from '@/components/Fund/lib/FundUtils';

interface FundingRightSidebarProps {
work: Work;
Expand Down Expand Up @@ -37,7 +38,7 @@ export const FundingRightSidebar = ({
createdDate: comment.createdDate,
content: comment.content,
}))}
startDate={work.createdDate}
startDate={getUpdatesStartDate(metadata.fundraising, work)}
className="p-0"
/>
{/* Applicants for the grant */}
Expand Down
13 changes: 1 addition & 12 deletions components/work/components/UpdatesSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@

import { Bell } from 'lucide-react';
import { ProgressUpdates } from '@/components/ui/ProgressUpdates';
import { calculateUpdateRate } from '@/components/Fund/lib/FundUtils';
import { UpdateRateBadge } from '@/components/ui/badges/UpdateRateBadge';

interface Update {
id: number;
Expand All @@ -18,25 +16,16 @@ interface UpdatesSectionProps {
}

export const UpdatesSection = ({ updates = [], startDate, className }: UpdatesSectionProps) => {
if (updates.length === 0 && !startDate) {
return null;
}

const updateRate = calculateUpdateRate(updates);

return (
<section className={className}>
<div className="flex items-center justify-between mb-4">
<div className="flex items-center space-x-2">
<Bell className="h-5 w-5 text-gray-500" />
<h2 className="text-base font-semibold text-gray-900">Author Updates</h2>
</div>
<div className="flex items-center gap-2">
<UpdateRateBadge updateRate={updateRate} />
</div>
</div>

<ProgressUpdates updates={updates} />
<ProgressUpdates updates={updates} startDate={startDate} />
</section>
);
};