Skip to content

Commit c264799

Browse files
committed
Resolve upstream conflicts and fix CI formatting
2 parents 8f2202a + d03a482 commit c264799

18 files changed

Lines changed: 1545 additions & 1902 deletions

app/api/streak/route.test.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,12 +122,21 @@ describe('GET /api/streak', () => {
122122

123123
expect(response.status).toBe(400);
124124
const body = await response.json();
125-
expect(response.status).toBe(400);
126125
expect(body.error).toBe('Invalid parameters');
127126
expect(body.details).not.toBeNull();
128127
expect(typeof body.details).toBe('object');
129128
expect(Array.isArray(body.details)).toBe(false);
130129
});
130+
it('returns 400 when org parameter contains spaces and invalid characters', async () => {
131+
const response = await GET(
132+
makeRequest({ user: 'octocat', org: 'invalid_org_name_with_spaces' })
133+
);
134+
135+
expect(response.status).toBe(400);
136+
const body = await response.json();
137+
expect(body.details.fieldErrors.org[0]).toBe('Invalid organization name format');
138+
expect(getOrgDashboardData).not.toHaveBeenCalled();
139+
});
131140

132141
it('does not hit the GitHub API at all when user is missing', async () => {
133142
await GET(makeRequest());

app/customize/page.test.tsx

Lines changed: 26 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import { fireEvent, render, screen } from '@testing-library/react';
1+
import { act, fireEvent, render, screen } from '@testing-library/react';
22
import type { AnchorHTMLAttributes, HTMLAttributes, ReactNode } from 'react';
3-
import { describe, expect, it, vi } from 'vitest';
3+
import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest';
44
import CustomizePage from './page';
55

66
type MockLinkProps = AnchorHTMLAttributes<HTMLAnchorElement> & {
@@ -76,26 +76,41 @@ vi.mock('./components/ExportPanel', () => ({
7676
}));
7777

7878
describe('CustomizePage timezone query params', () => {
79-
it('omits the default UTC timezone from export snippets', () => {
79+
beforeEach(() => {
80+
global.fetch = vi.fn().mockResolvedValue({
81+
ok: true,
82+
text: async () => '<svg></svg>',
83+
});
84+
});
85+
86+
afterEach(() => {
87+
vi.restoreAllMocks();
88+
});
89+
90+
it('omits the default UTC timezone from export snippets', async () => {
8091
render(<CustomizePage />);
8192

82-
fireEvent.change(screen.getByLabelText('Mock username'), {
83-
target: { value: 'octocat' },
93+
await act(async () => {
94+
fireEvent.change(screen.getByLabelText('Mock username'), {
95+
target: { value: 'octocat' },
96+
});
8497
});
8598

8699
const snippet = screen.getByLabelText('Mock export snippet').textContent;
87100
expect(snippet).toContain('user=octocat');
88101
expect(snippet).not.toContain('tz=');
89102
});
90103

91-
it('adds a selected non-default timezone to export snippets', () => {
104+
it('adds a selected non-default timezone to export snippets', async () => {
92105
render(<CustomizePage />);
93106

94-
fireEvent.change(screen.getByLabelText('Mock username'), {
95-
target: { value: 'octocat' },
96-
});
97-
fireEvent.change(screen.getByLabelText('Mock timezone'), {
98-
target: { value: 'Asia/Kolkata' },
107+
await act(async () => {
108+
fireEvent.change(screen.getByLabelText('Mock username'), {
109+
target: { value: 'octocat' },
110+
});
111+
fireEvent.change(screen.getByLabelText('Mock timezone'), {
112+
target: { value: 'Asia/Kolkata' },
113+
});
99114
});
100115

101116
const snippet = screen.getByLabelText('Mock export snippet').textContent;

app/page.tsx

Lines changed: 43 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
'use client';
22
import { trackUser } from '@/utils/tracking';
3-
import type { ReactNode } from 'react';
3+
import InteractiveViewer from '@/components/InteractiveViewer';
4+
45
import Link from 'next/link';
56
import { useRef, useState, useEffect } from 'react';
67
import { AnimatePresence, motion } from 'framer-motion';
@@ -11,7 +12,7 @@ import { CustomizeCTA } from './components/CustomizeCTA';
1112
import { useRecentSearches } from '@/hooks/useRecentSearches';
1213
import { useDebounce } from '@/hooks/useDebounce';
1314
import { Footer } from '@/app/components/Footer';
14-
import InteractiveViewer from '@/components/InteractiveViewer';
15+
1516
import { FeatureCard, FeatureCardsSection } from '@/components/FeatureCards';
1617
import { DiscordButton } from '@/components/DiscordButton';
1718

@@ -140,6 +141,39 @@ export default function LandingPage() {
140141
setTimeout(() => setCopied(false), 50000);
141142
};
142143

144+
const handleRotate3D = (dx: number, dy: number) => {
145+
const towersGroup = document.getElementById('cp-towers');
146+
if (!towersGroup) return;
147+
148+
// Remove any transition so dragging feels instant and responsive
149+
towersGroup.style.transition = 'none';
150+
151+
let currentX = 0;
152+
let currentY = 0;
153+
154+
const transformStr = towersGroup.style.transform;
155+
if (transformStr) {
156+
const matchX = transformStr.match(/rotateX\(([-0-9.]+)deg\)/);
157+
const matchY = transformStr.match(/rotateY\(([-0-9.]+)deg\)/);
158+
if (matchX) currentX = parseFloat(matchX[1]);
159+
if (matchY) currentY = parseFloat(matchY[1]);
160+
}
161+
162+
const newX = currentX - dy * 0.5;
163+
const newY = currentY + dx * 0.5;
164+
165+
towersGroup.style.transform = `translate(0px, 20px) rotateX(${newX}deg) rotateY(${newY}deg)`;
166+
};
167+
168+
const handleReset3D = () => {
169+
const towersGroup = document.getElementById('cp-towers');
170+
if (towersGroup) {
171+
// Apply a smooth transition for the reset
172+
towersGroup.style.transition = 'transform 1.2s cubic-bezier(0.16, 1, 0.3, 1)';
173+
towersGroup.style.transform = '';
174+
}
175+
};
176+
143177
return (
144178
<div className="min-h-screen overflow-x-hidden bg-transparent font-sans text-black dark:text-white selection:bg-black/20 dark:selection:bg-white/20">
145179
<div className="pointer-events-none fixed inset-0 overflow-hidden">
@@ -308,7 +342,12 @@ export default function LandingPage() {
308342

309343
<div className="group relative mt-10">
310344
<div className="absolute -inset-1 rounded-[2.5rem] bg-gradient-to-r from-emerald-500/20 to-cyan-500/20 opacity-50 blur-2xl transition duration-1000 group-hover:opacity-100" />
311-
<div className="relative flex min-h-[480px] md:min-h-[520px] items-center justify-center overflow-visible rounded-3xl border border-black/5 bg-white/50 p-8 backdrop-blur-xl shadow-2xl dark:border-white/10 dark:bg-[#0a0a0a]/80">
345+
<InteractiveViewer
346+
className="relative flex min-h-[480px] md:min-h-[520px] items-center justify-center overflow-visible rounded-3xl border border-black/5 bg-white/50 p-8 backdrop-blur-xl shadow-2xl dark:border-white/10 dark:bg-[#0a0a0a]/80"
347+
is3DMode={true}
348+
onRotate3D={handleRotate3D}
349+
onReset3D={handleReset3D}
350+
>
312351
{hasUsername ? (
313352
<div className="w-full flex items-center justify-center">
314353
{svgState === 'loading' && (
@@ -370,7 +409,7 @@ export default function LandingPage() {
370409
</p>
371410
</div>
372411
)}
373-
</div>
412+
</InteractiveViewer>
374413
</div>
375414
</section>
376415

components/DiscordButton.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
'use client';
22

33
import { useRef, useState, useEffect } from 'react';
4-
import { motion, useAnimation } from 'framer-motion';
4+
import { motion } from 'framer-motion';
55
import gsap from 'gsap';
66

77
export function DiscordButton() {

components/InteractiveViewer.tsx

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,11 +82,17 @@ export const formatDate = (dateStr: string): string => {
8282
interface InteractiveViewerProps {
8383
children: ReactNode;
8484
className?: string;
85+
is3DMode?: boolean;
86+
onRotate3D?: (dx: number, dy: number) => void;
87+
onReset3D?: () => void;
8588
}
8689

8790
export default function InteractiveViewer({
8891
children,
8992
className = '',
93+
is3DMode = false,
94+
onRotate3D,
95+
onReset3D,
9096
}: InteractiveViewerProps): ReactElement {
9197
const [pan, setPan] = useState({ x: 0, y: 0 });
9298
const [zoom, setZoom] = useState(1);
@@ -191,7 +197,13 @@ export default function InteractiveViewer({
191197
if (isDragging.current) {
192198
const dx = e.clientX - lastMousePos.current.x;
193199
const dy = e.clientY - lastMousePos.current.y;
194-
setPan((p) => ({ x: p.x + dx, y: p.y + dy }));
200+
201+
if (is3DMode && onRotate3D) {
202+
onRotate3D(dx, dy);
203+
} else {
204+
setPan((p) => ({ x: p.x + dx, y: p.y + dy }));
205+
}
206+
195207
lastMousePos.current = { x: e.clientX, y: e.clientY };
196208
// Hide tooltip during active drag/pan
197209
activeTooltipRef.current = null;
@@ -285,6 +297,14 @@ export default function InteractiveViewer({
285297
}
286298
};
287299

300+
const handleDoubleClick = (): void => {
301+
if (is3DMode && onReset3D) {
302+
onReset3D();
303+
}
304+
setPan({ x: 0, y: 0 });
305+
setZoom(1);
306+
};
307+
288308
return (
289309
<div
290310
ref={containerRef}
@@ -298,6 +318,7 @@ export default function InteractiveViewer({
298318
onPointerLeave={handlePointerLeave}
299319
onWheel={handleWheel}
300320
onKeyDown={handleKeyDown}
321+
onDoubleClick={handleDoubleClick}
301322
style={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }}
302323
>
303324
{/* ── Parallax background layer ──────────────────────────────────────────

components/dashboard/DashboardClient.tsx

Lines changed: 36 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import RepositoryGraph from './RepositoryGraph';
2121
import ComparisonStatsCard from './ComparisonStatsCard';
2222
import RadarChart from './RadarChart';
2323
import GrowthTrendChart from './GrowthTrendChart';
24+
import ProfileOptimizerModal from './ProfileOptimizerModal';
2425

2526
// Define the dashboard data structure
2627
interface DashboardData {
@@ -321,6 +322,7 @@ export default function DashboardClient({ initialData, username }: DashboardClie
321322
const [secondUserData, setSecondUserData] = useState<DashboardData | null>(null);
322323
const [isCompareMode, setIsCompareMode] = useState(false);
323324
const [isModalOpen, setIsModalOpen] = useState(false);
325+
const [isOptimizerOpen, setIsOptimizerOpen] = useState(false);
324326
const [secondUsernameInput, setSecondUsernameInput] = useState('');
325327
const [isLoadingSecond, setIsLoadingSecond] = useState(false);
326328
const [compareError, setCompareError] = useState<string | null>(null);
@@ -476,12 +478,34 @@ export default function DashboardClient({ initialData, username }: DashboardClie
476478
</div>
477479
<div className="flex gap-4 flex-wrap">
478480
{!isCompareMode && (
479-
<button
480-
onClick={handleOpenModal}
481-
className="flex items-center gap-2 rounded-xl border border-black/10 dark:border-[rgba(255,255,255,0.15)] bg-black dark:bg-[#111] hover:bg-zinc-800 dark:hover:bg-zinc-900 px-4 py-2 text-sm font-semibold text-white dark:text-white transition-all duration-200 active:scale-[0.98]"
482-
>
483-
Compare Profile
484-
</button>
481+
<>
482+
<button
483+
onClick={() => setIsOptimizerOpen(true)}
484+
className="flex items-center gap-2 rounded-xl border border-black/10 dark:border-[rgba(255,255,255,0.15)] bg-black dark:bg-[#111] hover:bg-zinc-800 dark:hover:bg-zinc-900 px-4 py-2 text-sm font-semibold text-white dark:text-white transition-all duration-200 active:scale-[0.98]"
485+
>
486+
<svg
487+
xmlns="http://www.w3.org/2000/svg"
488+
width="16"
489+
height="16"
490+
viewBox="0 0 24 24"
491+
fill="none"
492+
stroke="currentColor"
493+
strokeWidth="2"
494+
strokeLinecap="round"
495+
strokeLinejoin="round"
496+
>
497+
<polyline points="22 7 13.5 15.5 8.5 10.5 2 17"></polyline>
498+
<polyline points="16 7 22 7 22 13"></polyline>
499+
</svg>
500+
Profile Optimizer
501+
</button>
502+
<button
503+
onClick={handleOpenModal}
504+
className="flex items-center gap-2 rounded-xl border border-black/10 dark:border-[rgba(255,255,255,0.15)] bg-black dark:bg-[#111] hover:bg-zinc-800 dark:hover:bg-zinc-900 px-4 py-2 text-sm font-semibold text-white dark:text-white transition-all duration-200 active:scale-[0.98]"
505+
>
506+
Compare Profile
507+
</button>
508+
</>
485509
)}
486510
<RefreshButton username={username} />
487511
<button
@@ -1057,6 +1081,12 @@ export default function DashboardClient({ initialData, username }: DashboardClie
10571081
</div>
10581082
)}
10591083
</AnimatePresence>
1084+
1085+
<ProfileOptimizerModal
1086+
isOpen={isOptimizerOpen}
1087+
onClose={() => setIsOptimizerOpen(false)}
1088+
userData={initialData}
1089+
/>
10601090
</div>
10611091
);
10621092
}

0 commit comments

Comments
 (0)