Skip to content
Merged
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
1 change: 1 addition & 0 deletions drizzle/0002_issue_body.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ALTER TABLE "issues" ADD COLUMN IF NOT EXISTS "body" text;
34 changes: 34 additions & 0 deletions src/app/api/issues/[id]/claim/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { NextRequest, NextResponse } from "next/server";
import { verifySessionToken, COOKIE_NAME } from "@/lib/auth/session";
import { claimIssue } from "@/lib/issues/claims";

export async function POST(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
const token = request.cookies.get(COOKIE_NAME)?.value;
if (!token) {
return NextResponse.json({ error: "Authentication required to claim issues." }, { status: 401 });
}

const sessionUser = await verifySessionToken(token);
if (!sessionUser) {
return NextResponse.json({ error: "Invalid or expired session." }, { status: 401 });
}

const { id } = await params;

try {
const result = await claimIssue({
issueId: id,
userId: sessionUser.id,
});

return NextResponse.json(result);
} catch (err: any) {
return NextResponse.json(
{ error: err.message || "Failed to claim issue." },
{ status: 400 }
);
}
}
32 changes: 32 additions & 0 deletions src/app/api/issues/[id]/release/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { NextRequest, NextResponse } from "next/server";
import { getFreshAuthenticatedUser } from "@/lib/auth/authorization";
import { releaseIssueClaim } from "@/lib/issues/claims";

export async function POST(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
const freshUser = await getFreshAuthenticatedUser(request);
if (!freshUser) {
return NextResponse.json({ error: "Authentication required to release claim." }, { status: 401 });
}

const isAdmin = freshUser.role === "admin" || freshUser.role === "maintainer";

const { id } = await params;

try {
const result = await releaseIssueClaim({
issueId: id,
userId: freshUser.id,
isAdmin,
});

return NextResponse.json(result);
} catch (err: any) {
return NextResponse.json(
{ error: err.message || "Failed to release claim." },
{ status: 400 }
);
}
}
4 changes: 4 additions & 0 deletions src/app/dashboard/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { getDb, schema } from "@/lib/db";
import { eq, and, desc } from "drizzle-orm";
import { evaluateProgression, type ContributorLevel } from "@/lib/progression/rules";
import { recommendIssues } from "@/lib/recommendations/engine";
import { expireOverdueClaims } from "@/lib/issues/claims";
import { SyncContributionsButton } from "./SyncContributionsButton";
import { ProgressionPath } from "./ProgressionPath";

Expand Down Expand Up @@ -55,6 +56,9 @@ export default async function DashboardPage() {
.where(eq(schema.contributions.userId, sessionUser.id))
.orderBy(desc(schema.contributions.createdAt));

// Expire overdue claims across system
await expireOverdueClaims(db);

// Fetch active issue claims
const activeClaims = await db
.select({
Expand Down
39 changes: 24 additions & 15 deletions src/app/issues/IssueMarketplace.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"use client";

import { useState, useMemo } from "react";
import Link from "next/link";

export interface MarketplaceIssue {
id: string;
Expand Down Expand Up @@ -188,9 +189,9 @@ export function IssueMarketplace({ initialIssues }: { initialIssues: Marketplace
</div>

<h3 className="text-sm sm:text-base font-bold text-white hover:text-sky-300 transition-colors">
<a href={issue.htmlUrl} target="_blank" rel="noopener noreferrer">
<Link href={`/issues/${issue.id}`}>
{issue.title}
</a>
</Link>
</h3>

{issue.bodySnippet && (
Expand All @@ -214,20 +215,28 @@ export function IssueMarketplace({ initialIssues }: { initialIssues: Marketplace
</div>

<div className="flex sm:flex-col items-center sm:items-end gap-2.5 shrink-0 w-full sm:w-auto justify-between sm:justify-center border-t sm:border-t-0 border-slate-800 pt-3 sm:pt-0">
<a
href={issue.htmlUrl}
target="_blank"
rel="noopener noreferrer"
className="rounded-lg bg-sky-500 px-4 py-2 text-xs font-mono font-semibold text-slate-950 hover:bg-sky-400 transition-colors inline-flex items-center gap-1.5"
<Link
href={`/issues/${issue.id}`}
className="rounded-lg bg-sky-500 px-4 py-2 text-xs font-mono font-bold text-slate-950 hover:bg-sky-400 transition-colors inline-flex items-center gap-1.5 shadow-sm"
>
<span>View Issue</span>
<span>↗</span>
</a>
{issue.estimatedEffort && (
<span className="text-[10px] font-mono text-slate-500">
Est: {issue.estimatedEffort}
</span>
)}
<span>Start Contribution</span>
<span>→</span>
</Link>
<div className="flex items-center gap-2">
<a
href={issue.htmlUrl}
target="_blank"
rel="noopener noreferrer"
className="text-[11px] font-mono text-slate-400 hover:text-sky-300 transition-colors"
>
GitHub ↗
</a>
{issue.estimatedEffort && (
<span className="text-[10px] font-mono text-slate-500">
• {issue.estimatedEffort}
</span>
)}
</div>
</div>
</div>
))}
Expand Down
Loading
Loading