Skip to content

Commit f4054b7

Browse files
Merge branch 'main' into bug-goaltracker
2 parents 5e84915 + 1a6f2b3 commit f4054b7

167 files changed

Lines changed: 5838 additions & 1411 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.

THEMES.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# CommitPulse Themes
22

3-
All 28 available themes for your CommitPulse badge. Use the `?theme=<slug>` query parameter to apply a theme.
3+
All 30 available themes for your CommitPulse badge. Use the `?theme=<slug>` query parameter to apply a theme.
44

55
```
66
https://commitpulse.vercel.app/api/streak?user=YOUR_USERNAME&theme=<slug>

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ import { notFound, redirect } from 'next/navigation';
1010
import { resolveDashboardPeriod } from '@/utils/dashboardPeriod';
1111
import DashboardPageWrapper from '../DashboardPageWrapper';
1212

13+
import EducationalCurveTracker from '@/components/dashboard/EducationalCurveTracker';
14+
1315
export const revalidate = 3600; // Cache for 1 hour
1416

1517
const BASE_URL =
@@ -189,6 +191,7 @@ async function DashboardContent({
189191

190192
return (
191193
<DashboardPageWrapper>
194+
<EducationalCurveTracker username={username} />
192195
<DashboardClient
193196
initialData={data}
194197
allRepoActivity={allRepos}

app/admin/reviews/page.tsx

Lines changed: 316 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,316 @@
1+
'use client';
2+
3+
import { useState, useEffect, useRef } from 'react';
4+
import Link from 'next/link';
5+
6+
interface Review {
7+
_id: string;
8+
name: string;
9+
handle: string;
10+
platform: 'twitter' | 'github';
11+
message: string;
12+
accentColor: string;
13+
approved: boolean;
14+
createdAt: string;
15+
}
16+
17+
interface Pagination {
18+
page: number;
19+
limit: number;
20+
total: number;
21+
totalPages: number;
22+
}
23+
24+
export default function AdminReviewsPage() {
25+
const [reviews, setReviews] = useState<Review[]>([]);
26+
const [pagination, setPagination] = useState<Pagination | null>(null);
27+
const [token, setToken] = useState('');
28+
const [isAuthed, setIsAuthed] = useState(false);
29+
const [statusFilter, setStatusFilter] = useState<'pending' | 'approved' | 'all'>('pending');
30+
const [loading, setLoading] = useState(false);
31+
const [error, setError] = useState<string | null>(null);
32+
const [page, setPage] = useState(1);
33+
34+
const tokenRef = useRef(token);
35+
const statusFilterRef = useRef(statusFilter);
36+
37+
useEffect(() => {
38+
tokenRef.current = token;
39+
statusFilterRef.current = statusFilter;
40+
});
41+
42+
useEffect(() => {
43+
const saved = typeof window !== 'undefined' ? localStorage.getItem('review_admin_token') : null;
44+
if (saved) {
45+
// eslint-disable-next-line react-hooks/set-state-in-effect -- hydrating from localStorage on mount
46+
setToken(saved);
47+
setIsAuthed(true);
48+
}
49+
}, []);
50+
51+
useEffect(() => {
52+
if (!isAuthed || !tokenRef.current) return;
53+
54+
let cancelled = false;
55+
async function load() {
56+
setLoading(true);
57+
setError(null);
58+
try {
59+
const params = new URLSearchParams({ page: String(page), limit: '20' });
60+
const filter = statusFilterRef.current;
61+
if (filter !== 'all') params.set('status', filter);
62+
const res = await fetch(`/api/reviews?${params}`, {
63+
headers: { Authorization: `Bearer ${tokenRef.current}` },
64+
});
65+
const data = await res.json();
66+
if (cancelled) return;
67+
if (!res.ok || !data.success) {
68+
setError(data.message ?? 'Failed to fetch reviews.');
69+
setIsAuthed(false);
70+
return;
71+
}
72+
setReviews(data.reviews);
73+
setPagination(data.pagination);
74+
} catch {
75+
if (!cancelled) setError('Network error.');
76+
} finally {
77+
if (!cancelled) setLoading(false);
78+
}
79+
}
80+
load();
81+
return () => {
82+
cancelled = true;
83+
};
84+
}, [isAuthed, page, statusFilter]);
85+
86+
function handleLogin() {
87+
if (token.trim()) {
88+
localStorage.setItem('review_admin_token', token.trim());
89+
setToken(token.trim());
90+
setIsAuthed(true);
91+
}
92+
}
93+
94+
async function handleAction(id: string, action: 'approve' | 'reject' | 'delete') {
95+
if (!token) return;
96+
try {
97+
if (action === 'delete') {
98+
const res = await fetch(`/api/reviews/${id}`, {
99+
method: 'DELETE',
100+
headers: { Authorization: `Bearer ${token}` },
101+
});
102+
if (!res.ok) {
103+
const data = await res.json();
104+
setError(data.message ?? 'Failed to delete review.');
105+
return;
106+
}
107+
} else {
108+
const res = await fetch(`/api/reviews/${id}`, {
109+
method: 'PATCH',
110+
headers: {
111+
'Content-Type': 'application/json',
112+
Authorization: `Bearer ${token}`,
113+
},
114+
body: JSON.stringify({ approved: action === 'approve' }),
115+
});
116+
if (!res.ok) {
117+
const data = await res.json();
118+
setError(data.message ?? `Failed to ${action} review.`);
119+
return;
120+
}
121+
}
122+
setReviews((prev) => prev.filter((r) => r._id !== id));
123+
setPagination((prev) => (prev ? { ...prev, total: prev.total - 1 } : prev));
124+
} catch {
125+
setError('Network error.');
126+
}
127+
}
128+
129+
function handleLogout() {
130+
localStorage.removeItem('review_admin_token');
131+
setToken('');
132+
setIsAuthed(false);
133+
setReviews([]);
134+
setPagination(null);
135+
}
136+
137+
if (!isAuthed) {
138+
return (
139+
<main className="min-h-screen bg-[#030712] text-white px-6 py-20">
140+
<div className="mx-auto max-w-md space-y-8">
141+
<Link href="/" className="text-sm font-semibold text-emerald-300 hover:text-emerald-200">
142+
&larr; Back to home
143+
</Link>
144+
<h1 className="text-3xl font-extrabold">Review Admin</h1>
145+
<p className="text-white/60">Enter your admin secret to manage reviews.</p>
146+
<div className="space-y-4">
147+
<input
148+
type="password"
149+
value={token}
150+
onChange={(e) => setToken(e.target.value)}
151+
placeholder="Admin secret"
152+
className="w-full rounded-xl border border-white/10 bg-white/5 px-4 py-3 text-white placeholder:text-white/30 focus:outline-none focus:ring-2 focus:ring-emerald-500"
153+
/>
154+
<button
155+
onClick={handleLogin}
156+
className="w-full rounded-xl bg-emerald-600 px-6 py-3 font-semibold transition-colors hover:bg-emerald-500"
157+
>
158+
Sign In
159+
</button>
160+
{error && <p className="text-sm text-red-400">{error}</p>}
161+
</div>
162+
</div>
163+
</main>
164+
);
165+
}
166+
167+
return (
168+
<main className="min-h-screen bg-[#030712] text-white px-6 py-20">
169+
<div className="mx-auto max-w-5xl space-y-8">
170+
<div className="flex items-center justify-between">
171+
<div className="space-y-1">
172+
<Link href="/" className="text-sm text-emerald-300 hover:text-emerald-200">
173+
&larr; Home
174+
</Link>
175+
<h1 className="text-3xl font-extrabold">Review Admin</h1>
176+
<p className="text-white/50 text-sm">
177+
{pagination?.total ?? 0} total review{(pagination?.total ?? 0) !== 1 ? 's' : ''}
178+
</p>
179+
</div>
180+
<button
181+
onClick={handleLogout}
182+
className="rounded-lg border border-white/10 px-4 py-2 text-sm text-white/60 transition-colors hover:bg-white/5"
183+
>
184+
Sign Out
185+
</button>
186+
</div>
187+
188+
{/* Status filter tabs */}
189+
<div className="flex gap-2">
190+
{(['pending', 'approved', 'all'] as const).map((s) => (
191+
<button
192+
key={s}
193+
onClick={() => {
194+
setStatusFilter(s);
195+
setPage(1);
196+
}}
197+
className={`rounded-lg px-4 py-2 text-sm font-medium transition-colors ${
198+
statusFilter === s
199+
? 'bg-emerald-600 text-white'
200+
: 'bg-white/5 text-white/50 hover:bg-white/10'
201+
}`}
202+
>
203+
{s.charAt(0).toUpperCase() + s.slice(1)}
204+
</button>
205+
))}
206+
</div>
207+
208+
{error && (
209+
<p className="rounded-lg bg-red-500/10 border border-red-500/20 px-4 py-3 text-sm text-red-400">
210+
{error}
211+
</p>
212+
)}
213+
214+
{loading ? (
215+
<p className="text-white/40 py-12 text-center">Loading...</p>
216+
) : reviews.length === 0 ? (
217+
<p className="text-white/40 py-12 text-center">No reviews found.</p>
218+
) : (
219+
<div className="space-y-4">
220+
{reviews.map((review) => (
221+
<div
222+
key={review._id}
223+
className="rounded-2xl border border-white/10 bg-white/[0.03] p-6 space-y-4"
224+
>
225+
<div className="flex items-start justify-between gap-4">
226+
<div className="flex items-start gap-3 min-w-0">
227+
<div
228+
className="h-10 w-10 rounded-full flex items-center justify-center text-sm font-bold text-white flex-shrink-0"
229+
style={{ backgroundColor: review.accentColor }}
230+
>
231+
{review.name.charAt(0).toUpperCase()}
232+
</div>
233+
<div className="min-w-0">
234+
<p className="font-bold truncate">{review.name}</p>
235+
<p className="text-sm text-white/50">
236+
{review.handle} &middot; {review.platform}
237+
</p>
238+
</div>
239+
</div>
240+
<span
241+
className={`rounded-full px-3 py-1 text-xs font-semibold flex-shrink-0 ${
242+
review.approved
243+
? 'bg-emerald-500/20 text-emerald-300'
244+
: 'bg-amber-500/20 text-amber-300'
245+
}`}
246+
>
247+
{review.approved ? 'Approved' : 'Pending'}
248+
</span>
249+
</div>
250+
251+
<p className="text-sm text-white/70 leading-relaxed">{review.message}</p>
252+
253+
<p className="text-xs text-white/30">
254+
{new Date(review.createdAt).toLocaleDateString('en-US', {
255+
year: 'numeric',
256+
month: 'short',
257+
day: 'numeric',
258+
hour: '2-digit',
259+
minute: '2-digit',
260+
})}
261+
</p>
262+
263+
<div className="flex gap-2 pt-1">
264+
{!review.approved && (
265+
<button
266+
onClick={() => handleAction(review._id, 'approve')}
267+
className="rounded-lg bg-emerald-600 px-4 py-2 text-sm font-semibold transition-colors hover:bg-emerald-500"
268+
>
269+
Approve
270+
</button>
271+
)}
272+
{review.approved && (
273+
<button
274+
onClick={() => handleAction(review._id, 'reject')}
275+
className="rounded-lg bg-amber-600 px-4 py-2 text-sm font-semibold transition-colors hover:bg-amber-500"
276+
>
277+
Reject
278+
</button>
279+
)}
280+
<button
281+
onClick={() => handleAction(review._id, 'delete')}
282+
className="rounded-lg bg-red-600/80 px-4 py-2 text-sm font-semibold transition-colors hover:bg-red-500"
283+
>
284+
Delete
285+
</button>
286+
</div>
287+
</div>
288+
))}
289+
</div>
290+
)}
291+
292+
{pagination && pagination.totalPages > 1 && (
293+
<div className="flex items-center justify-center gap-4 pt-4">
294+
<button
295+
onClick={() => setPage((p) => Math.max(1, p - 1))}
296+
disabled={pagination.page <= 1}
297+
className="rounded-lg bg-white/5 px-4 py-2 text-sm disabled:opacity-30 hover:bg-white/10"
298+
>
299+
Previous
300+
</button>
301+
<span className="text-sm text-white/50">
302+
Page {pagination.page} of {pagination.totalPages}
303+
</span>
304+
<button
305+
onClick={() => setPage((p) => p + 1)}
306+
disabled={pagination.page >= pagination.totalPages}
307+
className="rounded-lg bg-white/5 px-4 py-2 text-sm disabled:opacity-30 hover:bg-white/10"
308+
>
309+
Next
310+
</button>
311+
</div>
312+
)}
313+
</div>
314+
</main>
315+
);
316+
}

app/api/architecture/route.ts

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,10 @@ import pLimit from 'p-limit';
99
import { cloneGitHubRepository } from '@/lib/git-clone';
1010
import { getGitHubTokens } from '@/lib/github';
1111
import { formatRepoRefForLogging, sanitizeErrorForLogging } from '@/lib/sanitize-git-credentials';
12-
import { auth } from '@/auth';
1312
import { getClientIp } from '@/utils/getClientIp';
1413

1514
const execFilePromise = promisify(execFile);
1615

17-
const REST_TIMEOUT_MS = 5000; // 5s timeout for external API requests
18-
1916
// Per-IP concurrent clone tracking (max 3 concurrent clones per IP)
2017
const MAX_CONCURRENT_CLONES_PER_IP = 3;
2118
const MAX_TEMP_DIR_SIZE_BYTES = 500 * 1024 * 1024; // 500MB
@@ -310,12 +307,6 @@ export async function POST(req: NextRequest) {
310307
let tempDir = '';
311308
const ip = getClientIp(req);
312309

313-
// Require authenticated session
314-
const session = await auth();
315-
if (!session?.user) {
316-
return NextResponse.json({ error: 'Authentication required' }, { status: 401 });
317-
}
318-
319310
// Check concurrent clone limit per IP
320311
if (!incrementClones(ip)) {
321312
return NextResponse.json(

app/api/enterprise/route.ts

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { NextRequest, NextResponse } from 'next/server';
22
import { aggregateTeamData } from '@/lib/analytics/teamHealth';
3-
import { requireEnterpriseAdmin } from '@/lib/enterprise-auth';
43
import type { TeamMember } from '@/types/enterprise';
54
import type { ContributionCalendar } from '@/types';
65

@@ -35,9 +34,6 @@ function createMockTeamMembers(usernames: string[]): TeamMember[] {
3534
}
3635

3736
export async function GET(request: NextRequest) {
38-
const { error } = await requireEnterpriseAdmin();
39-
if (error) return error;
40-
4137
const searchParams = request.nextUrl.searchParams;
4238
const teamId = searchParams.get('teamId') || 'default-team';
4339
const teamName = searchParams.get('teamName') || 'Engineering Team';
@@ -74,9 +70,6 @@ export async function GET(request: NextRequest) {
7470
}
7571

7672
export async function POST(request: NextRequest) {
77-
const { error } = await requireEnterpriseAdmin();
78-
if (error) return error;
79-
8073
let body: Record<string, unknown>;
8174
try {
8275
body = await request.json();

0 commit comments

Comments
 (0)