From 8c56c30e51fcfc4c28c515327adf3980ef7d2164 Mon Sep 17 00:00:00 2001 From: yeabnoah Date: Wed, 9 Apr 2025 15:26:27 +0300 Subject: [PATCH 1/4] working : implimented projects for other users profile --- src/app/api/users/[userId]/projects/route.ts | 73 ++++ src/components/settings/profile-setting.tsx | 330 +++++++++---------- src/components/settings/tabs/ProjectsTab.tsx | 34 +- 3 files changed, 239 insertions(+), 198 deletions(-) create mode 100644 src/app/api/users/[userId]/projects/route.ts diff --git a/src/app/api/users/[userId]/projects/route.ts b/src/app/api/users/[userId]/projects/route.ts new file mode 100644 index 00000000..bdbbd65f --- /dev/null +++ b/src/app/api/users/[userId]/projects/route.ts @@ -0,0 +1,73 @@ +import getUserSession from "@/functions/get-user"; +import { prisma } from "@/lib/prisma"; +import { NextRequest, NextResponse } from "next/server"; + +export async function GET( + request: NextRequest, + { params }: { params: { userId: string } }, +) { + try { + const session = await getUserSession(); + const cursor = request.nextUrl.searchParams.get("cursor"); + const limit = parseInt(request.nextUrl.searchParams.get("limit") || "10"); + + if (!session) { + return NextResponse.json( + { + message: "unauthorized | not logged in", + }, + { status: 400 }, + ); + } + + const [userProjects, total] = await Promise.all([ + prisma.project.findMany({ + where: { + userId: params.userId, + ...(cursor && { + id: { + lt: cursor, + }, + }), + }, + include: { + user: { + select: { + id: true, + name: true, + image: true, + }, + }, + }, + orderBy: { + id: "desc", + }, + take: limit + 1, + }), + prisma.project.count({ + where: { + userId: params.userId, + }, + }), + ]); + + const hasNextPage = userProjects.length > limit; + const items = hasNextPage ? userProjects.slice(0, -1) : userProjects; + const nextCursor = hasNextPage ? items[items.length - 1].id : null; + + return NextResponse.json({ + data: items, + pagination: { + nextCursor, + hasNextPage, + total, + }, + }); + } catch (error) { + console.error("Error fetching user projects:", error); + return NextResponse.json( + { error: "Failed to fetch user projects" }, + { status: 500 }, + ); + } +} diff --git a/src/components/settings/profile-setting.tsx b/src/components/settings/profile-setting.tsx index fffff57e..e891f920 100644 --- a/src/components/settings/profile-setting.tsx +++ b/src/components/settings/profile-setting.tsx @@ -21,8 +21,6 @@ import { Input } from "../ui/input"; import { Label } from "../ui/label"; import { AutosizeTextarea } from "../ui/resizeble-text-area"; import { Skeleton } from "../ui/skeleton"; -import { Tabs, TabsList, TabsTrigger, TabsContent } from "../ui/tabs"; -import FollowedProjects from "@/components/project/followed-projects"; const ProfileSettings = () => { const { @@ -137,193 +135,177 @@ const ProfileSettings = () => { Please provide accurate information - - - Profile - Projects - Settings - - -
-
- Cover Preview +
+
+ Cover Preview - - -
+ + +
-
-
- Profile Preview +
+
+ Profile Preview - {/*
*/} + {/*
*/} - - -
+ + +
-
- - setDisplayName(e.target.value)} - /> -
+
+ + setDisplayName(e.target.value)} + /> +
-
- - setLink(e.target.value)} - /> -
-
+
+ + setLink(e.target.value)} + />
+
+
-
-
- - {user?.country ? ( -

{user?.country?.name}

- ) : ( - setSelectedCountry(country)} - /> - )} -
-
- - setNerdAt(e.target.value)} - /> -
+
+
+ + {user?.country ? ( +

{user?.country?.name}

+ ) : ( + setSelectedCountry(country)} + /> + )} +
+
+ + setNerdAt(e.target.value)} + /> +
-
- - setBio(e.target.value)} - /> -
-
+
+ + setBio(e.target.value)} + /> +
+
-
- -
- - -
-

Followed Projects

- -
-
- {/* Settings content */} - + await mutation.mutate({ + country: user?.country ? undefined : selectedCountry, + image: uploadedImageUrl || user.image || "", + coverImage: uploadedCoverImageUrl || user.coverImage || "", + nerdAt, + bio, + displayName, + link, + firstTime: false, + }); + }} + > + Update my info + +
); diff --git a/src/components/settings/tabs/ProjectsTab.tsx b/src/components/settings/tabs/ProjectsTab.tsx index b6b25480..edd447f4 100644 --- a/src/components/settings/tabs/ProjectsTab.tsx +++ b/src/components/settings/tabs/ProjectsTab.tsx @@ -9,6 +9,7 @@ import { formatDistanceToNow } from "date-fns"; import Image from "next/image"; import { Calendar } from "lucide-react"; import { useRouter } from "next/navigation"; +import useUserProfileStore from "@/store/userProfile.store"; interface Project { id: string; @@ -96,36 +97,21 @@ function ProjectCard({ project }: { project: Project }) { } export default function ProjectsTab() { - const [activeTab, setActiveTab] = useState("followed"); + const { userProfile } = useUserProfileStore(); + const [activeTab, setActiveTab] = useState("owned"); - const { data: followedProjects, isLoading: isLoadingFollowed } = useQuery({ - queryKey: ["followed-projects"], + const { data: projects, isLoading } = useQuery({ + queryKey: ["user-projects", userProfile?.id], queryFn: async () => { - const response = await axios.get("/api/users/projects"); - return response.data.data; + if (!userProfile?.id) return { data: [] }; + const response = await axios.get(`/api/users/${userProfile.id}/projects`); + return response.data; }, + enabled: !!userProfile?.id, }); - const { data: ownedProjects, isLoading: isLoadingOwned } = useQuery({ - queryKey: ["owned-projects"], - queryFn: async () => { - const response = await axios.get("/api/users/projects/owned"); - return response.data.data; - }, - }); - - const isLoading = activeTab === "followed" ? isLoadingFollowed : isLoadingOwned; - const projects = activeTab === "followed" ? followedProjects : ownedProjects; - return (
- - - Followed Projects - My Projects - - - {isLoading ? (
{[...Array(6)].map((_, i) => ( @@ -137,7 +123,7 @@ export default function ProjectsTab() {
) : (
- {projects?.map((project: Project) => ( + {projects?.data?.map((project: Project) => ( ))}
From a3756371cd19c00842ae986f0066db71d7a06e10 Mon Sep 17 00:00:00 2001 From: yeabnoah Date: Wed, 9 Apr 2025 16:03:02 +0300 Subject: [PATCH 2/4] working : implimented following and followers for user-profile ( other users ) --- .../(app)/profile/[nerdAt]/followers/page.tsx | 69 +++++++++++++++++++ .../(app)/profile/[nerdAt]/following/page.tsx | 69 +++++++++++++++++++ src/app/api/user/route.ts | 7 +- src/app/api/users/[userId]/followers/route.ts | 69 +++++++++++++++++++ src/app/api/users/[userId]/following/route.ts | 69 +++++++++++++++++++ src/components/settings/user-profile.tsx | 21 ++++++ 6 files changed, 303 insertions(+), 1 deletion(-) create mode 100644 src/app/(app)/profile/[nerdAt]/followers/page.tsx create mode 100644 src/app/(app)/profile/[nerdAt]/following/page.tsx create mode 100644 src/app/api/users/[userId]/followers/route.ts create mode 100644 src/app/api/users/[userId]/following/route.ts diff --git a/src/app/(app)/profile/[nerdAt]/followers/page.tsx b/src/app/(app)/profile/[nerdAt]/followers/page.tsx new file mode 100644 index 00000000..ba616e1e --- /dev/null +++ b/src/app/(app)/profile/[nerdAt]/followers/page.tsx @@ -0,0 +1,69 @@ +"use client"; + +import { useQuery } from "@tanstack/react-query"; +import axios from "axios"; +import { useParams } from "next/navigation"; +import UserProfile from "@/components/settings/user-profile"; +import { Skeleton } from "@/components/ui/skeleton"; +import Image from "next/image"; +import Link from "next/link"; + +export default function FollowersPage() { + const params = useParams(); + if (!params?.nerdAt) return null; + const nerdAt = params.nerdAt as string; + + const { data: followers, isLoading } = useQuery({ + queryKey: ["user-followers", nerdAt], + queryFn: async () => { + const response = await axios.get(`/api/users/${nerdAt}/followers`); + return response.data.data; + }, + }); + + if (isLoading) { + return ( +
+
+ {[...Array(6)].map((_, i) => ( +
+ +
+ + +
+
+ ))} +
+
+ ); + } + + return ( +
+

Followers

+
+ {followers?.map((follower: any) => ( + +
+ {follower.name} +
+
+

{follower.visualName || follower.name}

+

Nerd@{follower.nerdAt}

+
+ + ))} +
+
+ ); +} \ No newline at end of file diff --git a/src/app/(app)/profile/[nerdAt]/following/page.tsx b/src/app/(app)/profile/[nerdAt]/following/page.tsx new file mode 100644 index 00000000..81e8d3c2 --- /dev/null +++ b/src/app/(app)/profile/[nerdAt]/following/page.tsx @@ -0,0 +1,69 @@ +"use client"; + +import { useQuery } from "@tanstack/react-query"; +import axios from "axios"; +import { useParams } from "next/navigation"; +import UserProfile from "@/components/settings/user-profile"; +import { Skeleton } from "@/components/ui/skeleton"; +import Image from "next/image"; +import Link from "next/link"; + +export default function FollowingPage() { + const params = useParams(); + if (!params?.nerdAt) return null; + const nerdAt = params.nerdAt as string; + + const { data: following, isLoading } = useQuery({ + queryKey: ["user-following", nerdAt], + queryFn: async () => { + const response = await axios.get(`/api/users/${nerdAt}/following`); + return response.data.data; + }, + }); + + if (isLoading) { + return ( +
+
+ {[...Array(6)].map((_, i) => ( +
+ +
+ + +
+
+ ))} +
+
+ ); + } + + return ( +
+

Following

+
+ {following?.map((user: any) => ( + +
+ {user.name} +
+
+

{user.visualName || user.name}

+

Nerd@{user.nerdAt}

+
+ + ))} +
+
+ ); +} \ No newline at end of file diff --git a/src/app/api/user/route.ts b/src/app/api/user/route.ts index 40144fa9..7efc41e9 100644 --- a/src/app/api/user/route.ts +++ b/src/app/api/user/route.ts @@ -29,12 +29,17 @@ export const GET = async (req: NextRequest) => { where: { id: userId, }, - include: { posts: true, country: true, followers: true, following: true, + _count: { + select: { + followers: true, + following: true, + }, + }, }, }); diff --git a/src/app/api/users/[userId]/followers/route.ts b/src/app/api/users/[userId]/followers/route.ts new file mode 100644 index 00000000..cb958b84 --- /dev/null +++ b/src/app/api/users/[userId]/followers/route.ts @@ -0,0 +1,69 @@ +import { prisma } from "@/lib/prisma"; +import { NextRequest, NextResponse } from "next/server"; + +export async function GET( + request: NextRequest, + { params }: { params: { userId: string } }, +) { + try { + const { userId } = params; + const cursor = request.nextUrl.searchParams.get("cursor"); + const limit = parseInt(request.nextUrl.searchParams.get("limit") || "10"); + + const [followers, total] = await Promise.all([ + prisma.follows.findMany({ + where: { + followingId: userId, + ...(cursor && { + createdAt: { + lt: new Date(cursor), + }, + }), + }, + include: { + follower: { + select: { + id: true, + name: true, + email: true, + image: true, + bio: true, + nerdAt: true, + coverImage: true, + }, + }, + }, + orderBy: { + createdAt: "desc", + }, + take: limit + 1, + }), + prisma.follows.count({ + where: { + followingId: userId, + }, + }), + ]); + + const hasNextPage = followers.length > limit; + const items = hasNextPage ? followers.slice(0, -1) : followers; + const nextCursor = hasNextPage + ? items[items.length - 1].createdAt.toISOString() + : null; + + return NextResponse.json({ + data: items.map((follow) => follow.follower), + pagination: { + nextCursor, + hasNextPage, + total, + }, + }); + } catch (error) { + console.error("Error fetching followers:", error); + return NextResponse.json( + { error: "Failed to fetch followers" }, + { status: 500 }, + ); + } +} diff --git a/src/app/api/users/[userId]/following/route.ts b/src/app/api/users/[userId]/following/route.ts new file mode 100644 index 00000000..80a6568e --- /dev/null +++ b/src/app/api/users/[userId]/following/route.ts @@ -0,0 +1,69 @@ +import { prisma } from "@/lib/prisma"; +import { NextRequest, NextResponse } from "next/server"; + +export async function GET( + request: NextRequest, + { params }: { params: { userId: string } }, +) { + try { + const { userId } = params; + const cursor = request.nextUrl.searchParams.get("cursor"); + const limit = parseInt(request.nextUrl.searchParams.get("limit") || "10"); + + const [following, total] = await Promise.all([ + prisma.follows.findMany({ + where: { + followerId: userId, + ...(cursor && { + createdAt: { + lt: new Date(cursor), + }, + }), + }, + include: { + following: { + select: { + id: true, + name: true, + email: true, + image: true, + bio: true, + nerdAt: true, + coverImage: true, + }, + }, + }, + orderBy: { + createdAt: "desc", + }, + take: limit + 1, + }), + prisma.follows.count({ + where: { + followerId: userId, + }, + }), + ]); + + const hasNextPage = following.length > limit; + const items = hasNextPage ? following.slice(0, -1) : following; + const nextCursor = hasNextPage + ? items[items.length - 1].createdAt.toISOString() + : null; + + return NextResponse.json({ + data: items.map((follow) => follow.following), + pagination: { + nextCursor, + hasNextPage, + total, + }, + }); + } catch (error) { + console.error("Error fetching following:", error); + return NextResponse.json( + { error: "Failed to fetch following" }, + { status: 500 }, + ); + } +} diff --git a/src/components/settings/user-profile.tsx b/src/components/settings/user-profile.tsx index 51069b3c..422c49bf 100644 --- a/src/components/settings/user-profile.tsx +++ b/src/components/settings/user-profile.tsx @@ -18,6 +18,7 @@ import ProjectsTab from "./tabs/ProjectsTab"; import CollectionsTab from "./tabs/CollectionsTab"; import BookmarksTab from "./tabs/BookmarksTab"; import PrivateTab from "./tabs/PrivateTab"; +import Link from "next/link"; export default function UserProfile() { const [activeTab, setActiveTab] = useState("posts"); @@ -64,6 +65,26 @@ export default function UserProfile() {

{userProfile?.bio}

+
+ + + {userProfile?._count?.followers || 0} + {" "} + Following + + + + {userProfile?._count?.following || 0} + {" "} + Followers + +
From 6316c321923555e602523b39cd8fbaaf4450526d Mon Sep 17 00:00:00 2001 From: yeabnoah Date: Wed, 9 Apr 2025 18:09:10 +0300 Subject: [PATCH 3/4] working : follwing and follows user-profile( other users) --- prisma/schema.prisma | 2 +- .../(app)/profile/[nerdAt]/followers/page.tsx | 96 +++++++----- .../(app)/profile/[nerdAt]/following/page.tsx | 96 +++++++----- src/app/api/user/route.ts | 50 +++--- .../[userId]/followers/route.ts | 18 ++- .../[userId]/following/route.ts | 18 ++- .../{ => [nerdAt]}/[userId]/projects/route.ts | 0 src/app/api/users/[nerdAt]/followers/route.ts | 80 ++++++++++ src/app/api/users/[nerdAt]/following/route.ts | 80 ++++++++++ .../app/profile/[username]/following/page.tsx | 28 +--- src/components/settings/Followers.tsx | 9 ++ src/components/settings/Following.tsx | 9 ++ src/components/settings/follow-list.tsx | 92 +++++++++++ src/components/settings/user-followers.tsx | 146 ++++++++++++++++++ src/components/settings/user-following.tsx | 146 ++++++++++++++++++ src/components/settings/user-profile.tsx | 29 +++- src/components/user/user-following-list.tsx | 146 ++++++++++++++++++ 17 files changed, 909 insertions(+), 136 deletions(-) rename src/app/api/users/{ => [nerdAt]}/[userId]/followers/route.ts (79%) rename src/app/api/users/{ => [nerdAt]}/[userId]/following/route.ts (79%) rename src/app/api/users/{ => [nerdAt]}/[userId]/projects/route.ts (100%) create mode 100644 src/app/api/users/[nerdAt]/followers/route.ts create mode 100644 src/app/api/users/[nerdAt]/following/route.ts create mode 100644 src/components/settings/Followers.tsx create mode 100644 src/components/settings/Following.tsx create mode 100644 src/components/settings/follow-list.tsx create mode 100644 src/components/settings/user-followers.tsx create mode 100644 src/components/settings/user-following.tsx create mode 100644 src/components/user/user-following-list.tsx diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 4dad2b2d..ec18382a 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -28,7 +28,7 @@ model User { updatedAt DateTime sessions Session[] accounts Account[] - nerdAt String? + nerdAt String? @unique posts Post[] bio String? countryId String? diff --git a/src/app/(app)/profile/[nerdAt]/followers/page.tsx b/src/app/(app)/profile/[nerdAt]/followers/page.tsx index ba616e1e..70107637 100644 --- a/src/app/(app)/profile/[nerdAt]/followers/page.tsx +++ b/src/app/(app)/profile/[nerdAt]/followers/page.tsx @@ -1,37 +1,48 @@ "use client"; +import { useParams } from "next/navigation"; import { useQuery } from "@tanstack/react-query"; import axios from "axios"; -import { useParams } from "next/navigation"; -import UserProfile from "@/components/settings/user-profile"; +import UserFollowers from "@/components/settings/user-followers"; import { Skeleton } from "@/components/ui/skeleton"; -import Image from "next/image"; -import Link from "next/link"; +import LeftNavbar from "@/components/navbar/left-navbar"; +import MobileNavBar from "@/components/navbar/mobile-nav-bar"; -export default function FollowersPage() { +export default function UserFollowersPage() { const params = useParams(); if (!params?.nerdAt) return null; + const nerdAt = params.nerdAt as string; - const { data: followers, isLoading } = useQuery({ - queryKey: ["user-followers", nerdAt], + const { + data: userData, + isLoading, + isError, + } = useQuery({ + queryKey: ["user-profile", nerdAt], queryFn: async () => { - const response = await axios.get(`/api/users/${nerdAt}/followers`); - return response.data.data; + const { data } = await axios.get(`/api/user?nerdAt=${nerdAt}`); + return data.data; }, }); if (isLoading) { return (
-
- {[...Array(6)].map((_, i) => ( -
- -
- - +
+ {[...Array(5)].map((_, i) => ( +
+
+ +
+ + +
+
))}
@@ -39,31 +50,36 @@ export default function FollowersPage() { ); } + if (isError) { + return ( +
+
+

+ Error loading user data. Please try again. +

+
+
+ ); + } + + if (!userData) { + return ( +
+
+

User not found

+
+
+ ); + } + return ( -
-

Followers

-
- {followers?.map((follower: any) => ( - -
- {follower.name} -
-
-

{follower.visualName || follower.name}

-

Nerd@{follower.nerdAt}

-
- - ))} +
+ +
+
+ +
); -} \ No newline at end of file +} diff --git a/src/app/(app)/profile/[nerdAt]/following/page.tsx b/src/app/(app)/profile/[nerdAt]/following/page.tsx index 81e8d3c2..18a9974f 100644 --- a/src/app/(app)/profile/[nerdAt]/following/page.tsx +++ b/src/app/(app)/profile/[nerdAt]/following/page.tsx @@ -1,37 +1,48 @@ "use client"; +import { useParams } from "next/navigation"; import { useQuery } from "@tanstack/react-query"; import axios from "axios"; -import { useParams } from "next/navigation"; -import UserProfile from "@/components/settings/user-profile"; +import UserFollowing from "@/components/settings/user-following"; import { Skeleton } from "@/components/ui/skeleton"; -import Image from "next/image"; -import Link from "next/link"; +import LeftNavbar from "@/components/navbar/left-navbar"; +import MobileNavBar from "@/components/navbar/mobile-nav-bar"; -export default function FollowingPage() { +export default function UserFollowingPage() { const params = useParams(); if (!params?.nerdAt) return null; + const nerdAt = params.nerdAt as string; - const { data: following, isLoading } = useQuery({ - queryKey: ["user-following", nerdAt], + const { + data: userData, + isLoading, + isError, + } = useQuery({ + queryKey: ["user-profile", nerdAt], queryFn: async () => { - const response = await axios.get(`/api/users/${nerdAt}/following`); - return response.data.data; + const { data } = await axios.get(`/api/user?nerdAt=${nerdAt}`); + return data.data; }, }); if (isLoading) { return (
-
- {[...Array(6)].map((_, i) => ( -
- -
- - +
+ {[...Array(5)].map((_, i) => ( +
+
+ +
+ + +
+
))}
@@ -39,31 +50,36 @@ export default function FollowingPage() { ); } + if (isError) { + return ( +
+
+

+ Error loading user data. Please try again. +

+
+
+ ); + } + + if (!userData) { + return ( +
+
+

User not found

+
+
+ ); + } + return ( -
-

Following

-
- {following?.map((user: any) => ( - -
- {user.name} -
-
-

{user.visualName || user.name}

-

Nerd@{user.nerdAt}

-
- - ))} +
+ +
+
+ +
); -} \ No newline at end of file +} diff --git a/src/app/api/user/route.ts b/src/app/api/user/route.ts index 7efc41e9..ee89760a 100644 --- a/src/app/api/user/route.ts +++ b/src/app/api/user/route.ts @@ -5,12 +5,13 @@ import { NextRequest, NextResponse } from "next/server"; export const GET = async (req: NextRequest) => { try { const session = await getUserSession(); - const userId = await req.nextUrl.searchParams.get("userId"); + const userId = req.nextUrl.searchParams.get("userId"); + const nerdAt = req.nextUrl.searchParams.get("nerdAt"); - if (!userId) { + if (!userId && !nerdAt) { return NextResponse.json( { - message: "userId is required", + message: "userId or nerdAt is required", }, { status: 400 }, ); @@ -26,20 +27,17 @@ export const GET = async (req: NextRequest) => { } const user = await prisma.user.findUnique({ - where: { - id: userId, - }, - include: { - posts: true, + where: userId ? { id: userId } : { nerdAt: nerdAt! }, + select: { + id: true, + name: true, + email: true, + image: true, + bio: true, + nerdAt: true, + coverImage: true, + visualName: true, country: true, - followers: true, - following: true, - _count: { - select: { - followers: true, - following: true, - }, - }, }, }); @@ -52,11 +50,27 @@ export const GET = async (req: NextRequest) => { ); } + // Get the actual counts + const [followersCount, followingCount] = await Promise.all([ + prisma.follows.count({ + where: { followingId: user.id }, + }), + prisma.follows.count({ + where: { followerId: user.id }, + }), + ]); + return NextResponse.json({ - data: user, + data: { + ...user, + _count: { + followers: followersCount, + following: followingCount, + }, + }, }); } catch (error) { - console.error("Error fetching posts:", error); + console.error("Error fetching user:", error); return NextResponse.json({ error: "error" }, { status: 500 }); } }; diff --git a/src/app/api/users/[userId]/followers/route.ts b/src/app/api/users/[nerdAt]/[userId]/followers/route.ts similarity index 79% rename from src/app/api/users/[userId]/followers/route.ts rename to src/app/api/users/[nerdAt]/[userId]/followers/route.ts index cb958b84..8bbd793d 100644 --- a/src/app/api/users/[userId]/followers/route.ts +++ b/src/app/api/users/[nerdAt]/[userId]/followers/route.ts @@ -3,17 +3,27 @@ import { NextRequest, NextResponse } from "next/server"; export async function GET( request: NextRequest, - { params }: { params: { userId: string } }, + { params }: { params: { nerdAt: string } }, ) { try { - const { userId } = params; + const { nerdAt } = params; const cursor = request.nextUrl.searchParams.get("cursor"); const limit = parseInt(request.nextUrl.searchParams.get("limit") || "10"); + // First get the user's ID from their nerdAt + const user = await prisma.user.findUnique({ + where: { nerdAt }, + select: { id: true }, + }); + + if (!user) { + return NextResponse.json({ error: "User not found" }, { status: 404 }); + } + const [followers, total] = await Promise.all([ prisma.follows.findMany({ where: { - followingId: userId, + followingId: user.id, ...(cursor && { createdAt: { lt: new Date(cursor), @@ -40,7 +50,7 @@ export async function GET( }), prisma.follows.count({ where: { - followingId: userId, + followingId: user.id, }, }), ]); diff --git a/src/app/api/users/[userId]/following/route.ts b/src/app/api/users/[nerdAt]/[userId]/following/route.ts similarity index 79% rename from src/app/api/users/[userId]/following/route.ts rename to src/app/api/users/[nerdAt]/[userId]/following/route.ts index 80a6568e..1d77ae24 100644 --- a/src/app/api/users/[userId]/following/route.ts +++ b/src/app/api/users/[nerdAt]/[userId]/following/route.ts @@ -3,17 +3,27 @@ import { NextRequest, NextResponse } from "next/server"; export async function GET( request: NextRequest, - { params }: { params: { userId: string } }, + { params }: { params: { nerdAt: string } }, ) { try { - const { userId } = params; + const { nerdAt } = params; const cursor = request.nextUrl.searchParams.get("cursor"); const limit = parseInt(request.nextUrl.searchParams.get("limit") || "10"); + // First get the user's ID from their nerdAt + const user = await prisma.user.findUnique({ + where: { nerdAt }, + select: { id: true }, + }); + + if (!user) { + return NextResponse.json({ error: "User not found" }, { status: 404 }); + } + const [following, total] = await Promise.all([ prisma.follows.findMany({ where: { - followerId: userId, + followerId: user.id, ...(cursor && { createdAt: { lt: new Date(cursor), @@ -40,7 +50,7 @@ export async function GET( }), prisma.follows.count({ where: { - followerId: userId, + followerId: user.id, }, }), ]); diff --git a/src/app/api/users/[userId]/projects/route.ts b/src/app/api/users/[nerdAt]/[userId]/projects/route.ts similarity index 100% rename from src/app/api/users/[userId]/projects/route.ts rename to src/app/api/users/[nerdAt]/[userId]/projects/route.ts diff --git a/src/app/api/users/[nerdAt]/followers/route.ts b/src/app/api/users/[nerdAt]/followers/route.ts new file mode 100644 index 00000000..0fcde343 --- /dev/null +++ b/src/app/api/users/[nerdAt]/followers/route.ts @@ -0,0 +1,80 @@ +import { prisma } from "@/lib/prisma"; +import { NextRequest, NextResponse } from "next/server"; + +export async function GET( + request: NextRequest, + { params }: { params: { nerdAt: string } }, +) { + try { + const { nerdAt } = params; + const cursor = request.nextUrl.searchParams.get("cursor"); + const limit = parseInt(request.nextUrl.searchParams.get("limit") || "10"); + + // First get the user by nerdAt + const user = await prisma.user.findUnique({ + where: { nerdAt }, + select: { id: true }, + }); + + if (!user) { + return NextResponse.json({ message: "User not found" }, { status: 404 }); + } + + const [followers, total] = await Promise.all([ + prisma.follows.findMany({ + where: { + followingId: user.id, + ...(cursor && { + createdAt: { + lt: new Date(cursor), + }, + }), + }, + include: { + follower: { + select: { + id: true, + name: true, + email: true, + image: true, + bio: true, + nerdAt: true, + coverImage: true, + visualName: true, + }, + }, + }, + orderBy: { + createdAt: "desc", + }, + take: limit + 1, + }), + prisma.follows.count({ + where: { + followingId: user.id, + }, + }), + ]); + + const hasNextPage = followers.length > limit; + const items = hasNextPage ? followers.slice(0, -1) : followers; + const nextCursor = hasNextPage + ? items[items.length - 1].createdAt.toISOString() + : null; + + return NextResponse.json({ + data: items.map((follow) => follow.follower), + pagination: { + nextCursor, + hasNextPage, + total, + }, + }); + } catch (error) { + console.error("Error fetching followers:", error); + return NextResponse.json( + { error: "Failed to fetch followers" }, + { status: 500 }, + ); + } +} diff --git a/src/app/api/users/[nerdAt]/following/route.ts b/src/app/api/users/[nerdAt]/following/route.ts new file mode 100644 index 00000000..07b7d82b --- /dev/null +++ b/src/app/api/users/[nerdAt]/following/route.ts @@ -0,0 +1,80 @@ +import { prisma } from "@/lib/prisma"; +import { NextRequest, NextResponse } from "next/server"; + +export async function GET( + request: NextRequest, + { params }: { params: { nerdAt: string } }, +) { + try { + const { nerdAt } = params; + const cursor = request.nextUrl.searchParams.get("cursor"); + const limit = parseInt(request.nextUrl.searchParams.get("limit") || "10"); + + // First get the user by nerdAt + const user = await prisma.user.findUnique({ + where: { nerdAt }, + select: { id: true }, + }); + + if (!user) { + return NextResponse.json({ message: "User not found" }, { status: 404 }); + } + + const [following, total] = await Promise.all([ + prisma.follows.findMany({ + where: { + followerId: user.id, + ...(cursor && { + createdAt: { + lt: new Date(cursor), + }, + }), + }, + include: { + following: { + select: { + id: true, + name: true, + email: true, + image: true, + bio: true, + nerdAt: true, + coverImage: true, + visualName: true, + }, + }, + }, + orderBy: { + createdAt: "desc", + }, + take: limit + 1, + }), + prisma.follows.count({ + where: { + followerId: user.id, + }, + }), + ]); + + const hasNextPage = following.length > limit; + const items = hasNextPage ? following.slice(0, -1) : following; + const nextCursor = hasNextPage + ? items[items.length - 1].createdAt.toISOString() + : null; + + return NextResponse.json({ + data: items.map((follow) => follow.following), + pagination: { + nextCursor, + hasNextPage, + total, + }, + }); + } catch (error) { + console.error("Error fetching following:", error); + return NextResponse.json( + { error: "Failed to fetch following" }, + { status: 500 }, + ); + } +} diff --git a/src/app/app/profile/[username]/following/page.tsx b/src/app/app/profile/[username]/following/page.tsx index d5fb641f..c314b054 100644 --- a/src/app/app/profile/[username]/following/page.tsx +++ b/src/app/app/profile/[username]/following/page.tsx @@ -1,27 +1,5 @@ -import LeftNavbar from "@/components/navbar/left-navbar"; -import MobileNavBar from "@/components/navbar/mobile-nav-bar"; -import Following from "@/components/user/following"; -import { authClient } from "@/lib/auth-client"; -import { redirect } from "next/navigation"; +import Following from "@/components/settings/Following"; -export default async function FollowingPage({ - params, -}: { - params: { username: string }; -}) { - const session = await authClient.getSession(); - if (!session) { - redirect("/login"); - } - - return ( -
- -
- -
- - -
- ); +export default function FollowingPage() { + return ; } diff --git a/src/components/settings/Followers.tsx b/src/components/settings/Followers.tsx new file mode 100644 index 00000000..ca8742cc --- /dev/null +++ b/src/components/settings/Followers.tsx @@ -0,0 +1,9 @@ +"use client"; + +export default function Followers() { + return ( +
+

Followers

+
+ ); +} \ No newline at end of file diff --git a/src/components/settings/Following.tsx b/src/components/settings/Following.tsx new file mode 100644 index 00000000..cc899769 --- /dev/null +++ b/src/components/settings/Following.tsx @@ -0,0 +1,9 @@ +"use client"; + +export default function Following() { + return ( +
+

Following

+
+ ); +} diff --git a/src/components/settings/follow-list.tsx b/src/components/settings/follow-list.tsx new file mode 100644 index 00000000..f7daa566 --- /dev/null +++ b/src/components/settings/follow-list.tsx @@ -0,0 +1,92 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { useInView } from "react-intersection-observer"; +import Image from "next/image"; +import Link from "next/link"; +import { User } from "@prisma/client"; + +interface FollowListProps { + type: "followers" | "following"; + nerdAt: string; +} + +interface PaginatedResponse { + data: User[]; + pagination: { + nextCursor: string | null; + hasNextPage: boolean; + total: number; + }; +} + +export default function FollowList({ type, nerdAt }: FollowListProps) { + const [users, setUsers] = useState([]); + const [cursor, setCursor] = useState(null); + const [hasNextPage, setHasNextPage] = useState(true); + const [isLoading, setIsLoading] = useState(false); + const { ref, inView } = useInView(); + + const fetchUsers = async () => { + if (!hasNextPage || isLoading) return; + + setIsLoading(true); + try { + const url = new URL( + `/api/users/${nerdAt}/${type}`, + window.location.origin + ); + if (cursor) { + url.searchParams.set("cursor", cursor); + } + url.searchParams.set("limit", "10"); + + const response = await fetch(url.toString()); + const data: PaginatedResponse = await response.json(); + + setUsers((prev) => [...prev, ...data.data]); + setCursor(data.pagination.nextCursor); + setHasNextPage(data.pagination.hasNextPage); + } catch (error) { + console.error(`Error fetching ${type}:`, error); + } finally { + setIsLoading(false); + } + }; + + useEffect(() => { + if (inView) { + fetchUsers(); + } + }, [inView]); + + return ( +
+ {users.map((user) => ( + +
+ {user.name} +
+
+

{user.name}

+

Nerd@{user.nerdAt}

+
+ + ))} + {hasNextPage && ( +
+ {isLoading &&
Loading...
} +
+ )} +
+ ); +} \ No newline at end of file diff --git a/src/components/settings/user-followers.tsx b/src/components/settings/user-followers.tsx new file mode 100644 index 00000000..e747a97f --- /dev/null +++ b/src/components/settings/user-followers.tsx @@ -0,0 +1,146 @@ +"use client"; + +import { useInfiniteQuery } from "@tanstack/react-query"; +import axios from "axios"; +import Image from "next/image"; +import { Button } from "@/components/ui/button"; +import { Skeleton } from "@/components/ui/skeleton"; +import { authClient } from "@/lib/auth-client"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import toast from "react-hot-toast"; +import { useRouter } from "next/navigation"; + +interface UserFollowersProps { + nerdAt: string; +} + +const UserFollowers = ({ nerdAt }: UserFollowersProps) => { + const queryClient = useQueryClient(); + const session = authClient.useSession(); + const router = useRouter(); + + const { + data, + isLoading, + isError, + fetchNextPage, + hasNextPage, + isFetchingNextPage, + } = useInfiniteQuery({ + queryKey: ["user-followers", nerdAt], + queryFn: async ({ pageParam = null }) => { + const { data } = await axios.get(`/api/users/${nerdAt}/followers`, { + params: { cursor: pageParam }, + }); + return data; + }, + initialPageParam: null, + getNextPageParam: (lastPage) => + lastPage?.pagination?.nextCursor ?? undefined, + }); + + const followMutation = useMutation({ + mutationKey: ["follow-user"], + mutationFn: async (followUserId: string) => { + const response = await axios.post(`/api/user/follow?userId=${followUserId}`); + return response.data.message; + }, + onSuccess: (message) => { + queryClient.invalidateQueries({ queryKey: ["user-followers", nerdAt] }); + toast.success(message); + }, + }); + + const handleFollow = (followUserId: string) => { + if (session.data?.user.id === followUserId) { + toast.error("You cannot follow yourself"); + return; + } + followMutation.mutate(followUserId); + }; + + const followers = data?.pages.flatMap((page) => page.data) || []; + + if (isLoading) { + return ( +
+ {[...Array(5)].map((_, i) => ( +
+
+ +
+ + +
+
+ +
+ ))} +
+ ); + } + + if (isError) { + return ( +
+ Error loading followers. Please try again. +
+ ); + } + + return ( +
+

Followers

+
+ {followers.length > 0 ? ( + followers.map((user: any) => ( +
+
+
+ {user.name} +
+
+

+ {user.visualName || user.name} +

+

+ Nerd@{user.nerdAt} +

+
+
+ {session.data?.user.id !== user.id && ( + + )} +
+ )) + ) : ( +

+ No followers yet +

+ )} +
+ {hasNextPage && ( +
+ +
+ )} +
+ ); +}; + +export default UserFollowers; \ No newline at end of file diff --git a/src/components/settings/user-following.tsx b/src/components/settings/user-following.tsx new file mode 100644 index 00000000..2b534bc2 --- /dev/null +++ b/src/components/settings/user-following.tsx @@ -0,0 +1,146 @@ +"use client"; + +import { useInfiniteQuery } from "@tanstack/react-query"; +import axios from "axios"; +import Image from "next/image"; +import { Button } from "@/components/ui/button"; +import { Skeleton } from "@/components/ui/skeleton"; +import { authClient } from "@/lib/auth-client"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import toast from "react-hot-toast"; +import { useRouter } from "next/navigation"; + +interface UserFollowingProps { + nerdAt: string; +} + +const UserFollowing = ({ nerdAt }: UserFollowingProps) => { + const queryClient = useQueryClient(); + const session = authClient.useSession(); + const router = useRouter(); + + const { + data, + isLoading, + isError, + fetchNextPage, + hasNextPage, + isFetchingNextPage, + } = useInfiniteQuery({ + queryKey: ["user-following", nerdAt], + queryFn: async ({ pageParam = null }) => { + const { data } = await axios.get(`/api/users/${nerdAt}/following`, { + params: { cursor: pageParam }, + }); + return data; + }, + initialPageParam: null, + getNextPageParam: (lastPage) => + lastPage?.pagination?.nextCursor ?? undefined, + }); + + const followMutation = useMutation({ + mutationKey: ["follow-user"], + mutationFn: async (followUserId: string) => { + const response = await axios.post(`/api/user/follow?userId=${followUserId}`); + return response.data.message; + }, + onSuccess: (message) => { + queryClient.invalidateQueries({ queryKey: ["user-following", nerdAt] }); + toast.success(message); + }, + }); + + const handleFollow = (followUserId: string) => { + if (session.data?.user.id === followUserId) { + toast.error("You cannot follow yourself"); + return; + } + followMutation.mutate(followUserId); + }; + + const following = data?.pages.flatMap((page) => page.data) || []; + + if (isLoading) { + return ( +
+ {[...Array(5)].map((_, i) => ( +
+
+ +
+ + +
+
+ +
+ ))} +
+ ); + } + + if (isError) { + return ( +
+ Error loading following. Please try again. +
+ ); + } + + return ( +
+

Following

+
+ {following.length > 0 ? ( + following.map((user: any) => ( +
+
+
+ {user.name} +
+
+

+ {user.visualName || user.name} +

+

+ Nerd@{user.nerdAt} +

+
+
+ {session.data?.user.id !== user.id && ( + + )} +
+ )) + ) : ( +

+ Not following anyone yet +

+ )} +
+ {hasNextPage && ( +
+ +
+ )} +
+ ); +}; + +export default UserFollowing; \ No newline at end of file diff --git a/src/components/settings/user-profile.tsx b/src/components/settings/user-profile.tsx index 422c49bf..ed6860c8 100644 --- a/src/components/settings/user-profile.tsx +++ b/src/components/settings/user-profile.tsx @@ -19,10 +19,31 @@ import CollectionsTab from "./tabs/CollectionsTab"; import BookmarksTab from "./tabs/BookmarksTab"; import PrivateTab from "./tabs/PrivateTab"; import Link from "next/link"; +import { usePathname } from "next/navigation"; +import FollowList from "./follow-list"; export default function UserProfile() { const [activeTab, setActiveTab] = useState("posts"); const { userProfile } = useUserProfileStore(); + const pathname = usePathname(); + const isFollowingPage = pathname?.includes("/following") || false; + const isFollowersPage = pathname?.includes("/followers") || false; + + if (isFollowingPage || isFollowersPage) { + return ( +
+
+

+ {isFollowingPage ? "Following" : "Followers"} +

+ +
+
+ ); + } return (
@@ -67,20 +88,20 @@ export default function UserProfile() {

- {userProfile?._count?.followers || 0} + {userProfile?._count?.following || 0} {" "} Following - {userProfile?._count?.following || 0} + {userProfile?._count?.followers || 0} {" "} Followers diff --git a/src/components/user/user-following-list.tsx b/src/components/user/user-following-list.tsx new file mode 100644 index 00000000..6d7a9f34 --- /dev/null +++ b/src/components/user/user-following-list.tsx @@ -0,0 +1,146 @@ +"use client"; + +import { useInfiniteQuery } from "@tanstack/react-query"; +import axios from "axios"; +import Image from "next/image"; +import { Button } from "@/components/ui/button"; +import { Skeleton } from "@/components/ui/skeleton"; +import { authClient } from "@/lib/auth-client"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import toast from "react-hot-toast"; +import { useRouter } from "next/navigation"; + +interface UserFollowingListProps { + userId: string; +} + +const UserFollowingList = ({ userId }: UserFollowingListProps) => { + const queryClient = useQueryClient(); + const session = authClient.useSession(); + const router = useRouter(); + + const { + data, + isLoading, + isError, + fetchNextPage, + hasNextPage, + isFetchingNextPage, + } = useInfiniteQuery({ + queryKey: ["user-following", userId], + queryFn: async ({ pageParam = null }) => { + const { data } = await axios.get(`/api/users/${userId}/following`, { + params: { cursor: pageParam }, + }); + return data; + }, + initialPageParam: null, + getNextPageParam: (lastPage) => + lastPage?.pagination?.nextCursor ?? undefined, + }); + + const followMutation = useMutation({ + mutationKey: ["follow-user"], + mutationFn: async (followUserId: string) => { + const response = await axios.post(`/api/user/follow?userId=${followUserId}`); + return response.data.message; + }, + onSuccess: (message) => { + queryClient.invalidateQueries({ queryKey: ["user-following", userId] }); + toast.success(message); + }, + }); + + const handleFollow = (followUserId: string) => { + if (session.data?.user.id === followUserId) { + toast.error("You cannot follow yourself"); + return; + } + followMutation.mutate(followUserId); + }; + + const following = data?.pages.flatMap((page) => page.data) || []; + + if (isLoading) { + return ( +
+ {[...Array(5)].map((_, i) => ( +
+
+ +
+ + +
+
+ +
+ ))} +
+ ); + } + + if (isError) { + return ( +
+ Error loading following. Please try again. +
+ ); + } + + return ( +
+

Following

+
+ {following.length > 0 ? ( + following.map((user: any) => ( +
+
+
+ {user.name} +
+
+

+ {user.visualName || user.name} +

+

+ Nerd@{user.nerdAt} +

+
+
+ {session.data?.user.id !== user.id && ( + + )} +
+ )) + ) : ( +

+ Not following anyone yet +

+ )} +
+ {hasNextPage && ( +
+ +
+ )} +
+ ); +}; + +export default UserFollowingList; \ No newline at end of file From 46c780ed427c82ebfe7958efa17ee99612b88160 Mon Sep 17 00:00:00 2001 From: yeabnoah Date: Wed, 9 Apr 2025 20:10:09 +0300 Subject: [PATCH 4/4] working : following and follower working for current user --- src/app/(app)/profile/[id]/followers/page.tsx | 158 ++++++++++++++++++ src/app/(app)/profile/[id]/following/page.tsx | 158 ++++++++++++++++++ src/app/(app)/profile/[id]/page.tsx | 14 ++ src/app/api/user/follow/route.ts | 29 +++- src/app/api/users/check-follow/route.ts | 27 ++- .../profile/[username]/followers/layout.tsx | 13 -- .../app/profile/[username]/followers/page.tsx | 27 --- .../profile/[username]/following/layout.tsx | 13 -- src/components/post/post-card.tsx | 2 +- src/components/settings/follow-button.tsx | 76 +++++++++ src/components/settings/profile.tsx | 4 +- .../settings/user-profile-stats.tsx | 85 ++++++++++ src/components/settings/user-profile.tsx | 21 +++ src/functions/follow.ts | 105 ++++++++++++ 14 files changed, 664 insertions(+), 68 deletions(-) create mode 100644 src/app/(app)/profile/[id]/followers/page.tsx create mode 100644 src/app/(app)/profile/[id]/following/page.tsx create mode 100644 src/app/(app)/profile/[id]/page.tsx delete mode 100644 src/app/app/profile/[username]/followers/layout.tsx delete mode 100644 src/app/app/profile/[username]/followers/page.tsx delete mode 100644 src/app/app/profile/[username]/following/layout.tsx create mode 100644 src/components/settings/follow-button.tsx create mode 100644 src/components/settings/user-profile-stats.tsx create mode 100644 src/functions/follow.ts diff --git a/src/app/(app)/profile/[id]/followers/page.tsx b/src/app/(app)/profile/[id]/followers/page.tsx new file mode 100644 index 00000000..4879327c --- /dev/null +++ b/src/app/(app)/profile/[id]/followers/page.tsx @@ -0,0 +1,158 @@ +"use client"; + +import LeftNavbar from "@/components/navbar/left-navbar"; +import MobileNavBar from "@/components/navbar/mobile-nav-bar"; +import { FollowResponse, followService } from "@/functions/follow"; +import { + useInfiniteQuery, + useMutation, + useQuery, + useQueryClient, +} from "@tanstack/react-query"; +import Image from "next/image"; +import Link from "next/link"; +import { useEffect, useState } from "react"; +import { useInView } from "react-intersection-observer"; +import { Button } from "@/components/ui/button"; +import { authClient } from "@/lib/auth-client"; +import toast from "react-hot-toast"; +import axios from "axios"; + +const FollowersPage = ({ params }: { params: { id: string } }) => { + const { ref, inView } = useInView(); + const queryClient = useQueryClient(); + const session = authClient.useSession(); + const [loadingStates, setLoadingStates] = useState>( + {}, + ); + + const { data, isLoading, fetchNextPage, hasNextPage, isFetchingNextPage } = + useInfiniteQuery({ + queryKey: ["followers"], + queryFn: ({ pageParam }) => + followService.getFollowers(pageParam as string | undefined), + getNextPageParam: (lastPage) => lastPage.pagination.nextCursor, + initialPageParam: undefined, + }); + + const followers = + data?.pages.flatMap((page: FollowResponse) => page.data) || []; + + const { data: followStatus } = useQuery({ + queryKey: ["follow-status", followers.map((f) => f.id).join(",")], + queryFn: async () => { + if (followers.length === 0) return {}; + const response = await axios.get( + `/api/users/check-follow?userIds=${followers.map((f) => f.id).join(",")}`, + ); + return response.data; + }, + enabled: followers.length > 0, + }); + + const followMutation = useMutation({ + mutationFn: async ({ + userId, + action, + }: { + userId: string; + action: "follow" | "unfollow"; + }) => { + setLoadingStates((prev) => ({ ...prev, [userId]: true })); + const response = await axios.post( + `/api/user/follow?userId=${userId}&action=${action}`, + ); + return response.data; + }, + onSuccess: (data, variables) => { + queryClient.invalidateQueries({ queryKey: ["followers"] }); + queryClient.invalidateQueries({ queryKey: ["following"] }); + queryClient.invalidateQueries({ queryKey: ["follow-status"] }); + toast.success(data.message); + }, + onError: () => { + toast.error("Error occurred while following/unfollowing user"); + }, + onSettled: (_, __, variables) => { + setLoadingStates((prev) => ({ ...prev, [variables.userId]: false })); + }, + }); + + const handleFollow = (userId: string) => { + if (session.data?.user.id === userId) { + toast.error("You cannot follow yourself"); + return; + } + const isFollowing = followStatus?.[userId]; + followMutation.mutate({ + userId, + action: isFollowing ? "unfollow" : "follow", + }); + }; + + useEffect(() => { + if (inView && hasNextPage && !isFetchingNextPage) { + fetchNextPage(); + } + }, [inView, hasNextPage, isFetchingNextPage, fetchNextPage]); + + return ( +
+ +
+
+

Followers

+
+ {followers.map((user) => ( +
+ +
+ {user.name +
+
+

{user.name}

+

+ {user.bio || "No bio yet"} +

+
+ + {session.data?.user.id !== user.id && ( + + )} +
+ ))} +
+
+ {isFetchingNextPage && ( +
Loading more...
+ )} +
+
+ +
+ ); +}; + +export default FollowersPage; diff --git a/src/app/(app)/profile/[id]/following/page.tsx b/src/app/(app)/profile/[id]/following/page.tsx new file mode 100644 index 00000000..29939433 --- /dev/null +++ b/src/app/(app)/profile/[id]/following/page.tsx @@ -0,0 +1,158 @@ +"use client"; + +import LeftNavbar from "@/components/navbar/left-navbar"; +import MobileNavBar from "@/components/navbar/mobile-nav-bar"; +import { FollowResponse, followService } from "@/functions/follow"; +import { + useInfiniteQuery, + useMutation, + useQuery, + useQueryClient, +} from "@tanstack/react-query"; +import Image from "next/image"; +import Link from "next/link"; +import { useEffect, useState } from "react"; +import { useInView } from "react-intersection-observer"; +import { Button } from "@/components/ui/button"; +import { authClient } from "@/lib/auth-client"; +import toast from "react-hot-toast"; +import axios from "axios"; + +const FollowingPage = ({ params }: { params: { id: string } }) => { + const { ref, inView } = useInView(); + const queryClient = useQueryClient(); + const session = authClient.useSession(); + const [loadingStates, setLoadingStates] = useState>( + {}, + ); + + const { data, isLoading, fetchNextPage, hasNextPage, isFetchingNextPage } = + useInfiniteQuery({ + queryKey: ["following"], + queryFn: ({ pageParam }) => + followService.getFollowing(pageParam as string | undefined), + getNextPageParam: (lastPage) => lastPage.pagination.nextCursor, + initialPageParam: undefined, + }); + + const following = + data?.pages.flatMap((page: FollowResponse) => page.data) || []; + + const { data: followStatus } = useQuery({ + queryKey: ["follow-status", following.map((f) => f.id).join(",")], + queryFn: async () => { + if (following.length === 0) return {}; + const response = await axios.get( + `/api/users/check-follow?userIds=${following.map((f) => f.id).join(",")}`, + ); + return response.data; + }, + enabled: following.length > 0, + }); + + const followMutation = useMutation({ + mutationFn: async ({ + userId, + action, + }: { + userId: string; + action: "follow" | "unfollow"; + }) => { + setLoadingStates((prev) => ({ ...prev, [userId]: true })); + const response = await axios.post( + `/api/user/follow?userId=${userId}&action=${action}`, + ); + return response.data; + }, + onSuccess: (data, variables) => { + queryClient.invalidateQueries({ queryKey: ["following"] }); + queryClient.invalidateQueries({ queryKey: ["followers"] }); + queryClient.invalidateQueries({ queryKey: ["follow-status"] }); + toast.success(data.message); + }, + onError: () => { + toast.error("Error occurred while following/unfollowing user"); + }, + onSettled: (_, __, variables) => { + setLoadingStates((prev) => ({ ...prev, [variables.userId]: false })); + }, + }); + + const handleFollow = (userId: string) => { + if (session.data?.user.id === userId) { + toast.error("You cannot follow yourself"); + return; + } + const isFollowing = followStatus?.[userId]; + followMutation.mutate({ + userId, + action: isFollowing ? "unfollow" : "follow", + }); + }; + + useEffect(() => { + if (inView && hasNextPage && !isFetchingNextPage) { + fetchNextPage(); + } + }, [inView, hasNextPage, isFetchingNextPage, fetchNextPage]); + + return ( +
+ +
+
+

Following

+
+ {following.map((user) => ( +
+ +
+ {user.name +
+
+

{user.name}

+

+ {user.bio || "No bio yet"} +

+
+ + {session.data?.user.id !== user.id && ( + + )} +
+ ))} +
+
+ {isFetchingNextPage && ( +
Loading more...
+ )} +
+
+ +
+ ); +}; + +export default FollowingPage; diff --git a/src/app/(app)/profile/[id]/page.tsx b/src/app/(app)/profile/[id]/page.tsx new file mode 100644 index 00000000..05fb7b47 --- /dev/null +++ b/src/app/(app)/profile/[id]/page.tsx @@ -0,0 +1,14 @@ +import React from "react"; +import { notFound } from "next/navigation"; + +const ProfilePage = ({ params }: { params: { id: string } }) => { + // This is the main profile page + return ( +
+

Profile {params.id}

+ {/* Add your profile content here */} +
+ ); +}; + +export default ProfilePage; diff --git a/src/app/api/user/follow/route.ts b/src/app/api/user/follow/route.ts index 7922cf64..bfb64e5d 100644 --- a/src/app/api/user/follow/route.ts +++ b/src/app/api/user/follow/route.ts @@ -5,7 +5,8 @@ import { NextRequest, NextResponse } from "next/server"; export const POST = async (req: NextRequest) => { try { const session = await getUserSession(); - const userId = await req.nextUrl.searchParams.get("userId"); + const userId = req.nextUrl.searchParams.get("userId"); + const action = req.nextUrl.searchParams.get("action") || "follow"; if (!userId) { return NextResponse.json( @@ -34,19 +35,29 @@ export const POST = async (req: NextRequest) => { ); } - const user = await prisma.follows.findFirst({ + const existingFollow = await prisma.follows.findFirst({ where: { followerId: session.user.id, followingId: userId, }, }); - if (user) { + if (action === "unfollow") { + if (!existingFollow) { + return NextResponse.json( + { + message: "You are not following this user", + }, + { status: 400 }, + ); + } + await prisma.follows.delete({ where: { - id: user.id, + id: existingFollow.id, }, }); + return NextResponse.json( { message: "Unfollowed successfully", @@ -54,12 +65,22 @@ export const POST = async (req: NextRequest) => { { status: 200 }, ); } else { + if (existingFollow) { + return NextResponse.json( + { + message: "You are already following this user", + }, + { status: 400 }, + ); + } + const following = await prisma.follows.create({ data: { followerId: session.user.id, followingId: userId, }, }); + return NextResponse.json( { data: following, diff --git a/src/app/api/users/check-follow/route.ts b/src/app/api/users/check-follow/route.ts index c2388d48..1e2d3fc6 100644 --- a/src/app/api/users/check-follow/route.ts +++ b/src/app/api/users/check-follow/route.ts @@ -10,25 +10,36 @@ export async function GET(request: Request) { } const { searchParams } = new URL(request.url); - const userId = searchParams.get("userId"); + const userIds = searchParams.get("userIds")?.split(","); - if (!userId) { + if (!userIds || userIds.length === 0) { return NextResponse.json( - { error: "User ID is required" }, + { error: "User IDs are required" }, { status: 400 }, ); } - const follow = await prisma.follows.findUnique({ + const follows = await prisma.follows.findMany({ where: { - followerId_followingId: { - followerId: session.user.id, - followingId: userId, + followerId: session.user.id, + followingId: { + in: userIds, }, }, + select: { + followingId: true, + }, }); - return NextResponse.json({ isFollowing: !!follow }); + const followStatus = userIds.reduce( + (acc, userId) => { + acc[userId] = follows.some((follow) => follow.followingId === userId); + return acc; + }, + {} as Record, + ); + + return NextResponse.json(followStatus); } catch (error) { console.error("Error checking follow status:", error); return NextResponse.json( diff --git a/src/app/app/profile/[username]/followers/layout.tsx b/src/app/app/profile/[username]/followers/layout.tsx deleted file mode 100644 index f6483f96..00000000 --- a/src/app/app/profile/[username]/followers/layout.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import Navbar from "@/components/navbar"; -import React from "react"; - -const FollowersLayout = ({ children }: { children: React.ReactNode }) => { - return ( -
- -
{children}
-
- ); -}; - -export default FollowersLayout; diff --git a/src/app/app/profile/[username]/followers/page.tsx b/src/app/app/profile/[username]/followers/page.tsx deleted file mode 100644 index 9a2ceaaa..00000000 --- a/src/app/app/profile/[username]/followers/page.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import LeftNavbar from "@/components/navbar/left-navbar"; -import MobileNavBar from "@/components/navbar/mobile-nav-bar"; -import Follower from "@/components/user/follower"; -import { authClient } from "@/lib/auth-client"; -import { redirect } from "next/navigation"; - -export default async function FollowersPage({ - params, -}: { - params: { username: string }; -}) { - const session = await authClient.getSession(); - if (!session) { - redirect("/login"); - } - - return ( -
- -
- -
- - -
- ); -} diff --git a/src/app/app/profile/[username]/following/layout.tsx b/src/app/app/profile/[username]/following/layout.tsx deleted file mode 100644 index eaad5987..00000000 --- a/src/app/app/profile/[username]/following/layout.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import Navbar from "@/components/navbar"; -import React from "react"; - -const FollowingLayout = ({ children }: { children: React.ReactNode }) => { - return ( -
- -
{children}
-
- ); -}; - -export default FollowingLayout; diff --git a/src/components/post/post-card.tsx b/src/components/post/post-card.tsx index 94494c39..63534df8 100644 --- a/src/components/post/post-card.tsx +++ b/src/components/post/post-card.tsx @@ -1,4 +1,4 @@ -"use client" +"use client"; import { getTrimLimit } from "@/functions/render-helper"; import postInterface from "@/interface/auth/post.interface"; diff --git a/src/components/settings/follow-button.tsx b/src/components/settings/follow-button.tsx new file mode 100644 index 00000000..b9786027 --- /dev/null +++ b/src/components/settings/follow-button.tsx @@ -0,0 +1,76 @@ +"use client"; + +import { Button } from "@/components/ui/button"; +import { useSession } from "@/lib/auth-client"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import axios from "axios"; +import toast from "react-hot-toast"; + +interface FollowButtonProps { + userId: string; + isCurrentUser: boolean; + isFollowing: boolean; +} + +export default function FollowButton({ + userId, + isCurrentUser, + isFollowing, +}: FollowButtonProps) { + const session = useSession(); + const queryClient = useQueryClient(); + + const followMutation = useMutation({ + mutationFn: async (action: "follow" | "unfollow") => { + if (action === "follow") { + const response = await axios.post(`/api/users/follow`, { + userId, + }); + return response.data; + } else { + const response = await axios.post(`/api/users/unfollow`, { + userId, + }); + return response.data; + } + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["followers"] }); + toast.success( + isFollowing + ? "Successfully unfollowed user" + : "Successfully followed user", + ); + }, + onError: () => { + toast.error("Error occurred while following/unfollowing user"); + }, + }); + + const handleFollow = () => { + if (!session?.data?.user?.email) { + toast.error("Please sign in to follow users"); + return; + } + followMutation.mutate(isFollowing ? "unfollow" : "follow"); + }; + + if (isCurrentUser || !session?.data?.user?.email) { + return null; + } + + return ( + + ); +} diff --git a/src/components/settings/profile.tsx b/src/components/settings/profile.tsx index c91fb152..c6ca7963 100644 --- a/src/components/settings/profile.tsx +++ b/src/components/settings/profile.tsx @@ -122,7 +122,7 @@ export default function ProfilePage() {

@@ -131,7 +131,7 @@ export default function ProfilePage() { Following diff --git a/src/components/settings/user-profile-stats.tsx b/src/components/settings/user-profile-stats.tsx new file mode 100644 index 00000000..f3501ebe --- /dev/null +++ b/src/components/settings/user-profile-stats.tsx @@ -0,0 +1,85 @@ +"use client"; + +import Link from "next/link"; +import { useRouter } from "next/navigation"; +import { useEffect, useState } from "react"; +import axios from "axios"; +import { Skeleton } from "@/components/ui/skeleton"; + +interface UserProfileStatsProps { + nerdAt: string; + userId: string; +} + +interface Stats { + following: number; + followers: number; +} + +export default function UserProfileStats({ nerdAt, userId }: UserProfileStatsProps) { + const router = useRouter(); + const [stats, setStats] = useState({ following: 0, followers: 0 }); + const [isLoading, setIsLoading] = useState(true); + + useEffect(() => { + const fetchStats = async () => { + try { + const [followingRes, followersRes] = await Promise.all([ + axios.get(`/api/users/${userId}/following?limit=1`), + axios.get(`/api/users/${userId}/followers?limit=1`) + ]); + + setStats({ + following: followingRes.data.pagination.total, + followers: followersRes.data.pagination.total + }); + } catch (error) { + console.error("Error fetching stats:", error); + } finally { + setIsLoading(false); + } + }; + + fetchStats(); + }, [userId]); + + if (isLoading) { + return ( +
+ + +
+ ); + } + + return ( +
+ { + e.preventDefault(); + router.push(`/app/profile/${nerdAt}/following`); + }} + > + + {stats.following} + {" "} + Following + + { + e.preventDefault(); + router.push(`/app/profile/${nerdAt}/followers`); + }} + > + + {stats.followers} + {" "} + Followers + +
+ ); +} \ No newline at end of file diff --git a/src/components/settings/user-profile.tsx b/src/components/settings/user-profile.tsx index ed6860c8..00861e60 100644 --- a/src/components/settings/user-profile.tsx +++ b/src/components/settings/user-profile.tsx @@ -19,8 +19,12 @@ import CollectionsTab from "./tabs/CollectionsTab"; import BookmarksTab from "./tabs/BookmarksTab"; import PrivateTab from "./tabs/PrivateTab"; import Link from "next/link"; +<<<<<<< HEAD import { usePathname } from "next/navigation"; import FollowList from "./follow-list"; +======= +import UserProfileStats from "./user-profile-stats"; +>>>>>>> e3f9868 (working : following and follower working for current user) export default function UserProfile() { const [activeTab, setActiveTab] = useState("posts"); @@ -88,7 +92,11 @@ export default function UserProfile() {

>>>>>> e3f9868 (working : following and follower working for current user) className="text-sm text-muted-foreground hover:text-primary" > @@ -97,7 +105,11 @@ export default function UserProfile() { Following >>>>>> e3f9868 (working : following and follower working for current user) className="text-sm text-muted-foreground hover:text-primary" > @@ -106,6 +118,15 @@ export default function UserProfile() { Followers
+<<<<<<< HEAD +======= + {userProfile?.nerdAt && userProfile?.id && ( + + )} +>>>>>>> e3f9868 (working : following and follower working for current user)
diff --git a/src/functions/follow.ts b/src/functions/follow.ts new file mode 100644 index 00000000..44d949a0 --- /dev/null +++ b/src/functions/follow.ts @@ -0,0 +1,105 @@ +import axios, { AxiosError } from "axios"; + +export interface FollowUser { + id: string; + name: string | null; + email: string | null; + image: string | null; + bio: string | null; + nerdAt: Date | null; + coverImage: string | null; +} + +export interface FollowResponse { + data: FollowUser[]; + pagination: { + nextCursor: string | null; + hasNextPage: boolean; + total: number; + }; +} + +export interface FollowStatus { + isFollowing: boolean; +} + +export class FollowError extends Error { + constructor( + message: string, + public status?: number, + ) { + super(message); + this.name = "FollowError"; + } +} + +export const followService = { + getFollowing: async (cursor?: string, limit: number = 10) => { + try { + const response = await axios.get("/api/users/following", { + params: { cursor, limit }, + }); + return response.data; + } catch (error) { + if (error instanceof AxiosError) { + throw new FollowError( + error.response?.data?.message || "Failed to fetch following", + error.response?.status, + ); + } + throw new FollowError("Failed to fetch following"); + } + }, + + getFollowers: async (cursor?: string, limit: number = 10) => { + try { + const response = await axios.get("/api/users/followers", { + params: { cursor, limit }, + }); + return response.data; + } catch (error) { + if (error instanceof AxiosError) { + throw new FollowError( + error.response?.data?.message || "Failed to fetch followers", + error.response?.status, + ); + } + throw new FollowError("Failed to fetch followers"); + } + }, + + getFollowStatus: async (userId: string) => { + try { + const response = await axios.get( + `/api/users/${userId}/follow-status`, + ); + return response.data; + } catch (error) { + if (error instanceof AxiosError) { + throw new FollowError( + error.response?.data?.message || "Failed to fetch follow status", + error.response?.status, + ); + } + throw new FollowError("Failed to fetch follow status"); + } + }, + + toggleFollow: async (userId: string, action: "follow" | "unfollow") => { + try { + const response = await axios.post( + `/api/users/${userId}/follow-status`, + { action }, + ); + return response.data; + } catch (error) { + if (error instanceof AxiosError) { + throw new FollowError( + error.response?.data?.message || `Failed to ${action} user`, + error.response?.status, + ); + } + throw new FollowError(`Failed to ${action} user`); + } + }, +};