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
107 changes: 48 additions & 59 deletions app/app/api/users/[id]/activity/route.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,18 @@
'use server';

import { apiError, apiSuccess } from '@/lib/api-response';

import { NextRequest } from 'next/server';
import { prisma } from '@/lib/prisma';
import { NextRequest } from "next/server";
import { prisma } from "@/lib/prisma";
import { apiSuccess, apiError } from "@/lib/api-response";
import { getCurrentUser } from "@/lib/auth";

type ActivityItem = {
id: string;
type: 'posted' | 'entered' | 'won' | 'liked';
timestamp: string; // ISO
timestamp: string;
subject: {
id: string | number;
title?: string | null;
slug?: string | null;
// for entries we include entryId
entryId?: string | number;
};
// optional raw meta for consumers that need more details (e.g. thumbnail)
meta?: Record<string, unknown>;
};

Expand All @@ -32,109 +28,102 @@ export async function GET (
return apiError('Missing user id', 400);
}

const currentUser = await getCurrentUser(request);

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

if (!user) {
return apiError("User not found", 404);
}

const isFollowing = currentUser
? await prisma.follow.findUnique({
where: {
userId_followingId: {
userId: currentUser.id,
followingId: id,
},
},
})
: null;

const canAccessProfile =
currentUser?.id === user.id ||
user.profileVisibility === 'public' ||
(user.profileVisibility === 'followers' && isFollowing);

if (!canAccessProfile) {
return apiError("Profile not accessible", 403);
}

const url = new URL(request.url);
const page = Math.max(1, parseInt(url.searchParams.get('page') || '1', 10));
let limit = parseInt(url.searchParams.get('limit') || '20', 10);
if (Number.isNaN(limit) || limit <= 0) limit = 20;
limit = Math.min(limit, MAX_LIMIT);

// Fetch activity in parallel (select only required fields)
const [posted, entered, won, liked] = await Promise.all([
prisma.post.findMany({
where: { userId: id },
select: {
id: true,
title: true,
slug: true,
createdAt: true,
},
select: { id: true, title: true, slug: true, createdAt: true },
}),
prisma.entry.findMany({
where: { userId: id },
select: {
id: true,
createdAt: true,
postId: true,
post: {
select: { id: true, title: true, slug: true },
},
id: true, createdAt: true, postId: true,
post: { select: { id: true, title: true, slug: true } },
},
}),
prisma.entry.findMany({
where: { userId: id, isWinner: true },
select: {
id: true,
createdAt: true,
postId: true,
post: {
select: { id: true, title: true, slug: true },
},
id: true, createdAt: true, postId: true,
post: { select: { id: true, title: true, slug: true } },
},
}),
prisma.interaction.findMany({
where: { userId: id, type: 'like' },
select: {
id: true,
createdAt: true,
postId: true,
post: {
select: { id: true, title: true, slug: true },
},
id: true, createdAt: true, postId: true,
post: { select: { id: true, title: true, slug: true } },
},
}),
]);

// Combine and normalize
const activities: ActivityItem[] = [
...posted.map((p) => ({
id: `post-${p.id}`,
type: 'posted' as const,
timestamp: p.createdAt.toISOString(),
subject: {
id: p.id,
title: p.title ?? null,
slug: p.slug ?? null,
},
subject: { id: p.id, title: p.title ?? null, slug: p.slug ?? null },
meta: {},
})),
...entered.map((e) => ({
id: `entry-${e.id}`,
type: 'entered' as const,
timestamp: e.createdAt.toISOString(),
subject: {
id: e.postId,
title: e.post?.title ?? null,
slug: e.post?.slug ?? null,
entryId: e.id,
},
subject: { id: e.postId, title: e.post?.title ?? null, slug: e.post?.slug ?? null, entryId: e.id },
meta: {},
})),
...won.map((e) => ({
id: `won-${e.id}`,
type: 'won' as const,
timestamp: e.createdAt.toISOString(),
subject: {
id: e.postId,
title: e.post?.title ?? null,
slug: e.post?.slug ?? null,
entryId: e.id,
},
subject: { id: e.postId, title: e.post?.title ?? null, slug: e.post?.slug ?? null, entryId: e.id },
meta: {},
})),
...liked.map((i) => ({
id: `like-${i.id}`,
type: 'liked' as const,
timestamp: i.createdAt.toISOString(),
subject: {
id: i.postId,
title: i.post?.title ?? null,
slug: i.post?.slug ?? null,
},
subject: { id: i.postId, title: i.post?.title ?? null, slug: i.post?.slug ?? null },
meta: {},
})),
];

// Sort newest first
activities.sort((a, b) => {
const ta = new Date(a.timestamp).getTime();
const tb = new Date(b.timestamp).getTime();
Expand All @@ -157,4 +146,4 @@ export async function GET (
console.error('GET /api/users/[id]/activity error', error);
return apiError('Failed to fetch activity', 500);
}
}
}
84 changes: 51 additions & 33 deletions app/app/api/users/[id]/entries/route.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { NextRequest } from 'next/server';
import { prisma } from '@/lib/prisma';
import { apiSuccess, apiError } from '@/lib/api-response';
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 GET(
request: NextRequest,
Expand All @@ -9,42 +10,59 @@ export async function GET(
try {
const { id } = await params;

// Try to fetch from database first
try {
// Verify user exists
const userExists = await prisma.user.findUnique({
where: { id },
select: { id: true },
});

if (!userExists) {
return apiError('User not found', 404);
}

// Fetch user's entries
const userEntries = await prisma.entry.findMany({
where: { userId: id },
orderBy: { createdAt: 'desc' },
include: {
post: {
select: {
id: true,
title: true,
type: true,
status: true,
createdAt: true,
const currentUser = await getCurrentUser(request);

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

if (!user) {
return apiError("User not found", 404);
}

const isFollowing = currentUser
? await prisma.follow.findUnique({
where: {
userId_followingId: {
userId: currentUser.id,
followingId: id,
},
},
},
});
})
: null;

return apiSuccess(userEntries);
const canAccessProfile =
currentUser?.id === user.id ||
user.profileVisibility === 'public' ||
(user.profileVisibility === 'followers' && isFollowing);

} catch (dbError) {
return apiError('Failed to fetch user entries', 500);
if (!canAccessProfile) {
return apiError("Profile not accessible", 403);
}

// Fetch user's entries
const userEntries = await prisma.entry.findMany({
where: { userId: id },
orderBy: { createdAt: "desc" },
include: {
post: {
select: {
id: true,
title: true,
type: true,
status: true,
createdAt: true,
},
},
},
});

return apiSuccess(userEntries);
} catch (error) {
return apiError('Failed to fetch user entries', 500);
return apiError("Failed to fetch user entries", 500);
}
}
48 changes: 44 additions & 4 deletions app/app/api/users/[id]/followers/route.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { NextRequest } from 'next/server';
import { prisma } from '@/lib/prisma';
import { apiSuccess, apiError } from '@/lib/api-response';
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 GET(
request: NextRequest,
Expand All @@ -12,6 +13,45 @@ export async function GET(
const limit = parseInt(searchParams.get('limit') || '20');
const skip = parseInt(searchParams.get('skip') || '0');

const currentUser = await getCurrentUser(request);

const targetUser = await prisma.user.findUnique({
where: { id: targetUserId },
select: {
id: true,
profileVisibility: true,
showEmail: true,
showWalletAddress: true,
},
});

if (!targetUser) {
return apiError("User not found", 404);
}

const isFollowing = currentUser
? await prisma.follow.findUnique({
where: {
userId_followingId: {
userId: currentUser.id,
followingId: targetUserId,
},
},
})
: null;

const canAccessFollowers =
currentUser?.id === targetUser.id ||
(targetUser.profileVisibility === 'public') ||
(targetUser.profileVisibility === 'followers' && isFollowing);

if (!canAccessFollowers) {
return apiError("Cannot access followers list", 403);
}

// Determine if we should include walletAddress based on user relationship and privacy settings
const shouldIncludeWalletAddress = currentUser?.id === targetUserId || targetUser.showWalletAddress;

// Followers are users who follow the target user
// i.e., followingId = targetUserId
// we want to return the `user` relation (the follower)
Expand All @@ -25,8 +65,8 @@ export async function GET(
name: true,
avatarUrl: true,
xp: true,
walletAddress: true,
bio: true,
...(shouldIncludeWalletAddress && { walletAddress: true }),
}
}
},
Expand Down
Loading
Loading