Skip to content

Commit

Permalink
feat: Follow Service (#11)
Browse files Browse the repository at this point in the history
---

<details open="true"><summary>Generated summary (powered by <a href="https://app.graphite.dev">Graphite</a>)</summary>

> ## TL;DR
> This pull request includes several changes to different files. It adds functionality for following and unfollowing users, updates React components to handle follow and unfollow actions, imports a new library for displaying toast messages, creates new functions for checking if a user is being followed and following a user, and modifies the database schema to include fields for user relationships.
> 
> ## What changed
> - Added functions for following and unfollowing users
> - Updated React components to handle follow and unfollow actions
> - Imported a new library for displaying toast messages
> - Created new functions for checking if a user is being followed and following a user
> - Modified the database schema to include fields for user relationships
> 
> ## How to test
> 1. Run the application and navigate to the UserPage component
> 2. Verify that the Actions component is rendered with the correct props
> 3. Click the "Follow" or "Unfollow" button and observe the transition state
> 4. Check if the API call is successful and the appropriate toast message is displayed
> 5. Verify that the button is disabled while the transition is pending
> 6. Test the new functions for checking if a user is being followed and following a user
> 7. Verify that the database schema has been updated with the new fields for user relationships
> 
> ## Why make this change
> - The code adds functionality for following and unfollowing users, which enhances the user experience and allows users to connect with each other.
> - The React component updates improve the user interface and provide visual feedback for follow and unfollow actions.
> - The addition of the toast message library enhances the user experience by displaying success or error messages for follow and unfollow actions.
> - The new functions for checking if a user is being followed and following a user provide important functionality for managing user relationships.
> - The modifications to the database schema allow for more efficient querying and management of user relationships.
</details>
  • Loading branch information
Zaid-maker authored Dec 17, 2023
2 parents f7295bd + ff9bbe1 commit cbae191
Show file tree
Hide file tree
Showing 10 changed files with 300 additions and 5 deletions.
36 changes: 36 additions & 0 deletions actions/follow.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
"use server";

import { followUser, unfollowUser } from "@/lib/follow-service";
import { revalidatePath } from "next/cache";

export const onFollow = async (id: string) => {
try {
const followedUser = await followUser(id);

revalidatePath("/");

if (followedUser) {
revalidatePath(`/${followedUser.following.username}`);
}

return followedUser;
} catch (error) {
throw new Error("Interal Error");
}
};

export const onUnfollow = async (id: string) => {
try {
const unfollowedUser = await unfollowUser(id);

revalidatePath("/");

if (unfollowedUser) {
revalidatePath(`/${unfollowedUser.following.username}`);
}

return unfollowedUser;
} catch (error) {
throw new Error("Internal Error");
}
};
51 changes: 51 additions & 0 deletions app/(browse)/[username]/_components/actions.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
"use client";

import { onFollow, onUnfollow } from "@/actions/follow";
import { Button } from "@/components/ui/button";
import { useTransition } from "react";
import { toast } from "sonner";

interface ActionsProps {
isFollowing: boolean;
userId: string;
}

export const Actions = ({ isFollowing, userId }: ActionsProps) => {
const [isPending, startTransition] = useTransition();

const handleFollow = () => {
startTransition(() => {
onFollow(userId)
.then((data) =>
toast.success(`You are now following ${data.following.username}`)
)
.catch(() => toast.error("Something went wrong"));
});
};

const handleUnfollow = () => {
startTransition(() => {
onUnfollow(userId)
.then((data) =>
toast.success(`You have unfollowed ${data.following.username}`)
)
.catch(() => toast.error("Something went wrong"));
});
};

const onClick = () => {
if (isFollowing) {
handleUnfollow();
} else {
handleFollow();
}
};

return (
<>
<Button disabled={isPending} onClick={onClick} variant="primary">
{isFollowing ? "Unfollow" : "Follow"}
</Button>
</>
);
};
29 changes: 29 additions & 0 deletions app/(browse)/[username]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { getUserByUsername } from "@/lib/user-service";
import { notFound } from "next/navigation";
import React from "react";
import { Actions } from "./_components/actions";
import { isFollowingUser } from "@/lib/follow-service";

interface UserPageProps {
params: {
username: string;
};
}

const UserPage = async ({ params }: UserPageProps) => {
const user = await getUserByUsername(params.username);

if (!user) {
notFound();
}

const isFollowing = await isFollowingUser(params.username);

return (
<div>
<Actions isFollowing={isFollowing} userId={user.id} />
</div>
);
};

export default UserPage;
2 changes: 2 additions & 0 deletions app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import "./globals.css";
import { ClerkProvider } from "@clerk/nextjs";
import { dark } from "@clerk/themes";
import { ThemeProvider } from "@/components/theme-provider";
import { Toaster } from "sonner";

const inter = Inter({ subsets: ["latin"] });

Expand All @@ -26,6 +27,7 @@ export default function RootLayout({
forcedTheme="dark"
storageKey="gamehub-theme"
>
<Toaster theme="light" position="bottom-center" />
{children}
</ThemeProvider>
</body>
Expand Down
111 changes: 111 additions & 0 deletions lib/follow-service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import { getSelf } from "@/lib/auth-service";
import { db } from "@/lib/db";

export const isFollowingUser = async (id: string) => {
try {
const self = await getSelf();

const otherUser = await db.user.findUnique({
where: { id },
});

if (!otherUser) {
throw new Error("User not found");
}

if (otherUser.id === self.id) {
return true;
}

const existingFollow = await db.follow.findFirst({
where: {
followerId: self.id,
followingId: otherUser.id,
},
});

return !!existingFollow;
} catch {
return false;
}
};

export const followUser = async (id: string) => {
const self = await getSelf();

const otherUser = await db.user.findUnique({
where: { id },
});

if (!otherUser) {
throw new Error("User not found");
}

if (otherUser.id === self.id) {
throw new Error("Cannot follow yourself");
}

const existingFollow = await db.follow.findFirst({
where: {
followerId: self.id,
followingId: otherUser.id,
},
});

if (existingFollow) {
throw new Error("Already Following");
}

const follow = await db.follow.create({
data: {
followerId: self.id,
followingId: otherUser.id,
},
include: {
following: true,
follower: true,
},
});

return follow;
};

export const unfollowUser = async (id: string) => {
const self = await getSelf();

const otherUser = await db.user.findUnique({
where: {
id,
},
});

if (!otherUser) {
throw new Error("User not found");
}

if (otherUser.id === self.id) {
throw new Error("Cannot unfollow yourself");
}

const existingFollow = await db.follow.findFirst({
where: {
followerId: self.id,
followingId: otherUser.id,
},
});

if (!existingFollow) {
throw new Error("Not following");
}

const follow = await db.follow.delete({
where: {
id: existingFollow.id,
},
include: {
following: true,
},
});

return follow;
};
35 changes: 30 additions & 5 deletions lib/recommended-service.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,36 @@
import { db } from "./db";
import { db } from "@/lib/db";
import { getSelf } from "@/lib/auth-service";

export const getRecommended = async () => {
const users = await db.user.findMany({
orderBy: {
createAt: "desc",
let userId;

try {
const self = await getSelf();
userId = self.id;
} catch {
userId = null;
}

let users = [];

if (userId) {
users = await db.user.findMany({
where: {
NOT: {
id: userId,
},
},
orderBy: {
createAt: "desc",
},
});
} else {
users = await db.user.findMany({
orderBy: {
createAt: "desc",
},
});
}

return users;
return users;
};
11 changes: 11 additions & 0 deletions lib/user-service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { db } from "@/lib/db";

export const getUserByUsername = async (username: string) => {
const user = await db.user.findUnique({
where: {
username,
},
});

return user;
};
10 changes: 10 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"query-string": "^8.1.0",
"react": "^18",
"react-dom": "^18",
"sonner": "^1.2.4",
"svix": "^1.15.0",
"tailwind-merge": "^2.1.0",
"tailwindcss-animate": "^1.0.7",
Expand Down
19 changes: 19 additions & 0 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ model User {
externalUserId String @unique
bio String? @db.Text
following Follow[] @relation("Following")
followedBy Follow[] @relation("FollowedBy")
createAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
Expand All @@ -27,3 +30,19 @@ model Stream {
name String @db.Text
thumbnailUrl String? @db.Text
}

model Follow {
id String @id @default(uuid())
followerId String
followingId String
follower User @relation(name: "Following", fields: [followerId], references: [id], onDelete: Cascade)
following User @relation(name: "FollowedBy", fields: [followingId], references: [id], onDelete: Cascade)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([followerId, followingId])
@@index([followerId])
@@index([followingId])
}

0 comments on commit cbae191

Please sign in to comment.