diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..c43849e --- /dev/null +++ b/.gitattributes @@ -0,0 +1,12 @@ +# Normalize line endings for all text files +* text=auto + +# Force LF for code files (prevents CRLF issues on Windows) +*.js text eol=lf +*.ts text eol=lf +*.tsx text eol=lf +*.json text eol=lf +*.css text eol=lf +*.md text eol=lf +*.yml text eol=lf +*.yaml text eol=lf \ No newline at end of file diff --git a/src/app/(app)/issues/issues-list.tsx b/src/app/(app)/issues/issues-list.tsx index 2d7ade9..166fe38 100644 --- a/src/app/(app)/issues/issues-list.tsx +++ b/src/app/(app)/issues/issues-list.tsx @@ -11,6 +11,8 @@ import { type RepoOption, } from '@/app/actions/issues'; + + const DIFFICULTY_LABEL: Record = { E: 'L1', M: 'L2', H: 'L3' }; const DIFFICULTY_COLOR: Record = { E: 'border-emerald-700 text-emerald-400', @@ -33,11 +35,13 @@ function IssueCard({ onClaim, onUnclaim, actionPending, + onOpen, }: { issue: IssueWithStatus; onClaim: (id: number) => void; onUnclaim: (recId: number) => void; actionPending: boolean; + onOpen: (issue: IssueWithStatus) => void; }) { const isClaimed = issue.userRecStatus === 'claimed'; const repoName = issue.repoFullName.split('/')[1] ?? issue.repoFullName; @@ -82,14 +86,12 @@ function IssueCard({ - onOpen(issue)} + className="mb-3 block cursor-pointer font-serif text-xl leading-snug text-white hover:text-zinc-300" > {issue.title} - + {issue.labels && issue.labels.length > 0 && (
@@ -170,6 +172,10 @@ export function IssuesList({ const [actionIssueId, setActionIssueId] = useState(null); const [actionError, setActionError] = useState(null); + + const [selectedIssue, setSelectedIssue] = useState(null); + const [open, setOpen] = useState(false); + const [search, setSearch] = useState(initialFilters.search ?? ''); const [state, setState] = useState<'open' | 'closed'>(initialFilters.state ?? 'open'); const [difficulty, setDifficulty] = useState(initialFilters.difficulty ?? ''); @@ -223,6 +229,8 @@ export function IssuesList({ router.refresh(); }; + + const handleUnclaim = async (recId: number, issueId: number) => { setActionIssueId(issueId); setActionError(null); @@ -240,6 +248,77 @@ export function IssuesList({ return (
+
+ {initialData.issues.map((issue) => ( + handleUnclaim(recId, issue.id)} + actionPending={actionIssueId === issue.id} + onOpen={(issue) => { + setSelectedIssue(issue); + setOpen(true); + }} + /> + ))} +
+ + {open && selectedIssue && ( +
+ + {/* BACKDROP */} +
setOpen(false)} + /> + + {/* MODAL */} +
+ +

+ {selectedIssue.title} +

+ +

+ {selectedIssue.body || "No description available."} +

+ +
+

Repo: {selectedIssue.repoFullName}

+

Difficulty: {selectedIssue.difficulty}

+

XP: {selectedIssue.xpReward}

+
+ + + Open on GitHub + + +
+ + + + + +
+
+
+ )} + + {/* Filters */}
diff --git a/src/app/api/webhooks/github/route.ts b/src/app/api/webhooks/github/route.ts index 3786860..ab76638 100644 --- a/src/app/api/webhooks/github/route.ts +++ b/src/app/api/webhooks/github/route.ts @@ -3,6 +3,7 @@ import { NextResponse, type NextRequest } from 'next/server'; import { verifyWebhookSignature } from '@/lib/github/webhook-verify'; import { getServiceSupabase } from '@/lib/supabase/service'; import { inngest } from '@/inngest/client'; +import { rateLimit } from '@/lib/rate-limit'; /** * GitHub App webhook receiver. @@ -27,6 +28,18 @@ export async function POST(req: NextRequest) { return NextResponse.json({ error: 'missing required headers' }, { status: 400 }); } + const ip = + req.headers.get('x-forwarded-for')?.split(',')[0] || + req.headers.get('x-real-ip') || + 'unknown'; + + await rateLimit({ + namespace: 'webhook', + key: ip, + limit: 100, + windowSec: 60, + }); + const raw = await req.text(); if (!verifyWebhookSignature(raw, signature, secret)) {