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
90 changes: 90 additions & 0 deletions src/components/CodingPersonaWidget.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import React from 'react';
import { calculateCodingPersona, ContributionItem } from '../utils/persona';
import { useTheme } from '@mui/material/styles';

interface CodingPersonaWidgetProps {
issues?: ContributionItem[];
pullRequests?: ContributionItem[];
}

export function CodingPersonaWidget({ issues = [], pullRequests = [] }: CodingPersonaWidgetProps) {
// 🌟 Access the active Material UI theme context dynamically
const theme = useTheme();
const isDarkMode = theme.palette.mode === 'dark';

const allContributions = [...issues, ...pullRequests];
const { personaTitle, earlyBirdPercent, nightOwlPercent, totalCount } = calculateCodingPersona(allContributions);

if (totalCount === 0) return null;

return (
<div
className="w-full border rounded-xl p-6 mb-6 shadow-sm transition-all duration-300"
style={{
backgroundColor: theme.palette.background.paper,
borderColor: theme.palette.divider,
}}
>
<div className="flex flex-col md:flex-row md:items-center justify-between gap-6">

{/* Left Side: Details Text Box */}
<div>
<span
className="text-xs font-bold tracking-wider uppercase opacity-60"
style={{ color: theme.palette.text.secondary }}
>
Developer Persona Analyzer
</span>
<h3
className="text-2xl font-black mt-1 tracking-tight"
style={{ color: theme.palette.text.primary }}
>
{personaTitle}
</h3>
<p
className="text-xs mt-1 opacity-80"
style={{ color: theme.palette.text.secondary }}
>
Calculated across {totalCount} records
</p>
</div>

{/* Right Side: Progress Meter Sliders */}
<div className="flex-1 max-w-md w-full">
<div
className="flex justify-between text-xs font-semibold mb-2"
style={{ color: theme.palette.text.secondary }}
>
<span className="flex items-center gap-1.5">
<span className="w-2 h-2 bg-amber-400 rounded-full" />
Morning: {earlyBirdPercent}%
</span>
<span className="flex items-center gap-1.5">
<span className="w-2 h-2 bg-indigo-500 rounded-full" />
Night: {nightOwlPercent}%
</span>
</div>

{/* Outer Progress Track Backdrop */}
<div
className="w-full rounded-full h-3.5 overflow-hidden flex border"
style={{
backgroundColor: isDarkMode ? '#1f2937' : '#f1f5f9', // slate-800 vs slate-100
borderColor: theme.palette.divider
}}
>
<div
style={{ width: `${earlyBirdPercent}%` }}
className="bg-gradient-to-r from-amber-500 to-amber-300 h-full transition-all duration-500 ease-out"
/>
<div
style={{ width: `${nightOwlPercent}%` }}
className="bg-gradient-to-r from-indigo-600 to-indigo-400 h-full transition-all duration-500 ease-out ml-auto"
/>
</div>
</div>

</div>
</div>
);
}
13 changes: 12 additions & 1 deletion src/components/Dashboard.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { CodingPersonaWidget } from './CodingPersonaWidget';
import React from 'react';
import {
PieChart,
Expand Down Expand Up @@ -74,8 +75,18 @@ const Dashboard: React.FC<DashboardProps> = ({ totalIssues, totalPrs, data, them
);
}

// 🌟 Filter data items dynamically into separate buckets for our calculator widget
const passingIssues = data.filter(item => !item.pull_request);
const passingPrs = data.filter(item => !!item.pull_request);

return (
<Box sx={{ mb: 4 }}>

{/* 🌟 BRING IN OUR NEW CALCULATOR ROW (Placed cleanly right above the graphs grid container) */}
<Box sx={{ mb: 3 }}>
<CodingPersonaWidget issues={passingIssues} pullRequests={passingPrs} />
</Box>

<Grid container spacing={3}>
{/* Pie Chart: Issues vs PRs */}
<Grid item xs={12} md={6}>
Expand Down Expand Up @@ -139,4 +150,4 @@ const Dashboard: React.FC<DashboardProps> = ({ totalIssues, totalPrs, data, them
);
};

export default Dashboard;
export default Dashboard;
11 changes: 10 additions & 1 deletion src/pages/Tracker/Tracker.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { CodingPersonaWidget } from '../../components/CodingPersonaWidget';
import React, { useState, useEffect } from "react"
import {
IssueOpenedIcon,
Expand Down Expand Up @@ -274,6 +275,14 @@ const Home: React.FC = () => {
/>
</Box>

{/* 🌟 FIXED WIDGET ARRAY PROPS DATA PIPELINE 🌟 */}
{/* Feeds both dataset logs together seamlessly to prevent premature pagination caps */}
{!loading && (issues.length > 0 || prs.length > 0) && (
<Box sx={{ mb: 2 }}>
<CodingPersonaWidget issues={issues} pullRequests={prs} />
</Box>
)}

{/* Tabs + State Filter */}
<Box
sx={{
Expand Down Expand Up @@ -400,4 +409,4 @@ const Home: React.FC = () => {
);
};

export default Home;
export default Home;
71 changes: 71 additions & 0 deletions src/utils/persona.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
export interface ContributionItem {
created_at?: string;
Created?: string;
[key: string]: any;
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

export interface PersonaResult {
personaTitle: string;
earlyBirdPercent: number;
nightOwlPercent: number;
totalCount: number;
}

export function calculateCodingPersona(items: ContributionItem[]): PersonaResult {
let earlyBirdCount = 0;
let nightOwlCount = 0;
let midDayCount = 0;
let validTimestampsCount = 0;

items.forEach((item) => {
const dateString = item.created_at || item.Created;
if (!dateString) return;

const date = new Date(dateString);

// Fix 1: Guard against invalid timestamps before bucketing to prevent data leakage
if (isNaN(date.getTime())) return;

// Fix 2: Changed to local getHours() to calculate personas based on actual user timezone timing
const hour = date.getHours();

// ☀️ Early Bird Bucket: 5:00 AM to 11:59 AM
if (hour >= 5 && hour < 12) {
earlyBirdCount++;
validTimestampsCount++;
}
// 🦉 Night Owl Bucket: 10:00 PM to 4:59 AM
else if (hour >= 22 || hour < 5) {
nightOwlCount++;
validTimestampsCount++;
}
// 🚀 Mid-Day / Afternoon Builder: 12:00 PM to 9:59 PM
else {
midDayCount++;
validTimestampsCount++;
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
});

// Calculate percentages based on the complete global dataset logs
const earlyBirdPercent = validTimestampsCount > 0 ? Math.round((earlyBirdCount / validTimestampsCount) * 100) : 0;
const nightOwlPercent = validTimestampsCount > 0 ? Math.round((nightOwlCount / validTimestampsCount) * 100) : 0;
const midDayPercent = validTimestampsCount > 0 ? Math.round((midDayCount / validTimestampsCount) * 100) : 0;

// Determine the dominant persona title string rule
let personaTitle = "Balanced Builder ⚖️";

if (earlyBirdPercent > nightOwlPercent && earlyBirdPercent > midDayPercent) {
personaTitle = "Early Bird ☀️";
} else if (nightOwlPercent > earlyBirdPercent && nightOwlPercent > midDayPercent) {
personaTitle = "Night Owl 🦉";
} else if (midDayPercent > earlyBirdPercent && midDayPercent > nightOwlPercent) {
personaTitle = "Productive Peer 🚀";
}

return {
personaTitle,
earlyBirdPercent,
nightOwlPercent,
totalCount: validTimestampsCount
};
}