Skip to content
Open
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
15 changes: 15 additions & 0 deletions app/profile/[userId]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
EarningsSummary,
type EarningsSummary as EarningsSummaryType,
} from "@/components/reputation/earnings-summary";
import { ReputationDashboard } from "@/components/profile/reputation-dashboard";
import { Button } from "@/components/ui/button";
import { Skeleton } from "@/components/ui/skeleton";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
Expand All @@ -17,6 +18,7 @@ import Link from "next/link";
import { useParams } from "next/navigation";
import { useMemo } from "react";
import { useCompletionHistory } from "@/hooks/use-reputation";
import { ContributorReputationWithMetrics } from "@/types/reputation";

export default function ProfilePage() {
const params = useParams();
Expand Down Expand Up @@ -194,6 +196,12 @@ export default function ProfilePage() {
>
Bounty History
</TabsTrigger>
<TabsTrigger
value="dashboard"
className="rounded-none border-b-2 border-transparent data-[state=active]:border-primary data-[state=active]:bg-transparent px-4 py-3"
>
Reputation Dashboard
</TabsTrigger>
<TabsTrigger
value="analytics"
className="rounded-none border-b-2 border-transparent data-[state=active]:border-primary data-[state=active]:bg-transparent px-4 py-3"
Expand Down Expand Up @@ -224,6 +232,13 @@ export default function ProfilePage() {
)}
</TabsContent>

<TabsContent value="dashboard" className="mt-6">
<h2 className="text-xl font-bold mb-4">Reputation Dashboard</h2>
<ReputationDashboard
reputation={reputation as ContributorReputationWithMetrics}
/>
Comment on lines +235 to +239
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Unsafe cast can crash the Dashboard tab at runtime.

At Line 238, reputation is force-cast to ContributorReputationWithMetrics, but API data is typed as ContributorReputation (no guaranteed metrics). Opening this tab can throw when metrics.xp is accessed.

💡 Suggested fix (guard + graceful partial-data handling)
             <TabsContent value="dashboard" className="mt-6">
               <h2 className="text-xl font-bold mb-4">Reputation Dashboard</h2>
-              <ReputationDashboard
-                reputation={reputation as ContributorReputationWithMetrics}
-              />
+              {"metrics" in reputation && reputation.metrics ? (
+                <ReputationDashboard
+                  reputation={reputation as ContributorReputationWithMetrics}
+                />
+              ) : (
+                <div className="p-8 border rounded-lg text-center text-muted-foreground bg-secondary/5">
+                  Reputation metrics are not available yet.
+                </div>
+              )}
             </TabsContent>
📝 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
<TabsContent value="dashboard" className="mt-6">
<h2 className="text-xl font-bold mb-4">Reputation Dashboard</h2>
<ReputationDashboard
reputation={reputation as ContributorReputationWithMetrics}
/>
<TabsContent value="dashboard" className="mt-6">
<h2 className="text-xl font-bold mb-4">Reputation Dashboard</h2>
{"metrics" in reputation && reputation.metrics ? (
<ReputationDashboard
reputation={reputation as ContributorReputationWithMetrics}
/>
) : (
<div className="p-8 border rounded-lg text-center text-muted-foreground bg-secondary/5">
Reputation metrics are not available yet.
</div>
)}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/profile/`[userId]/page.tsx around lines 235 - 239, The code force-casts
reputation to ContributorReputationWithMetrics when rendering
<ReputationDashboard>, which can crash if the API returned ContributorReputation
without metrics; instead, check for reputation.metrics before casting and handle
the missing-metrics case gracefully—either render a fallback/placeholder UI or
pass default metrics (e.g., zeros) so ReputationDashboard always receives the
expected shape; update the prop handling in ReputationDashboard usage (or its
prop type) to accept ContributorReputation | ContributorReputationWithMetrics
and guard/use default values when metrics is undefined.

</TabsContent>

<TabsContent value="analytics" className="mt-6">
<div className="p-8 border rounded-lg text-center text-muted-foreground bg-secondary/5">
Detailed analytics coming soon.
Expand Down
245 changes: 245 additions & 0 deletions components/profile/contribution-heatmap.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,245 @@
"use client";

import { ContributionHistory } from "@/types/reputation";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { cn } from "@/lib/utils";
import { format, subDays, startOfDay, isSameDay } from "date-fns";

Check warning on line 12 in components/profile/contribution-heatmap.tsx

View workflow job for this annotation

GitHub Actions / build-and-lint (24.x)

'isSameDay' is defined but never used
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Address lint warnings for unused symbols.

Line 12 imports isSameDay but never uses it, and Line 86 defines index but does not use it. This is currently reported by CI lint warnings.

🧹 Minimal cleanup patch
-import { format, subDays, startOfDay, isSameDay } from "date-fns";
+import { format, subDays, startOfDay } from "date-fns";
...
-  daysData.forEach((day, index) => {
+  daysData.forEach((day) => {

Also applies to: 86-86

🧰 Tools
🪛 GitHub Actions: CI - Build & Lint Check

[warning] 12-12: ESLint (@typescript-eslint/no-unused-vars): 'isSameDay' is defined but never used.

🪛 GitHub Check: build-and-lint (24.x)

[warning] 12-12:
'isSameDay' is defined but never used

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/profile/contribution-heatmap.tsx` at line 12, Remove the unused
import and parameter to fix lint warnings: delete the unused named import
isSameDay from the top import list and remove the unused map callback parameter
index (or rename it to _ if needed) in the component's map/iteration where index
is defined so both symbols are no longer unused; ensure any logic relying on
isSameDay is preserved or replaced with the used utilities (format, subDays,
startOfDay) and run the linter to confirm warnings are resolved.


interface ContributionHeatmapProps {
contributionHistory: ContributionHistory;
className?: string;
}

interface DayData {
date: string;
count: number;
}

// Color levels for different contribution intensities
const getContributionColor = (count: number, maxCount: number): string => {
if (count === 0) return "bg-muted";

const ratio = count / (maxCount || 1);

if (ratio >= 0.8) return "bg-primary";
if (ratio >= 0.6) return "bg-primary/80";
if (ratio >= 0.4) return "bg-primary/60";
if (ratio >= 0.2) return "bg-primary/40";
return "bg-primary/20";
};

const WEEKDAYS = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
const MONTHS = [
"Jan",
"Feb",
"Mar",
"Apr",
"May",
"Jun",
"Jul",
"Aug",
"Sep",
"Oct",
"Nov",
"Dec",
];

export function ContributionHeatmap({
contributionHistory,
className,
}: ContributionHeatmapProps) {
const { contributions, totalContributions, streak } = contributionHistory;

// Generate last 365 days (52 weeks + 1 day = 365 days)
const today = startOfDay(new Date());
const daysData: DayData[] = [];

for (let i = 364; i >= 0; i--) {
const date = subDays(today, i);
const dateStr = format(date, "yyyy-MM-dd");
const contribution = contributions.find((c) => c.date === dateStr);
daysData.push({
date: dateStr,
count: contribution?.count ?? 0,
});
}

// Organize into weeks for grid display
const weeks: DayData[][] = [];
let currentWeek: DayData[] = [];

// Find the starting day of week (Sunday)
const startDate = subDays(today, 364);
const startDayOfWeek = startDate.getDay();

// Add padding days for the first week
for (let i = 0; i < startDayOfWeek; i++) {
currentWeek.push({ date: "", count: 0 });
}

daysData.forEach((day, index) => {

Check warning on line 86 in components/profile/contribution-heatmap.tsx

View workflow job for this annotation

GitHub Actions / build-and-lint (24.x)

'index' is defined but never used
currentWeek.push(day);
if (currentWeek.length === 7) {
weeks.push(currentWeek);
currentWeek = [];
}
});

// Push remaining days
if (currentWeek.length > 0) {
while (currentWeek.length < 7) {
currentWeek.push({ date: "", count: 0 });
}
weeks.push(currentWeek);
}

const maxCount = Math.max(...daysData.map((d) => d.count), 1);

// Handle empty data
if (totalContributions === 0) {
return (
<Card className={cn("border-border/50", className)}>
<CardHeader className="pb-3 border-b border-border/50 bg-secondary/10">
<div className="flex items-center justify-between">
<CardTitle className="text-lg font-semibold">
Contribution Activity
</CardTitle>
</div>
</CardHeader>
<CardContent className="p-6">
<div className="flex items-center justify-center h-40 text-muted-foreground">
<p>No contributions in the last year</p>
</div>
</CardContent>
</Card>
);
}

return (
<Card className={cn("border-border/50", className)}>
<CardHeader className="pb-3 border-b border-border/50 bg-secondary/10">
<div className="flex items-center justify-between">
<CardTitle className="text-lg font-semibold">
Contribution Activity
</CardTitle>
<div className="flex items-center gap-4 text-sm text-muted-foreground">
<span>
<span className="font-semibold text-foreground">
{streak.current}
</span>{" "}
day streak
</span>
<span>
<span className="font-semibold text-foreground">
{streak.longest}
</span>{" "}
best
</span>
</div>
</div>
</CardHeader>
<CardContent className="p-4">
<TooltipProvider>
<div className="flex flex-col gap-1">
{/* Month labels */}
<div className="flex gap-[3px] ml-6 mb-1">
{MONTHS.map((month, index) => {
// Show month label at the start of each month
const shouldShow =
index === 0 ||
(index > 0 && index % 2 === 0) ||
index === MONTHS.length - 1;
if (!shouldShow) {
return <div key={month} className="w-3" />;
}
return (
<div
key={month}
className="text-xs text-muted-foreground w-8"
>
{month}
</div>
);
})}
Comment on lines +151 to +169
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Month labels are static and can be chronologically incorrect.

At Line 151–169, labels are hard-coded (Jan..Dec) instead of derived from the rendered week columns. This can show wrong month boundaries for the actual 365-day window.

🗓️ Suggested direction (derive labels from weeks)
-            <div className="flex gap-[3px] ml-6 mb-1">
-              {MONTHS.map((month, index) => {
-                // Show month label at the start of each month
-                const shouldShow =
-                  index === 0 ||
-                  (index > 0 && index % 2 === 0) ||
-                  index === MONTHS.length - 1;
-                if (!shouldShow) {
-                  return <div key={month} className="w-3" />;
-                }
-                return (
-                  <div
-                    key={month}
-                    className="text-xs text-muted-foreground w-8"
-                  >
-                    {month}
-                  </div>
-                );
-              })}
-            </div>
+            <div className="flex gap-[3px] ml-6 mb-1">
+              {monthLabels.map((label, weekIndex) => (
+                <div key={`month-${weekIndex}`} className="text-xs text-muted-foreground w-3">
+                  {label}
+                </div>
+              ))}
+            </div>

And compute monthLabels from weeks (based on each week’s first real day) before render.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/profile/contribution-heatmap.tsx` around lines 151 - 169, The
current month labels use the static MONTHS array and can misalign with the
actual 365-day heatmap; instead compute monthLabels from the rendered weeks
array: iterate over weeks (use each week’s first real day or first non-null
day), derive its month name/index, and only emit a label when the month changes
or at the first/last week; replace the static MONTHS usage in the render block
with the computed monthLabels so labels align with week columns (refer to
symbols weeks, monthLabels, and MONTHS in contribution-heatmap.tsx).

</div>

<div className="flex gap-1">
{/* Weekday labels */}
<div className="flex flex-col gap-[3px] pr-2">
{WEEKDAYS.map((day, index) => (
<div
key={day}
className={cn(
"text-xs text-muted-foreground h-3 w-6 flex items-center justify-end",
index % 2 === 1 && "opacity-0",
)}
>
{day}
</div>
))}
</div>

{/* Heatmap grid */}
<div className="flex gap-[3px] overflow-x-auto">
{weeks.map((week, weekIndex) => (
<div key={weekIndex} className="flex flex-col gap-[3px]">
{week.map((day, dayIndex) => {
const colorClass = day.date
? getContributionColor(day.count, maxCount)
: "bg-transparent";

return (
<Tooltip
key={`${weekIndex}-${dayIndex}`}
delayDuration={0}
>
<TooltipTrigger asChild>
<div
className={cn(
"h-3 w-3 rounded-sm transition-colors hover:ring-2 hover:ring-primary/50",
colorClass,
)}
/>
</TooltipTrigger>
{day.date && (
<TooltipContent side="top" className="text-xs">
<p className="font-medium">
{day.count} contribution
{day.count !== 1 ? "s" : ""}
</p>
<p className="text-muted-foreground">
{format(new Date(day.date), "MMM d, yyyy")}
</p>
</TooltipContent>
)}
</Tooltip>
);
})}
</div>
))}
</div>
</div>

{/* Legend */}
<div className="flex items-center justify-end gap-1 mt-2 text-xs text-muted-foreground">
<span>Less</span>
<div className="h-2 w-2 rounded-sm bg-muted" />
<div className="h-2 w-2 rounded-sm bg-primary/20" />
<div className="h-2 w-2 rounded-sm bg-primary/40" />
<div className="h-2 w-2 rounded-sm bg-primary/60" />
<div className="h-2 w-2 rounded-sm bg-primary/80" />
<div className="h-2 w-2 rounded-sm bg-primary" />
<span>More</span>
</div>
</div>
</TooltipProvider>
</CardContent>
</Card>
);
}
34 changes: 34 additions & 0 deletions components/profile/reputation-dashboard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
"use client";

import { ContributorReputationWithMetrics } from "@/types/reputation";
import { XpDisplay } from "./xp-display";
import { SkillRadarChart } from "./skill-radar-chart";
import { ContributionHeatmap } from "./contribution-heatmap";

interface ReputationDashboardProps {
reputation: ContributorReputationWithMetrics;
}

export function ReputationDashboard({ reputation }: ReputationDashboardProps) {
const { metrics } = reputation;

return (
<div className="space-y-6">
{/* XP Display */}
<XpDisplay xp={metrics.xp} />

{/* Charts Grid */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Skill Radar Chart */}
<SkillRadarChart skills={metrics.skills} />

{/* Contribution Stats */}
<div className="space-y-6">
<ContributionHeatmap
contributionHistory={metrics.contributionHistory}
/>
</div>
</div>
</div>
);
}
Loading
Loading