Skip to content

Commit 07e083b

Browse files
authored
Merge branch 'main' into copilot/add-badge-styling-feature
2 parents 31fd37a + e217885 commit 07e083b

31 files changed

Lines changed: 8019 additions & 71 deletions

app/api/resources/[id]/route.ts

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import { NextRequest, NextResponse } from "next/server";
2+
import { getServerSession } from "next-auth";
3+
import { authOptions } from "@/lib/auth";
4+
import {
5+
updateResource,
6+
deleteResource,
7+
incrementResourceViews,
8+
incrementResourceShares,
9+
incrementResourceBookmarks,
10+
decrementResourceBookmarks,
11+
} from "@/lib/firestore/resources";
12+
import { isAdminUser } from "@/lib/admin";
13+
14+
/**
15+
* PATCH /api/resources/[id]
16+
*
17+
* Body (all fields optional):
18+
* { action: "increment_views" | "increment_shares" | "bookmark" | "unbookmark" }
19+
* { description, pinned, category, source, author, title, url, readTime, thumbnail }
20+
* Admin-only: description, pinned, category, source, author, title, url, readTime, thumbnail
21+
*/
22+
export async function PATCH(
23+
request: NextRequest,
24+
{ params }: { params: Promise<{ id: string }> }
25+
) {
26+
const session = await getServerSession(authOptions);
27+
if (!session) {
28+
return NextResponse.json({ error: "Unauthorised" }, { status: 401 });
29+
}
30+
31+
const { id } = await params;
32+
const body: Record<string, unknown> = await request.json();
33+
const userId = (session.user as { id?: string }).id;
34+
35+
try {
36+
const { action, ...patch } = body;
37+
38+
if (action === "increment_views") {
39+
await incrementResourceViews(id);
40+
return NextResponse.json({ ok: true });
41+
}
42+
43+
if (action === "increment_shares") {
44+
await incrementResourceShares(id);
45+
return NextResponse.json({ ok: true });
46+
}
47+
48+
if (action === "bookmark") {
49+
await incrementResourceBookmarks(id);
50+
return NextResponse.json({ ok: true });
51+
}
52+
53+
if (action === "unbookmark") {
54+
await decrementResourceBookmarks(id);
55+
return NextResponse.json({ ok: true });
56+
}
57+
58+
// Admin-only metadata edits
59+
if (!isAdminUser(userId)) {
60+
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
61+
}
62+
63+
await updateResource(id, patch as Parameters<typeof updateResource>[1]);
64+
return NextResponse.json({ ok: true });
65+
} catch (err) {
66+
console.error(`[PATCH /api/resources/${id}]`, err);
67+
return NextResponse.json({ error: "Internal server error" }, { status: 500 });
68+
}
69+
}
70+
71+
/** DELETE /api/resources/[id] — admin only */
72+
export async function DELETE(
73+
_request: NextRequest,
74+
{ params }: { params: Promise<{ id: string }> }
75+
) {
76+
const session = await getServerSession(authOptions);
77+
if (!session) {
78+
return NextResponse.json({ error: "Unauthorised" }, { status: 401 });
79+
}
80+
81+
const userId = (session.user as { id?: string }).id;
82+
if (!isAdminUser(userId)) {
83+
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
84+
}
85+
86+
const { id } = await params;
87+
88+
try {
89+
await deleteResource(id);
90+
return NextResponse.json({ ok: true });
91+
} catch (err) {
92+
console.error(`[DELETE /api/resources/${id}]`, err);
93+
return NextResponse.json({ error: "Internal server error" }, { status: 500 });
94+
}
95+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { NextRequest, NextResponse } from "next/server";
2+
import { getServerSession } from "next-auth";
3+
import { authOptions } from "@/lib/auth";
4+
import { deleteFeaturedCollection } from "@/lib/firestore/resources";
5+
import { isAdminUser } from "@/lib/admin";
6+
7+
/** DELETE /api/resources/collections/[id] — admin only */
8+
export async function DELETE(
9+
_request: NextRequest,
10+
{ params }: { params: Promise<{ id: string }> }
11+
) {
12+
const session = await getServerSession(authOptions);
13+
if (!session) {
14+
return NextResponse.json({ error: "Unauthorised" }, { status: 401 });
15+
}
16+
17+
const userId = (session.user as { id?: string }).id;
18+
if (!isAdminUser(userId)) {
19+
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
20+
}
21+
22+
const { id } = await params;
23+
24+
try {
25+
await deleteFeaturedCollection(id);
26+
return NextResponse.json({ ok: true });
27+
} catch (err) {
28+
console.error(`[DELETE /api/resources/collections/${id}]`, err);
29+
return NextResponse.json({ error: "Internal server error" }, { status: 500 });
30+
}
31+
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { NextRequest, NextResponse } from "next/server";
2+
import { getServerSession } from "next-auth";
3+
import { authOptions } from "@/lib/auth";
4+
import {
5+
getFeaturedCollections,
6+
createFeaturedCollection,
7+
} from "@/lib/firestore/resources";
8+
import { isAdminUser } from "@/lib/admin";
9+
10+
/** GET /api/resources/collections — returns all featured collections */
11+
export async function GET() {
12+
const session = await getServerSession(authOptions);
13+
if (!session) {
14+
return NextResponse.json({ error: "Unauthorised" }, { status: 401 });
15+
}
16+
17+
try {
18+
const collections = await getFeaturedCollections();
19+
return NextResponse.json(collections);
20+
} catch (err) {
21+
console.error("[GET /api/resources/collections]", err);
22+
return NextResponse.json({ error: "Internal server error" }, { status: 500 });
23+
}
24+
}
25+
26+
/**
27+
* POST /api/resources/collections — admin only
28+
* Body: { title: string; description: string; resourceIds?: string[] }
29+
*/
30+
export async function POST(request: NextRequest) {
31+
const session = await getServerSession(authOptions);
32+
if (!session) {
33+
return NextResponse.json({ error: "Unauthorised" }, { status: 401 });
34+
}
35+
36+
const userId = (session.user as { id?: string }).id;
37+
if (!isAdminUser(userId)) {
38+
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
39+
}
40+
41+
const body: { title?: string; description?: string; resourceIds?: string[] } =
42+
await request.json();
43+
44+
if (!body.title?.trim()) {
45+
return NextResponse.json({ error: "title is required" }, { status: 400 });
46+
}
47+
48+
try {
49+
const id = await createFeaturedCollection({
50+
title: body.title.trim(),
51+
description: body.description?.trim() ?? "",
52+
resourceIds: body.resourceIds ?? [],
53+
});
54+
return NextResponse.json({ id }, { status: 201 });
55+
} catch (err) {
56+
console.error("[POST /api/resources/collections]", err);
57+
return NextResponse.json({ error: "Internal server error" }, { status: 500 });
58+
}
59+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { NextRequest, NextResponse } from "next/server";
2+
import { getServerSession } from "next-auth";
3+
import { authOptions } from "@/lib/auth";
4+
import { approveSubmission, rejectSubmission } from "@/lib/firestore/resources";
5+
import { isAdminUser } from "@/lib/admin";
6+
import type { RejectReason } from "@/lib/resources";
7+
8+
/**
9+
* PATCH /api/resources/pending/[id]
10+
*
11+
* Body: { action: "approve" } | { action: "reject"; reason: RejectReason }
12+
*/
13+
export async function PATCH(
14+
request: NextRequest,
15+
{ params }: { params: Promise<{ id: string }> }
16+
) {
17+
const session = await getServerSession(authOptions);
18+
if (!session) {
19+
return NextResponse.json({ error: "Unauthorised" }, { status: 401 });
20+
}
21+
22+
const userId = (session.user as { id?: string }).id;
23+
if (!isAdminUser(userId)) {
24+
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
25+
}
26+
27+
const { id } = await params;
28+
const body: { action: string; reason?: RejectReason } = await request.json();
29+
30+
try {
31+
if (body.action === "approve") {
32+
await approveSubmission(id);
33+
return NextResponse.json({ ok: true });
34+
}
35+
36+
if (body.action === "reject") {
37+
if (!body.reason) {
38+
return NextResponse.json({ error: "reason is required" }, { status: 400 });
39+
}
40+
await rejectSubmission(id, body.reason);
41+
return NextResponse.json({ ok: true });
42+
}
43+
44+
return NextResponse.json({ error: "Unknown action" }, { status: 400 });
45+
} catch (err) {
46+
console.error(`[PATCH /api/resources/pending/${id}]`, err);
47+
return NextResponse.json({ error: "Internal server error" }, { status: 500 });
48+
}
49+
}

app/api/resources/pending/route.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { NextResponse } from "next/server";
2+
import { getServerSession } from "next-auth";
3+
import { authOptions } from "@/lib/auth";
4+
import { getPendingSubmissions } from "@/lib/firestore/resources";
5+
import { isAdminUser } from "@/lib/admin";
6+
7+
/** GET /api/resources/pending — admin only */
8+
export async function GET() {
9+
const session = await getServerSession(authOptions);
10+
if (!session) {
11+
return NextResponse.json({ error: "Unauthorised" }, { status: 401 });
12+
}
13+
14+
const userId = (session.user as { id?: string }).id;
15+
if (!isAdminUser(userId)) {
16+
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
17+
}
18+
19+
try {
20+
const pending = await getPendingSubmissions();
21+
return NextResponse.json(pending);
22+
} catch (err) {
23+
console.error("[GET /api/resources/pending]", err);
24+
return NextResponse.json({ error: "Internal server error" }, { status: 500 });
25+
}
26+
}

app/api/resources/route.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { NextResponse } from "next/server";
2+
import { getServerSession } from "next-auth";
3+
import { authOptions } from "@/lib/auth";
4+
import {
5+
getResources,
6+
seedResourcesIfEmpty,
7+
} from "@/lib/firestore/resources";
8+
9+
/** GET /api/resources — list all approved resources */
10+
export async function GET() {
11+
const session = await getServerSession(authOptions);
12+
if (!session) {
13+
return NextResponse.json({ error: "Unauthorised" }, { status: 401 });
14+
}
15+
16+
try {
17+
await seedResourcesIfEmpty();
18+
const resources = await getResources();
19+
return NextResponse.json(resources);
20+
} catch (err) {
21+
console.error("[GET /api/resources]", err);
22+
return NextResponse.json({ error: "Internal server error" }, { status: 500 });
23+
}
24+
}

app/dashboard/page.tsx

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import { redirect } from "next/navigation";
22
import { getServerSession } from "next-auth";
33
import { authOptions } from "@/lib/auth";
44
import { DashboardClient } from "@/components/DashboardClient";
5+
import { getResources, seedResourcesIfEmpty } from "@/lib/firestore/resources";
6+
import type { Resource } from "@/lib/resources";
57

68
export default async function DashboardPage() {
79
const session = await getServerSession(authOptions);
@@ -10,5 +12,15 @@ export default async function DashboardPage() {
1012
redirect("/");
1113
}
1214

13-
return <DashboardClient session={session} />;
15+
let trendingResources: Resource[] = [];
16+
try {
17+
await seedResourcesIfEmpty();
18+
const all = await getResources();
19+
trendingResources = [...all].sort((a, b) => b.views - a.views).slice(0, 3);
20+
} catch {
21+
// If Firestore is unavailable (e.g. env vars not configured), fall back to
22+
// an empty list — the widget handles this gracefully.
23+
}
24+
25+
return <DashboardClient session={session} trendingResources={trendingResources} />;
1426
}

app/directory/page.tsx

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { redirect } from "next/navigation";
2+
import { getServerSession } from "next-auth";
3+
import { authOptions } from "@/lib/auth";
4+
import { CommunityDirectory } from "@/components/CommunityDirectory";
5+
import { Navbar } from "@/components/Navbar";
6+
7+
export default async function DirectoryPage() {
8+
const session = await getServerSession(authOptions);
9+
10+
if (!session) {
11+
redirect("/");
12+
}
13+
14+
return (
15+
<div className="min-h-screen bg-tech-dark text-white">
16+
<div
17+
className="fixed inset-0 opacity-30 pointer-events-none"
18+
style={{
19+
backgroundImage:
20+
"radial-gradient(circle at 25px 25px, rgba(59,130,246,0.15) 1px, transparent 0), radial-gradient(circle at 75px 75px, rgba(139,92,246,0.15) 1px, transparent 0)",
21+
backgroundSize: "100px 100px",
22+
}}
23+
/>
24+
<Navbar user={session.user} />
25+
<main className="relative z-10 max-w-6xl mx-auto px-6 py-8">
26+
<div className="mb-8">
27+
<h1 className="text-3xl font-bold mb-1">
28+
<span className="bg-gradient-to-r from-brand-cyan to-brand-purple bg-clip-text text-transparent">
29+
Community Directory
30+
</span>
31+
</h1>
32+
<p className="text-gray-400">
33+
Connect with fellow Debugging Disciples across faith, tech, and career.
34+
</p>
35+
</div>
36+
<CommunityDirectory />
37+
</main>
38+
</div>
39+
);
40+
}

0 commit comments

Comments
 (0)