diff --git a/app/api/calculate/route.ts b/app/api/calculate/route.ts index 85501d2..0f1e574 100644 --- a/app/api/calculate/route.ts +++ b/app/api/calculate/route.ts @@ -22,6 +22,7 @@ export async function POST(request: NextRequest) { return NextResponse.json({ success: true, + calculation, ...calculation, }); } catch (error: any) { diff --git a/app/api/contributions/route.ts b/app/api/contributions/route.ts new file mode 100644 index 0000000..ae6217b --- /dev/null +++ b/app/api/contributions/route.ts @@ -0,0 +1,34 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { sessionManager } from '@/lib/services/SessionManager'; +import { gitHubApiClient } from '@/lib/services/GitHubApiClient'; + +export async function GET(request: NextRequest) { + const cookieValue = request.cookies.get(sessionManager.getSessionCookieName())?.value; + const user = sessionManager.parseUser(cookieValue); + + if (!user || !user.login) { + return NextResponse.json( + { success: false, message: 'GitHub sign-in is required.' }, + { status: 401 } + ); + } + + try { + const contributions = await gitHubApiClient.fetchContributionCalendar( + user.login, + user.accessToken + ); + + return NextResponse.json({ + success: true, + user: user.login, + contributions, + totalActiveDays: Object.keys(contributions).length, + }); + } catch (error: any) { + return NextResponse.json( + { success: false, message: error.message || 'Failed to fetch contribution calendar' }, + { status: error.status || 500 } + ); + } +} diff --git a/app/components/HeatmapCanvas.tsx b/app/components/HeatmapCanvas.tsx index 6ef2bdd..49f4d85 100644 --- a/app/components/HeatmapCanvas.tsx +++ b/app/components/HeatmapCanvas.tsx @@ -10,14 +10,19 @@ import { Shuffle, Check, Hand, - X + X, + ShieldCheck, + ShieldAlert } from 'lucide-react'; import { Button } from '@/app/components/ui/Button'; import { Badge } from '@/app/components/ui/Badge'; -interface HeatmapCanvasProps { +export interface HeatmapCanvasProps { matrix: CustomCommitMatrix; onChange: (matrix: CustomCommitMatrix) => void; + existingContributions?: Record; + protectExisting?: boolean; + onToggleProtectExisting?: (protect: boolean) => void; } const LEVEL_COLORS: Record = { @@ -28,6 +33,14 @@ const LEVEL_COLORS: Record = { 4: '#39d353', // Ultra (10 commits) }; +const EXISTING_LEVEL_COLORS: Record = { + 0: '#161b22', + 1: 'rgba(6, 182, 212, 0.40)', // Cyan 500 subtle + 2: 'rgba(6, 182, 212, 0.65)', // Cyan 500 medium + 3: 'rgba(14, 165, 233, 0.85)', // Sky 500 strong + 4: '#06b6d4', // Cyan 500 full glow +}; + const LEVEL_COMMITS: Record = { 0: 0, 1: 1, @@ -39,11 +52,23 @@ const LEVEL_COMMITS: Record = { const MONTH_NAMES = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; const DAY_LABELS = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']; -export function HeatmapCanvas({ matrix, onChange }: HeatmapCanvasProps) { +export function HeatmapCanvas({ + matrix, + onChange, + existingContributions = {}, + protectExisting = true, + onToggleProtectExisting +}: HeatmapCanvasProps) { const [selectedBrush, setSelectedBrush] = useState(3); // default Level 3 const [isMouseDown, setIsMouseDown] = useState(false); const [touchMode, setTouchMode] = useState<'draw' | 'pan'>('draw'); - const [hoveredCell, setHoveredCell] = useState<{ date: string; day: string; count: number; level: number } | null>(null); + const [hoveredCell, setHoveredCell] = useState<{ + date: string; + day: string; + count: number; + level: number; + isProtected?: boolean; + } | null>(null); // Responsive full-width container tracking const containerRef = useRef(null); @@ -110,14 +135,20 @@ export function HeatmapCanvas({ matrix, onChange }: HeatmapCanvasProps) { }, []); const paintCell = useCallback((dateStr: string, level: number) => { + if (protectExisting && (existingContributions[dateStr] || 0) > 0) { + return; // Cell is protected with existing contributions + } const count = LEVEL_COMMITS[level] || 0; onChange({ ...matrix, [dateStr]: count, }); - }, [matrix, onChange]); + }, [matrix, onChange, protectExisting, existingContributions]); const handleCellPointerDown = (dateStr: string) => { + if (protectExisting && (existingContributions[dateStr] || 0) > 0) { + return; // Do not paint on protected cell + } setIsMouseDown(true); if (touchMode === 'draw') { paintCell(dateStr, selectedBrush); @@ -125,6 +156,9 @@ export function HeatmapCanvas({ matrix, onChange }: HeatmapCanvasProps) { }; const handleCellPointerEnter = (dateStr: string) => { + if (protectExisting && (existingContributions[dateStr] || 0) > 0) { + return; // Do not paint on protected cell + } if (isMouseDown && touchMode === 'draw') { paintCell(dateStr, selectedBrush); } @@ -138,6 +172,9 @@ export function HeatmapCanvas({ matrix, onChange }: HeatmapCanvasProps) { const newMatrix: CustomCommitMatrix = {}; calendarGrid.forEach(week => { week.forEach(cell => { + if (protectExisting && (existingContributions[cell.dateStr] || 0) > 0) { + return; // Skip protected cells + } const curCount = matrix[cell.dateStr] || 0; newMatrix[cell.dateStr] = curCount > 0 ? 0 : LEVEL_COMMITS[selectedBrush || 3]; }); @@ -149,6 +186,9 @@ export function HeatmapCanvas({ matrix, onChange }: HeatmapCanvasProps) { const newMatrix: CustomCommitMatrix = {}; calendarGrid.forEach(week => { week.forEach(cell => { + if (protectExisting && (existingContributions[cell.dateStr] || 0) > 0) { + return; // Skip protected cells + } const isWeekend = cell.dayIndex === 0 || cell.dayIndex === 6; const chance = isWeekend ? 0.2 : 0.65; if (Math.random() < chance) { @@ -183,6 +223,9 @@ export function HeatmapCanvas({ matrix, onChange }: HeatmapCanvasProps) { if (weekIdx < 52 && calendarGrid[weekIdx]) { const cell = calendarGrid[weekIdx][r]; if (cell) { + if (protectExisting && (existingContributions[cell.dateStr] || 0) > 0) { + continue; // Do not overwrite protected cells with stencil + } const isFilled = rows[r]?.[c]; if (isFilled) { newMatrix[cell.dateStr] = LEVEL_COMMITS[selectedBrush || 3]; @@ -314,6 +357,30 @@ export function HeatmapCanvas({ matrix, onChange }: HeatmapCanvasProps) { {touchMode === 'draw' ? 'Draw Mode' : 'Pan Mode'} + {/* Protect Original Contributions Toggle */} + {onToggleProtectExisting && ( + + )} + {/* Stencil */}