Skip to content

Commit 2ee88cd

Browse files
committed
Merge remote-tracking branch 'origin/main' into fix/api-student-resume-upload-timezone
2 parents 59add60 + 3c9bdd6 commit 2ee88cd

300 files changed

Lines changed: 21475 additions & 1004 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.env.local.example

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,3 +57,10 @@ NEXT_ALLOWED_DEV_ORIGINS=
5757
# Find your GitHub user ID at https://api.github.com/users/<username> (the "id" field).
5858
# Example: ENTERPRISE_ADMIN_GITHUB_IDS=12345678,87654321
5959
ENTERPRISE_ADMIN_GITHUB_IDS=
60+
61+
# Spotify Integration (Optional)
62+
# Required for the "Currently Playing" SVG feature.
63+
# Follow the setup guide in the documentation to get these credentials.
64+
SPOTIFY_CLIENT_ID=
65+
SPOTIFY_CLIENT_SECRET=
66+
SPOTIFY_REFRESH_TOKEN=

.github/workflows/ci.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,14 @@ jobs:
4747
- name: Run Unit Tests (Vitest)
4848
run: npm run test:coverage
4949

50+
- name: Upload Visual Regression Diffs
51+
if: failure()
52+
uses: actions/upload-artifact@v4
53+
with:
54+
name: visual-test-diffs
55+
path: tests/visual/diffs/
56+
if-no-files-found: ignore
57+
5058
build:
5159
name: Production Build
5260
runs-on: ubuntu-latest

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
# testing
1414
/coverage
15+
/tests/visual/diffs/
1516

1617
# next.js
1718
/.next/

README.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,18 +139,35 @@ Transform your GitHub contribution history into a cinematic 3D monolith.
139139
| `monokai` | Classic vibrant dark | `272822` | `a6e22e` | `f8f8f2` |
140140
| `midnight_ocean` | Deep navy bioluminescent | `020c1b` | `0af5ff` | `ccd6f6` |
141141
| `india` | Saffron & India green | `0a0a0a` | `FF9933` | `ffffff` |
142+
| `ocean` | Deep sea teal & navy | `0a192f` | `64ffda` | `ccd6f6` |
143+
| `sunset` | Warm dusk orange | `1a0a0a` | `ff6b35` | `ffd6c0` |
144+
| `forest` | Emerald woodland green | `0d1f0d` | `39d353` | `c8f0c8` |
145+
| `rose` | Blush pink romance | `1f0d14` | `ff6b9d` | `f0c8d4` |
146+
| `nord` | Arctic frost blue | `2e3440` | `88c0d0` | `d8dee9` |
147+
| `synthwave` | 80s retro pink glow | `0d0221` | `ff2d78` | `f8f8f2` |
148+
| `aurora_cyberpunk` | Aurora violet neon | `090B13` | `9D5CFF` | `EAF2FF` |
149+
| `catppuccin_latte` | Pastel latte light | `eff1f5` | `1e66f5` | `4c4f69` |
150+
| `solarized_light` | Warm solarized light | `fdf6e3` | `268bd2` | `586e75` |
151+
| `gruvbox_light` | Retro warm light | `fbf1c7` | `d65d0e` | `3c3836` |
152+
| `nord_light` | Arctic frost light | `eceff4` | `5e81ac` | `2e3440` |
153+
| `cyberpunk` | Neon yellow cyberpunk | `fce22a` | `ff003c` | `111111` |
154+
| `cyberpunk_neon` | Electric cyan neon | `0d0d14` | `ff0055` | `00f3ff` |
155+
| `enterprise` | Professional indigo dark | `1a1a2e` | `6366f1` | `e2e8f0` |
142156

143157
> **`auto` uses CSS `@media (prefers-color-scheme)`** inside the SVG so the badge switches between the `light` and `dark` palettes based on the viewer's OS setting — no JavaScript required. This is ideal for GitHub profile READMEs where visitors may use either mode.
144158
145159
For all URL parameters and configuration possibilities (including grace periods, custom fonts, timezone overrides, versus comparison mode, heatmap view, LOC mode, and layout dimensions), check out the **[🎨 Customization Guide & Parameters](docs/customization.md)**.
146160

161+
For advanced usage examples including custom gradient backgrounds, multi-user comparisons, organization dashboards, and custom date ranges, see the **[🚀 Advanced Usage Examples](docs/customization.md#-advanced-usage-examples)** section in the customization guide.
162+
147163
---
148164

149165
## ✨ Features
150166

151167
CommitPulse transforms GitHub contribution data into visually engaging and highly customizable SVG badges.
152168

153169
- **🎨 Theme & Customization**: Multiple built-in themes, custom colors (`bg`, `accent`, `text`), dynamic font selection, adjustable dimensions, border radius, opacity, and system-aware `auto` light/dark theme.
170+
- **🎵 Spotify "Currently Playing"**: Showcase your current Spotify playback on your GitHub profile with a customizable, near real-time SVG card.
154171
- **📈 Contribution Analytics**: Current streak and longest streak tracking, monthly contribution summaries, historical year-by-year viewing, and custom grace period configurations.
155172
- **🔥 Visualization Modes**: Isometric 3D monolith rendering (with ghost city blueprint foundations), GitHub-style heatmap, monthly statistics view, and radar chart view.
156173
- **🌍 Localization & Accessibility**: Multi-language support (e.g. English, Hindi, Simplified Chinese, Portuguese), timezone-aware calculations, and high-contrast accessibility themes.
@@ -166,6 +183,7 @@ To keep the repository clean and readable, technical details have been modulariz
166183
- **[🎨 Customization Guide & Parameters](docs/customization.md)**: Explore the list of over 30 URL parameters including `theme`, `view` (e.g. `skyline`, `heatmap`, `radar`, `monthly`), `radius`, `grace`, `tz`, `entrance`, `versus`, and layout dimensions to style your monolith.
167184
- **[🏛️ Architecture & Design Philosophy](docs/architecture.md)**: Read about why we built isometric 3D monolith landscapes instead of flat meters, and check out our Next.js 16 Edge computing pipeline.
168185
- **[🚀 Self-Hosting & Deployment](docs/self_hosting.md)**: Step-by-step instructions to clone, configure `.env.local` with GitHub Personal Access Tokens (PAT), set up MongoDB tracking, and deploy to Vercel with one click.
186+
- **[🎵 Spotify Setup Guide](docs/SPOTIFY_SETUP.md)**: Instructions for setting up Spotify integration for the Currently Playing feature.
169187
- **[🤖 Automated Contributor Workflow](docs/contributor_workflow.md)**: Overview of GSSoC contribution automation, self-claiming comments `/claim`, anti-hoarding rules, stale unassign scripts, and Gemini AI-powered semantic issue duplication check.
170188
- **[🎯 Real-Time Accuracy & Caching](docs/accuracy.md)**: Deep dive into the "off-by-N contributions" problem and how CommitPulse solves it with UTC midnight CDN expiration and no-store GraphQL fetches.
171189
- **[❓ FAQ & Troubleshooting](docs/faq.md)**: Answers to common questions regarding timezone overrides, private contribution visibility, GitHub API rate limits, and troubleshooting.

THEMES.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ https://commitpulse.vercel.app/api/streak?user=YOUR_USERNAME&theme=<slug>
4141
| monokai | `#272822` | `#f8f8f2` | `#a6e22e` |
4242
| retro-terminal | `#000000` | `#00ff41` | `#00ff41` |
4343
| midnight_ocean | `#020c1b` | `#ccd6f6` | `#0af5ff` |
44+
| ayu_mirage | `#212733` | `#D9D7CE` | `#FFCC66` |
4445

4546
---
4647

@@ -394,6 +395,16 @@ https://commitpulse.vercel.app/api/streak?user=YOUR_USERNAME&theme=<slug>
394395

395396
---
396397

398+
### Ayu Mirage
399+
400+
![ayu_mirage](https://commitpulse.vercel.app/api/streak?user=jhasourav07&theme=ayu_mirage)
401+
402+
| Parameter | Value |
403+
| --------- | ------ |
404+
| `bg` | 212733 |
405+
| `text` | D9D7CE |
406+
| `accent` | FFCC66 |
407+
397408
## Custom Theme
398409

399410
Not finding what you want? Build your own using raw color parameters - all values are hex codes **without** the `#` prefix:

app/(root)/dashboard/[username]/page.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ export default async function DashboardPage({
9292
month?: string;
9393
from?: string;
9494
to?: string;
95+
excludeBots?: string;
9596
}>;
9697
}) {
9798
const { username } = await params;
@@ -116,9 +117,11 @@ async function DashboardContent({
116117
month?: string;
117118
from?: string;
118119
to?: string;
120+
excludeBots?: string;
119121
};
120122
}) {
121123
const bypassCache = searchParams?.refresh === 'true';
124+
const excludeBots = searchParams?.excludeBots === 'true';
122125
const compareUsername = searchParams?.compare;
123126
const period = resolveDashboardPeriod({
124127
year: searchParams?.year,
@@ -137,6 +140,7 @@ async function DashboardContent({
137140
to: period.to,
138141
rangeLabel: period.label,
139142
token: userToken,
143+
excludeBots,
140144
});
141145
} catch (error) {
142146
if (error instanceof Error && error.message.includes('not found')) {
@@ -176,6 +180,7 @@ async function DashboardContent({
176180
compareData = await getFullDashboardData(compareUsername, {
177181
bypassCache,
178182
token: userToken,
183+
excludeBots,
179184
});
180185
} catch {
181186
compareData = null;
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { render, screen } from '@testing-library/react';
2+
import { describe, it, expect, vi } from 'vitest';
3+
import React, { Component, ReactNode, ErrorInfo } from 'react';
4+
import DashboardLayout from './layout';
5+
6+
vi.mock('sonner', () => ({
7+
Toaster: () => <div data-testid="toaster" />,
8+
}));
9+
10+
class TestErrorBoundary extends Component<{ children: ReactNode }, { hasError: boolean }> {
11+
constructor(props: { children: ReactNode }) {
12+
super(props);
13+
this.state = { hasError: false };
14+
}
15+
static getDerivedStateFromError() {
16+
return { hasError: true };
17+
}
18+
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
19+
// catch silently for tests
20+
}
21+
render() {
22+
if (this.state.hasError) {
23+
return <div>Test Fallback UI</div>;
24+
}
25+
return this.props.children;
26+
}
27+
}
28+
29+
const ThrowError = () => {
30+
throw new Error('Hydration error simulated');
31+
};
32+
33+
describe('DashboardLayout - Error Resilience', () => {
34+
it('prevents layout structural crash when child components throw errors', () => {
35+
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
36+
37+
render(
38+
<TestErrorBoundary>
39+
<DashboardLayout>
40+
<ThrowError />
41+
</DashboardLayout>
42+
</TestErrorBoundary>
43+
);
44+
45+
expect(screen.getByText('Test Fallback UI')).toBeInTheDocument();
46+
47+
consoleErrorSpy.mockRestore();
48+
});
49+
50+
it('renders gracefully with fallback UI placeholders during hydration', () => {
51+
render(
52+
<DashboardLayout>
53+
<div data-testid="hydration-fallback">Loading layout skeleton...</div>
54+
</DashboardLayout>
55+
);
56+
57+
expect(screen.getByTestId('hydration-fallback')).toBeInTheDocument();
58+
expect(screen.getByTestId('toaster')).toBeInTheDocument();
59+
});
60+
});
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { render, screen, act } from '@testing-library/react';
2+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
3+
import DashboardLayout from './layout';
4+
import React from 'react';
5+
6+
vi.mock('sonner', () => ({
7+
Toaster: () => <div data-testid="toaster" />,
8+
}));
9+
10+
describe('DashboardLayout - mock integrations', () => {
11+
beforeEach(() => {
12+
vi.useFakeTimers();
13+
});
14+
15+
afterEach(() => {
16+
vi.clearAllTimers();
17+
vi.useRealTimers();
18+
});
19+
20+
it('renders children asynchronously resolving from a mocked service layer', async () => {
21+
const MockAsyncChild = () => {
22+
const [data, setData] = React.useState<string | null>(null);
23+
React.useEffect(() => {
24+
setTimeout(() => setData('Async Data Loaded'), 100);
25+
}, []);
26+
return <div data-testid="async-child">{data || 'Loading...'}</div>;
27+
};
28+
29+
render(
30+
<DashboardLayout>
31+
<MockAsyncChild />
32+
</DashboardLayout>
33+
);
34+
35+
expect(screen.getByTestId('async-child')).toHaveTextContent('Loading...');
36+
37+
await act(async () => {
38+
vi.advanceTimersByTime(100);
39+
});
40+
41+
expect(screen.getByTestId('async-child')).toHaveTextContent('Async Data Loaded');
42+
});
43+
44+
it('renders correctly with a local cache stub', () => {
45+
const cachedData = { user: 'Test User', cached: true };
46+
render(
47+
<DashboardLayout>
48+
<div data-testid="cache-child">{cachedData.user}</div>
49+
</DashboardLayout>
50+
);
51+
expect(screen.getByTestId('cache-child')).toHaveTextContent('Test User');
52+
expect(screen.getByTestId('toaster')).toBeInTheDocument();
53+
});
54+
});
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { render, screen } from '@testing-library/react';
2+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
3+
import DashboardLayout from './layout';
4+
import { mockTimezone, restoreTimezone } from '@/test-utils/timezone-mock';
5+
6+
vi.mock('sonner', () => ({
7+
Toaster: () => <div data-testid="toaster" />,
8+
}));
9+
10+
describe('DashboardLayout - Timezone Boundaries', () => {
11+
beforeEach(() => {
12+
vi.useFakeTimers();
13+
});
14+
15+
afterEach(() => {
16+
vi.clearAllTimers();
17+
vi.useRealTimers();
18+
restoreTimezone();
19+
});
20+
21+
it('maintains structural integrity across varying timezones', () => {
22+
const timezones = ['America/New_York', 'Asia/Kolkata', 'Asia/Tokyo'] as const;
23+
24+
for (const tz of timezones) {
25+
mockTimezone(tz);
26+
27+
const { unmount } = render(
28+
<DashboardLayout>
29+
<div>Timezone Content: {tz}</div>
30+
</DashboardLayout>
31+
);
32+
33+
expect(screen.getByText(`Timezone Content: ${tz}`)).toBeInTheDocument();
34+
expect(screen.getByRole('main')).toHaveClass('max-w-[1600px]', 'mx-auto', 'min-h-screen');
35+
36+
unmount();
37+
restoreTimezone();
38+
}
39+
});
40+
41+
it('handles leap year boundaries correctly', () => {
42+
vi.setSystemTime(new Date('2024-02-29T12:00:00Z'));
43+
44+
render(
45+
<DashboardLayout>
46+
<div>Leap Year Content</div>
47+
</DashboardLayout>
48+
);
49+
50+
expect(screen.getByText('Leap Year Content')).toBeInTheDocument();
51+
expect(screen.getByTestId('toaster')).toBeInTheDocument();
52+
});
53+
});

app/(root)/dashboard/org/[orgname]/page.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import CommitClock from '@/components/dashboard/CommitClock';
1111
import Heatmap from '@/components/dashboard/Heatmap';
1212
import AIInsights from '@/components/dashboard/AIInsights';
1313
import Achievements from '@/components/dashboard/Achievements';
14+
import BotFilterToggle from '@/components/dashboard/BotFilterToggle';
1415
import { getOrgDashboardData, buildCommitClock, generateAchievements } from '@/lib/github';
1516
import logger from '@/lib/logger';
1617
import { getUserGitHubToken } from '@/lib/githubtoken';
@@ -50,17 +51,18 @@ export default async function OrgDashboardPage({
5051
searchParams,
5152
}: {
5253
params: Promise<{ orgname: string }>;
53-
searchParams: Promise<{ refresh?: string }>;
54+
searchParams: Promise<{ refresh?: string; excludeBots?: string }>;
5455
}) {
5556
const { orgname } = await params;
5657
const refreshParams = await searchParams;
5758
const bypassCache = refreshParams?.refresh === 'true';
59+
const excludeBots = refreshParams?.excludeBots === 'true';
5860
const userToken = await getUserGitHubToken();
5961

6062
let data;
6163

6264
try {
63-
data = await getOrgDashboardData(orgname, { bypassCache, token: userToken });
65+
data = await getOrgDashboardData(orgname, { bypassCache, token: userToken, excludeBots });
6466
} catch (error) {
6567
logger.error('Failed to load organization page', {
6668
error,
@@ -144,6 +146,7 @@ export default async function OrgDashboardPage({
144146
languages: [], // Orgs skip language donut chart for now
145147
}}
146148
/>
149+
<BotFilterToggle />
147150
<Achievements achievements={achievements} />
148151
</aside>
149152

0 commit comments

Comments
 (0)