Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -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
91 changes: 85 additions & 6 deletions src/app/(app)/issues/issues-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
type RepoOption,
} from '@/app/actions/issues';



const DIFFICULTY_LABEL: Record<string, string> = { E: 'L1', M: 'L2', H: 'L3' };
const DIFFICULTY_COLOR: Record<string, string> = {
E: 'border-emerald-700 text-emerald-400',
Expand All @@ -33,11 +35,13 @@
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;
Expand Down Expand Up @@ -82,14 +86,12 @@
</span>
</div>

<a
href={issue.url}
target="_blank"
rel="noopener noreferrer"
className="mb-3 block font-serif text-xl leading-snug text-white hover:text-zinc-300"
<div
onClick={() => onOpen(issue)}
className="mb-3 block cursor-pointer font-serif text-xl leading-snug text-white hover:text-zinc-300"
>
{issue.title}
</a>
</div>

{issue.labels && issue.labels.length > 0 && (
<div className="mb-4 flex flex-wrap gap-2">
Expand Down Expand Up @@ -170,6 +172,10 @@
const [actionIssueId, setActionIssueId] = useState<number | null>(null);
const [actionError, setActionError] = useState<string | null>(null);


const [selectedIssue, setSelectedIssue] = useState<IssueWithStatus | null>(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<string>(initialFilters.difficulty ?? '');
Expand Down Expand Up @@ -223,6 +229,8 @@
router.refresh();
};



const handleUnclaim = async (recId: number, issueId: number) => {
setActionIssueId(issueId);
setActionError(null);
Expand All @@ -240,6 +248,77 @@

return (
<div>
<div>
{initialData.issues.map((issue) => (
<IssueCard
key={issue.id}
issue={issue}
onClaim={handleClaim}
onUnclaim={(recId) => handleUnclaim(recId, issue.id)}
actionPending={actionIssueId === issue.id}
onOpen={(issue) => {
setSelectedIssue(issue);
setOpen(true);
}}
/>
))}
</div>

{open && selectedIssue && (
<div className="fixed inset-0 z-50 flex items-center justify-center">

{/* BACKDROP */}
<div
className="absolute inset-0 bg-black/70"
onClick={() => setOpen(false)}
/>

{/* MODAL */}
<div className="relative z-50 w-[520px] rounded-lg border border-[#2d333b] bg-[#161b22] p-5">

<h2 className="mb-3 text-lg font-bold text-white">
{selectedIssue.title}
</h2>

<p className="mb-4 text-sm text-zinc-400">
{selectedIssue.body || "No description available."}

Check failure on line 284 in src/app/(app)/issues/issues-list.tsx

View workflow job for this annotation

GitHub Actions / check

Property 'body' does not exist on type 'IssueWithStatus'.
</p>

<div className="mb-4 text-[11px] uppercase tracking-widest text-zinc-500">
<p>Repo: {selectedIssue.repoFullName}</p>
<p>Difficulty: {selectedIssue.difficulty}</p>
<p>XP: {selectedIssue.xpReward}</p>
</div>

<a
href={selectedIssue.url}
target="_blank"
className="text-[11px] uppercase tracking-widest text-blue-400 underline"
>
Open on GitHub
</a>

<div className="mt-4 flex gap-2">

<button
onClick={() => {
handleClaim(selectedIssue.id);
setOpen(false);
}}
>
CLAIM
</button>

<button onClick={() => setOpen(false)}>
SKIP
</button>

</div>
</div>
</div>
)}


{/* Filters */}
<div className="mb-10 flex flex-wrap items-center gap-3">
<div className="relative min-w-[180px] flex-1">
Expand Down Expand Up @@ -332,7 +411,7 @@
</div>
) : (
initialData.issues.map((issue) => (
<IssueCard

Check failure on line 414 in src/app/(app)/issues/issues-list.tsx

View workflow job for this annotation

GitHub Actions / check

Property 'onOpen' is missing in type '{ key: number; issue: IssueWithStatus; onClaim: (issueId: number) => Promise<void>; onUnclaim: (recId: number) => Promise<void>; actionPending: boolean; }' but required in type '{ issue: IssueWithStatus; onClaim: (id: number) => void; onUnclaim: (recId: number) => void; actionPending: boolean; onOpen: (issue: IssueWithStatus) => void; }'.
key={issue.id}
issue={issue}
onClaim={handleClaim}
Expand Down
13 changes: 13 additions & 0 deletions src/app/api/webhooks/github/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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)) {
Expand Down
Loading