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
36 changes: 36 additions & 0 deletions app/app/api/comments/[id]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { NextRequest } from 'next/server';
import { prisma } from '@/lib/prisma';
import { apiSuccess, apiError } from '@/lib/api-response';
import { getCurrentUser } from '@/lib/auth';

export async function DELETE(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> },
) {
try {
const currentUser = await getCurrentUser(request);
if (!currentUser) return apiError('Unauthorized', 401);

const { id } = await params;

const comment = await prisma.comment.findUnique({
where: { id }
});

if (!comment) {
return apiError('Comment not found', 404);
}

if (comment.userId !== currentUser.id) {
return apiError('Forbidden', 403);
}

await prisma.comment.delete({
where: { id }
});

return apiSuccess({ success: true });
} catch (error) {
return apiError('Failed to delete comment', 500);
}
}
99 changes: 99 additions & 0 deletions app/app/api/posts/[id]/comments/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { NextRequest } from 'next/server';
import { prisma } from '@/lib/prisma';
import { apiSuccess, apiError } from '@/lib/api-response';
import { getCurrentUser } from '@/lib/auth';
import { readJsonBody } from '@/lib/parse-json-body';

export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> },
) {
try {
const { id } = await params;
const { searchParams } = new URL(request.url);
const page = parseInt(searchParams.get('page') || '1');
const limit = parseInt(searchParams.get('limit') || '50');
const skip = (page - 1) * limit;

const [comments, total] = await Promise.all([
prisma.comment.findMany({
where: { postId: id, parentId: null },
include: {
user: {
select: { id: true, name: true, avatarUrl: true, rank: true }
},
replies: {
include: {
user: {
select: { id: true, name: true, avatarUrl: true, rank: true }
}
},
orderBy: { createdAt: 'asc' }
}
},
orderBy: { createdAt: 'desc' },
skip,
take: limit,
}),
prisma.comment.count({
where: { postId: id, parentId: null }
})
]);

return apiSuccess({
comments,
pagination: {
page,
limit,
total,
totalPages: Math.ceil(total / limit)
}
});
} catch (error) {
return apiError('Failed to fetch comments', 500);
}
}

export async function POST(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> },
) {
try {
const currentUser = await getCurrentUser(request);
if (!currentUser) return apiError('Unauthorized', 401);

const { id } = await params;
const body = await readJsonBody(request);
if (!body.ok) return body.response;

const { content, parentId, entryId } = body.data as any;

if (!content || typeof content !== 'string') {
return apiError('Content is required', 400);
}

const comment = await prisma.comment.create({
data: {
content,
postId: id,
userId: currentUser.id,
parentId: parentId || null,
entryId: entryId || null,
},
include: {
user: {
select: { id: true, name: true, avatarUrl: true, rank: true }
},
replies: {
include: {
user: true
}
}
}
});

return apiSuccess(comment);
} catch (error) {
return apiError('Failed to create comment', 500);
}
}
163 changes: 163 additions & 0 deletions app/app/api/posts/[id]/contributions/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
import { NextRequest } from 'next/server';
import { prisma } from '@/lib/prisma';
import { apiSuccess, apiError } from '@/lib/api-response';
import { getCurrentUser } from '@/lib/auth';
import { readJsonBody } from '@/lib/parse-json-body';
import { XP_REWARDS, awardXp } from '@/lib/xp';
import { checkAndAwardBadges } from '@/lib/badges';

export async function POST(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> },
) {
try {
const currentUser = await getCurrentUser(request);
if (!currentUser) return apiError('Unauthorized', 401);

const { id: postId } = await params;
const body = await readJsonBody<Record<string, unknown>>(request);
if (!body.ok) return body.response;

const { amount, message, isAnonymous } = body.data as any;
const parsedAmount = Number(amount);

if (isNaN(parsedAmount) || parsedAmount <= 0) {
return apiError('Amount must be greater than 0', 400);
}

const post = await prisma.post.findUnique({
where: { id: postId },
select: { id: true, type: true, targetAmount: true, status: true, userId: true },
});

if (!post) {
return apiError('Post not found', 404);
}

if (post.type !== 'request') {
return apiError('Contributions can only be made to help requests', 400);
}

if (!['open', 'in_progress', 'active'].includes(post.status)) {
return apiError('Help request is not accepting contributions', 400);
}

const contribution = await prisma.$transaction(async (tx) => {
const created = await tx.helpContribution.create({
data: {
postId,
userId: currentUser.id,
amount: parsedAmount,
message: message || null,
isAnonymous: !!isAnonymous,
contributedAt: new Date(),
},
include: {
user: {
select: { id: true, name: true, avatarUrl: true, rank: true }
}
}
});

await awardXp(
currentUser.id,
XP_REWARDS.contributeToHelpRequest,
'help_request_contributed',
{ metadata: { postId, contributionId: created.id } },
tx
);

// Award the creator for receiving a contribution
await awardXp(
post.userId,
XP_REWARDS.receiveContribution,
'help_request_received_contribution',
{ metadata: { postId, contributionId: created.id } },
tx
);

// Optional: notification to creator
if (post.userId !== currentUser.id && tx.notification?.create) {
try {
await tx.notification.create({
data: {
userId: post.userId,
type: 'help_contribution',
message: `${isAnonymous ? 'Someone' : currentUser.name} contributed to your help request`,
link: `/post/${postId}`,
}
});
} catch(e) { /* ignore if table missing */ }
}

return created;
});

// Check badges
checkAndAwardBadges(currentUser.id).catch(console.error);

// Calculate updated progress
const aggregate = await prisma.helpContribution.aggregate({
where: { postId },
_sum: { amount: true }
});
const totalRaised = aggregate._sum.amount || 0;

return apiSuccess({
contribution,
progress: {
totalRaised,
targetAmount: post.targetAmount,
percentage: post.targetAmount ? Math.min(100, Math.round((totalRaised / post.targetAmount) * 100)) : 0
}
}, 'Contribution successful', 201);
} catch (error) {
console.error('Contribution error:', error);
return apiError('Failed to process contribution', 500);
}
}

export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> },
) {
try {
const { id: postId } = await params;

const post = await prisma.post.findUnique({
where: { id: postId },
select: { id: true }
});

if (!post) {
return apiError('Post not found', 404);
}

const contributions = await prisma.helpContribution.findMany({
where: { postId },
include: {
user: {
select: { id: true, name: true, avatarUrl: true, rank: true }
}
},
orderBy: { contributedAt: 'desc' }
});

const aggregate = await prisma.helpContribution.aggregate({
where: { postId },
_sum: { amount: true }
});
const totalRaised = aggregate._sum.amount || 0;

return apiSuccess({
contributions: contributions.map(c => ({
...c,
user: c.isAnonymous ? { id: 'anonymous', name: 'Anonymous', avatarUrl: null, rank: null } : c.user
})),
totalRaised
});
} catch (error) {
console.error('Fetch contributions error:', error);
return apiError('Failed to fetch contributions', 500);
}
}
3 changes: 3 additions & 0 deletions app/app/api/posts/[id]/entries/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { NextRequest } from 'next/server';
import { getCurrentUser } from '@/lib/auth';
import { prisma } from '@/lib/prisma';
import { readJsonBody } from '@/lib/parse-json-body';
import { checkAndAwardBadges } from '@/lib/badges';

/**
* POST /api/posts/[id]/entries
Expand Down Expand Up @@ -129,6 +130,8 @@ export async function POST (
return createdEntry;
});

checkAndAwardBadges(user.id).catch(console.error);

return apiSuccess(entry, 'Entry created successfully', 201);
} catch (error) {
console.error('Error creating entry:', error);
Expand Down
16 changes: 15 additions & 1 deletion app/app/api/posts/[id]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,14 +51,28 @@ const GET = async (
}
}
},
contributions: {
include: {
user: {
select: {
id: true,
name: true,
avatarUrl: true,
username: true,
}
}
}
},
},
});

if (!post) {
return apiError('Post not found', 404);
}

return apiSuccess(post);
const currentAmount = post.contributions?.reduce((sum, c) => sum + c.amount, 0) || 0;

return apiSuccess({ ...post, currentAmount });
} catch (error) {
return apiError('Failed to fetch post', 500);
}
Expand Down
5 changes: 5 additions & 0 deletions app/app/api/posts/[id]/select-winners/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { NextRequest } from "next/server";
import { getCurrentUser } from "@/lib/auth";
import { prisma } from "@/lib/prisma";
import { readJsonBody } from "@/lib/parse-json-body";
import { checkAndAwardBadges } from "@/lib/badges";

export const POST = async (
request: NextRequest,
Expand Down Expand Up @@ -97,6 +98,10 @@ export const POST = async (

const selectedUsers = selectedEntries.map(e => e.userId);

for (const winnerId of selectedUsers) {
checkAndAwardBadges(winnerId).catch(console.error);
}

return apiSuccess({
message: 'Winners selected successfully',
winners: selectedUsers
Expand Down
3 changes: 3 additions & 0 deletions app/app/api/posts/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { getCurrentUser } from "@/lib/auth";
import { prisma } from "@/lib/prisma";
import { readJsonBody } from "@/lib/parse-json-body";
import { POST_SLUG_MAX_LENGTH, sanitizePostSlug } from "@/lib/post-slug";
import { checkAndAwardBadges } from "@/lib/badges";

const SLUG_SUFFIX_LENGTH = 6;

Expand Down Expand Up @@ -174,6 +175,8 @@ const POST = async (request: NextRequest) => {
return createdPost;
});

checkAndAwardBadges(user.id).catch(console.error);

return apiSuccess(post, "Post created successfully", 201);
} catch (error) {
if (
Expand Down
2 changes: 2 additions & 0 deletions app/app/api/users/[id]/follow/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { NextRequest } from 'next/server';
import { prisma } from '@/lib/prisma';
import { apiSuccess, apiError } from '@/lib/api-response';
import { getCurrentUser } from '@/lib/auth';
import { checkAndAwardBadges } from '@/lib/badges';

export async function POST(
request: NextRequest,
Expand Down Expand Up @@ -43,6 +44,7 @@ export async function POST(
followingId: targetUserId,
}
});
checkAndAwardBadges(targetUserId).catch(console.error);
return apiSuccess({ success: true, follow }, undefined, 201);
}

Expand Down
Loading
Loading