Skip to content

Commit 15e879b

Browse files
committed
merge: resolve conflict on app/components/navbar.responsive-breakpoints.test.tsx
2 parents 6965b14 + 5dc4da7 commit 15e879b

202 files changed

Lines changed: 14998 additions & 917 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.

.github/scripts/issue-management/claim-handler.js

Lines changed: 34 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -28,27 +28,13 @@ async function handleClaim({ github, context }) {
2828
return;
2929
}
3030

31-
const issueAuthor = context.payload.issue.user.login;
32-
33-
const MAINTAINERS = ['jhasourav07', 'aamod-dev', 'souravjhahind'];
34-
const isOpenedByMaintainer = MAINTAINERS.includes(issueAuthor.toLowerCase());
35-
36-
if (!isOpenedByMaintainer && commenter.toLowerCase() !== issueAuthor.toLowerCase()) {
37-
await github.rest.issues.createComment({
38-
owner,
39-
repo,
40-
issue_number: issueNumber,
41-
body: `❌ Only the author of this issue (@${issueAuthor}) can claim it.`,
42-
});
43-
return;
44-
}
45-
46-
// Re-fetch to avoid stale assignee data from the webhook payload
31+
// Re-fetch to avoid stale issue/assignee/state data from the webhook payload
4732
const { data: freshIssue } = await github.rest.issues.get({
4833
owner,
4934
repo,
5035
issue_number: issueNumber,
5136
});
37+
5238
const currentAssignees = freshIssue.assignees.map((a) => a.login.toLowerCase());
5339

5440
if (currentAssignees.length > 0) {
@@ -71,6 +57,38 @@ async function handleClaim({ github, context }) {
7157
return;
7258
}
7359

60+
const issueAuthor = freshIssue.user.login;
61+
const issueAuthorLower = issueAuthor.toLowerCase();
62+
const commenterLower = commenter.toLowerCase();
63+
64+
const MAINTAINERS = ['jhasourav07', 'aamod-dev', 'souravjhahind'];
65+
const isOpenedByMaintainer = MAINTAINERS.includes(issueAuthorLower);
66+
67+
// Check if the issue is older than 1 week (7 days)
68+
const createdAt = new Date(freshIssue.created_at);
69+
const now = new Date();
70+
const ageInMs = now.getTime() - createdAt.getTime();
71+
const oneWeekInMs = 7 * 24 * 60 * 60 * 1000;
72+
const isOlderThanOneWeek = ageInMs > oneWeekInMs;
73+
74+
// Check if opened by any other person other than jhasourav07 or aamod007 (and aamod-dev for safety)
75+
const isOpenedByOther = !['jhasourav07', 'aamod007', 'aamod-dev'].includes(issueAuthorLower);
76+
77+
const isAuthor = commenterLower === issueAuthorLower;
78+
const canClaimOlderIssue = isOpenedByOther && isOlderThanOneWeek;
79+
80+
const isAllowedToClaim = isAuthor || isOpenedByMaintainer || canClaimOlderIssue;
81+
82+
if (!isAllowedToClaim) {
83+
await github.rest.issues.createComment({
84+
owner,
85+
repo,
86+
issue_number: issueNumber,
87+
body: `❌ Only the author of this issue (@${issueAuthor}) can claim it.`,
88+
});
89+
return;
90+
}
91+
7492
const existingIssues = await findExistingAssignments(github, owner, repo, commenter, issueNumber);
7593
if (existingIssues.length >= MAX_ASSIGNED_ISSUES) {
7694
const issueList = existingIssues

CONTRIBUTING.md

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -412,7 +412,7 @@ To maintain high quality in our codebase, we use structured **Issue Templates**
412412
- **🐛 Bug Report Template**: For reporting visual glitches, API errors, or unexpected behavior.
413413
- **✨ Feature Request Template**: For suggesting new isometric monolith designs, themes, or time/accuracy improvements.
414414

415-
By using these templates, you provide maintainers with clear details and context. Since you authored the issue, you can immediately claim it for yourself by commenting `/claim`! (Note: Issues authored by `jhasourav07` can be claimed by anyone).
415+
By using these templates, you provide maintainers with clear details and context. Since you authored the issue, you can immediately claim it for yourself by commenting `/claim`! (Note: Issues authored by `jhasourav07` can be claimed by anyone immediately. Additionally, if an issue is opened by anyone other than `jhasourav07` or `aamod007`, is older than 1 week, and has no assignees, any contributor can claim it).
416416

417417
### 🔍 Semantic Duplicate Detection
418418

@@ -427,13 +427,13 @@ To help maintainers keep the repository organized and prevent multiple contribut
427427

428428
Our automation runs entirely through issue comments. Here is how you interact with it:
429429

430-
| Command | Who Can Use It? | What It Does |
431-
| ----------------------------- | ------------------------------------------------------- | --------------------------------------------------------- |
432-
| `/claim` | **Issue Author (or Anyone if authored by jhasourav07)** | Self-assigns the issue to you. |
433-
| `/unclaim` | **Assigned Contributor** | Removes the assignment from yourself (opens it back up). |
434-
| `/addlabel <label1> <label2>` | **Anyone** | Adds labels to the issue (e.g. `/addlabel frontend bug`). |
435-
| `/unassign @username` | **Maintainers Only** | Removes the assignee from an issue. |
436-
| `/assign @username` | **Maintainers Only** | Manually assigns someone to an issue. |
430+
| Command | Who Can Use It? | What It Does |
431+
| ----------------------------- | --------------------------------------------------------------------------------------------------------- | --------------------------------------------------------- |
432+
| `/claim` | **Issue Author (or Anyone if authored by jhasourav07, or Anyone if older than 1 week with no assignees)** | Self-assigns the issue to you. |
433+
| `/unclaim` | **Assigned Contributor** | Removes the assignment from yourself (opens it back up). |
434+
| `/addlabel <label1> <label2>` | **Anyone** | Adds labels to the issue (e.g. `/addlabel frontend bug`). |
435+
| `/unassign @username` | **Maintainers Only** | Removes the assignee from an issue. |
436+
| `/assign @username` | **Maintainers Only** | Manually assigns someone to an issue. |
437437

438438
### ⏳ The Inactivity Policy (Assignment Expiry)
439439

@@ -458,7 +458,7 @@ If the bot rejects your command, check these common scenarios:
458458
- **"Commands cannot be used on closed issues"**: You cannot claim, assign, or unassign on closed issues. Find an open one!
459459
- **"You already have X/5 active assigned issues"**: You have reached the maximum of 5 concurrent assignments. Finish one of your current tasks before claiming a new issue. If you're stuck, use the `/unclaim` command to unassign yourself from an issue, or ask a maintainer to `/unassign` you.
460460
- **"This issue is already assigned to @username"**: Be faster next time! Look for issues without assignees.
461-
- **"Only the author of this issue can claim it"**: You tried to `/claim` an issue you did not create. You can only claim issues that you authored (unless the issue was authored by `jhasourav07`, which anyone can claim).
461+
- **"Only the author of this issue can claim it"**: You tried to `/claim` an issue you did not create. You can only claim issues that you authored (unless the issue is older than 1 week with no assignees, or was authored by `jhasourav07`, which anyone can claim).
462462
- **"The following label(s) do not exist"**: You can only add existing repo labels. The bot will reply with a list of valid labels you can use.
463463
- **"You don't have permission"**: You tried to use `/assign` or `/unassign`. Please use `/claim` instead.
464464

README.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,11 +139,27 @@ 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

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:
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/api/articles/route.ts

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import { NextResponse } from 'next/server';
2+
import { fetchLatestArticles } from '@/lib/rss';
3+
import { generateArticlesSVG } from '@/lib/svg/articles';
4+
import { getNormalizedThemeKey, themes } from '@/lib/svg/themes';
5+
import { sanitizeHexColor } from '@/lib/svg/sanitizer';
6+
7+
export async function GET(request: Request) {
8+
try {
9+
const { searchParams } = new URL(request.url);
10+
const user = searchParams.get('user');
11+
const platformParam = searchParams.get('platform');
12+
13+
if (!user) {
14+
return new NextResponse('Missing user parameter', { status: 400 });
15+
}
16+
17+
const platform = platformParam === 'hashnode' ? 'hashnode' : 'devto';
18+
19+
// Fetch RSS articles
20+
const articles = await fetchLatestArticles(platform, user);
21+
22+
// Apply theme
23+
const themeKey = getNormalizedThemeKey(searchParams.get('theme') || 'default');
24+
const theme = themes[themeKey] || themes.default;
25+
26+
// Allow custom colors
27+
const bg = searchParams.get('bg')
28+
? sanitizeHexColor(searchParams.get('bg')!, theme.bg)
29+
: theme.bg;
30+
const text = searchParams.get('text')
31+
? sanitizeHexColor(searchParams.get('text')!, theme.text)
32+
: theme.text;
33+
let accent = theme.accent;
34+
const accentParam = searchParams.get('accent');
35+
if (accentParam) {
36+
// support comma separated list, just take the first
37+
const accents = accentParam.split(',').map((c) => sanitizeHexColor(c.trim(), theme.accent));
38+
if (accents.length > 0 && accents[0]) {
39+
accent = accents[0];
40+
}
41+
}
42+
43+
const radius = searchParams.get('radius') || '4';
44+
const size = searchParams.get('size') || '1';
45+
46+
const params = {
47+
user,
48+
platform,
49+
bg,
50+
text,
51+
accent,
52+
radius,
53+
size,
54+
};
55+
56+
const svg = generateArticlesSVG(articles, params);
57+
58+
return new NextResponse(svg, {
59+
headers: {
60+
'Content-Type': 'image/svg+xml',
61+
'Cache-Control': 'public, s-maxage=3600, stale-while-revalidate=86400',
62+
},
63+
});
64+
} catch (error) {
65+
console.error('Error generating articles SVG:', error);
66+
return new NextResponse('Internal Server Error', { status: 500 });
67+
}
68+
}

0 commit comments

Comments
 (0)