├── .github ├── FUNDING.yml └── assets │ ├── README.md │ ├── activity.png │ ├── communities.png │ ├── community-profile.png │ ├── create-organization.png │ ├── create-thread.png │ ├── edit-profile.png │ ├── explore.png │ ├── home.png │ ├── my-profile-followers-tab.png │ ├── my-profile.png │ ├── onboarding.png │ ├── search.png │ ├── sign-in.png │ ├── sign-up.png │ ├── thread-likes-page.png │ ├── thread-page.png │ └── user-profile.png ├── .gitignore ├── LICENSE ├── README.md ├── app ├── (auth) │ ├── layout.tsx │ ├── onboarding │ │ └── page.tsx │ ├── sign-in │ │ └── [[...sign-in]] │ │ │ └── page.tsx │ └── sign-up │ │ └── [[...sign-up]] │ │ └── page.tsx ├── (root) │ ├── activity │ │ └── page.tsx │ ├── communities │ │ ├── [id] │ │ │ └── page.tsx │ │ └── page.tsx │ ├── create-thread │ │ └── page.tsx │ ├── edit-thread │ │ └── [id] │ │ │ └── page.tsx │ ├── explore │ │ └── page.tsx │ ├── layout.tsx │ ├── page.tsx │ ├── profile │ │ ├── [id] │ │ │ └── page.tsx │ │ └── edit │ │ │ └── page.tsx │ ├── search │ │ └── page.tsx │ └── thread │ │ ├── [id] │ │ └── page.tsx │ │ └── reactions │ │ └── [id] │ │ └── page.tsx ├── api │ ├── uploadthing │ │ ├── core.ts │ │ └── route.ts │ └── webhook │ │ └── clerk │ │ └── route.ts ├── favicon.ico └── globals.css ├── components.json ├── components ├── atoms │ ├── EditThread.tsx │ ├── FollowUser.tsx │ └── ReactThread.tsx ├── cards │ ├── CommunityCard.tsx │ ├── ThreadCard.tsx │ └── UserCard.tsx ├── forms │ ├── AccountProfile.tsx │ ├── Comment.tsx │ ├── DeleteThread.tsx │ └── PostThread.tsx ├── shared │ ├── Bottombar.tsx │ ├── LeftSidebar.tsx │ ├── Pagination.tsx │ ├── ProfileHeader.tsx │ ├── RightSidebar.tsx │ ├── Searchbar.tsx │ ├── ThreadsTab.tsx │ └── Topbar.tsx └── ui │ ├── button.tsx │ ├── form.tsx │ ├── input.tsx │ ├── label.tsx │ ├── tabs.tsx │ └── textarea.tsx ├── constants └── index.js ├── lib ├── actions │ ├── community.actions.ts │ ├── thread.actions.ts │ └── user.actions.ts ├── models │ ├── community.model.ts │ ├── thread.model.ts │ └── user.model.ts ├── mongoose.ts ├── uploadthing.ts ├── utils.ts └── validations │ ├── thread.ts │ └── user.ts ├── middleware.ts ├── next.config.js ├── package-lock.json ├── package.json ├── postcss.config.js ├── public ├── assets │ ├── community.svg │ ├── create.svg │ ├── delete-purple.svg │ ├── delete.svg │ ├── edit.svg │ ├── explore.svg │ ├── follow.svg │ ├── followers.svg │ ├── following.svg │ ├── heart-filled.svg │ ├── heart-gray.svg │ ├── heart.svg │ ├── home.svg │ ├── logo.svg │ ├── logout.svg │ ├── members.svg │ ├── more.svg │ ├── profile.svg │ ├── reply.svg │ ├── repost.svg │ ├── request.svg │ ├── search-gray.svg │ ├── search.svg │ ├── share.svg │ ├── tag.svg │ ├── unfollow.svg │ └── user.svg ├── next.svg └── vercel.svg ├── tailwind.config.js ├── tailwind.config.ts └── tsconfig.json /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: ladunjexa 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 13 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 14 | -------------------------------------------------------------------------------- /.github/assets/README.md: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /.github/assets/activity.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ladunjexa/nextjs13-threads/be6339e60fc907bd25426ac38528a4afbeb26532/.github/assets/activity.png -------------------------------------------------------------------------------- /.github/assets/communities.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ladunjexa/nextjs13-threads/be6339e60fc907bd25426ac38528a4afbeb26532/.github/assets/communities.png -------------------------------------------------------------------------------- /.github/assets/community-profile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ladunjexa/nextjs13-threads/be6339e60fc907bd25426ac38528a4afbeb26532/.github/assets/community-profile.png -------------------------------------------------------------------------------- /.github/assets/create-organization.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ladunjexa/nextjs13-threads/be6339e60fc907bd25426ac38528a4afbeb26532/.github/assets/create-organization.png -------------------------------------------------------------------------------- /.github/assets/create-thread.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ladunjexa/nextjs13-threads/be6339e60fc907bd25426ac38528a4afbeb26532/.github/assets/create-thread.png -------------------------------------------------------------------------------- /.github/assets/edit-profile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ladunjexa/nextjs13-threads/be6339e60fc907bd25426ac38528a4afbeb26532/.github/assets/edit-profile.png -------------------------------------------------------------------------------- /.github/assets/explore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ladunjexa/nextjs13-threads/be6339e60fc907bd25426ac38528a4afbeb26532/.github/assets/explore.png -------------------------------------------------------------------------------- /.github/assets/home.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ladunjexa/nextjs13-threads/be6339e60fc907bd25426ac38528a4afbeb26532/.github/assets/home.png -------------------------------------------------------------------------------- /.github/assets/my-profile-followers-tab.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ladunjexa/nextjs13-threads/be6339e60fc907bd25426ac38528a4afbeb26532/.github/assets/my-profile-followers-tab.png -------------------------------------------------------------------------------- /.github/assets/my-profile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ladunjexa/nextjs13-threads/be6339e60fc907bd25426ac38528a4afbeb26532/.github/assets/my-profile.png -------------------------------------------------------------------------------- /.github/assets/onboarding.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ladunjexa/nextjs13-threads/be6339e60fc907bd25426ac38528a4afbeb26532/.github/assets/onboarding.png -------------------------------------------------------------------------------- /.github/assets/search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ladunjexa/nextjs13-threads/be6339e60fc907bd25426ac38528a4afbeb26532/.github/assets/search.png -------------------------------------------------------------------------------- /.github/assets/sign-in.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ladunjexa/nextjs13-threads/be6339e60fc907bd25426ac38528a4afbeb26532/.github/assets/sign-in.png -------------------------------------------------------------------------------- /.github/assets/sign-up.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ladunjexa/nextjs13-threads/be6339e60fc907bd25426ac38528a4afbeb26532/.github/assets/sign-up.png -------------------------------------------------------------------------------- /.github/assets/thread-likes-page.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ladunjexa/nextjs13-threads/be6339e60fc907bd25426ac38528a4afbeb26532/.github/assets/thread-likes-page.png -------------------------------------------------------------------------------- /.github/assets/thread-page.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ladunjexa/nextjs13-threads/be6339e60fc907bd25426ac38528a4afbeb26532/.github/assets/thread-page.png -------------------------------------------------------------------------------- /.github/assets/user-profile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ladunjexa/nextjs13-threads/be6339e60fc907bd25426ac38528a4afbeb26532/.github/assets/user-profile.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env*.local 29 | 30 | # vercel 31 | .vercel 32 | 33 | # typescript 34 | *.tsbuildinfo 35 | next-env.d.ts 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Liron Abutbul 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /app/(auth)/layout.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import type { Metadata } from "next"; 3 | import { Inter } from "next/font/google"; 4 | import { ClerkProvider } from "@clerk/nextjs"; 5 | import { dark } from "@clerk/themes"; 6 | 7 | import "../globals.css"; 8 | 9 | export const metadata: Metadata = { 10 | title: "Threads", 11 | description: "A Next.js 13 Meta Threads Application clone", 12 | }; 13 | 14 | const inter = Inter({ subsets: ["latin"] }); 15 | 16 | export default function RootLayout({ 17 | children, 18 | }: { 19 | children: React.ReactNode; 20 | }) { 21 | return ( 22 | 27 | 28 | 29 |
30 | {children} 31 |
32 | 33 | 34 |
35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /app/(auth)/onboarding/page.tsx: -------------------------------------------------------------------------------- 1 | import { currentUser } from "@clerk/nextjs"; 2 | import { redirect } from "next/navigation"; 3 | 4 | import { fetchUser } from "@/lib/actions/user.actions"; 5 | import AccountProfile from "@/components/forms/AccountProfile"; 6 | 7 | async function Page() { 8 | const user = await currentUser(); 9 | if (!user) return null; 10 | 11 | const userInfo = await fetchUser(user.id); 12 | if (userInfo?.onboarded) redirect("/"); 13 | 14 | const userData = { 15 | id: user?.id, 16 | objectId: userInfo?._id, 17 | username: userInfo?.username || user?.username, 18 | name: userInfo?.name || user?.firstName || "", 19 | bio: userInfo?.bio || "", 20 | image: userInfo?.image || user?.imageUrl, 21 | }; 22 | 23 | return ( 24 |
25 |

Onboarding

26 |

27 | Complete your profile now to use Threads 28 |

29 | 30 |
31 | 32 |
33 |
34 | ); 35 | } 36 | 37 | export default Page; 38 | -------------------------------------------------------------------------------- /app/(auth)/sign-in/[[...sign-in]]/page.tsx: -------------------------------------------------------------------------------- 1 | import { SignIn } from "@clerk/nextjs"; 2 | 3 | export default function Page() { 4 | return ; 5 | } 6 | -------------------------------------------------------------------------------- /app/(auth)/sign-up/[[...sign-up]]/page.tsx: -------------------------------------------------------------------------------- 1 | import { SignUp } from "@clerk/nextjs"; 2 | 3 | export default function Page() { 4 | return ; 5 | } 6 | -------------------------------------------------------------------------------- /app/(root)/activity/page.tsx: -------------------------------------------------------------------------------- 1 | import Image from "next/image"; 2 | import Link from "next/link"; 3 | import { currentUser } from "@clerk/nextjs"; 4 | import { redirect } from "next/navigation"; 5 | 6 | import { fetchUser, getActivity } from "@/lib/actions/user.actions"; 7 | import { truncateString, formatDateWithMeasure } from "@/lib/utils"; 8 | 9 | async function Page() { 10 | const user = await currentUser(); 11 | if (!user) return null; 12 | 13 | const userInfo = await fetchUser(user.id); 14 | if (!userInfo?.onboarded) redirect("/onboarding"); 15 | 16 | const activity = await getActivity(userInfo._id); 17 | 18 | return ( 19 | <> 20 |

Activity

21 | 22 |
23 | {activity.length > 0 ? ( 24 | <> 25 | {activity.map((activity: any) => ( 26 | 33 |
34 | user_logo 41 | 48 |
49 | 50 | ))} 51 | 52 | ) : ( 53 |

No activity yet

54 | )} 55 |
56 | 57 | ); 58 | } 59 | 60 | const ActivityComponent = ({ author, createdAt, activityType, text }: any) => ( 61 |

62 | 63 | {author.name} 64 | {" "} 65 | <> 66 | {activityType === "follow" && "followed you"} 67 | {activityType === "reaction" && "like your thread"} 68 | {text && `replied to your thread: "${truncateString(text, 100)}"`} 69 | {" "} 70 | ~ {formatDateWithMeasure(createdAt)} 71 |

72 | ); 73 | 74 | export default Page; 75 | -------------------------------------------------------------------------------- /app/(root)/communities/[id]/page.tsx: -------------------------------------------------------------------------------- 1 | import Image from "next/image"; 2 | import { currentUser } from "@clerk/nextjs"; 3 | 4 | import { communityTabs } from "@/constants"; 5 | 6 | import UserCard from "@/components/cards/UserCard"; 7 | import ThreadsTab from "@/components/shared/ThreadsTab"; 8 | import ProfileHeader from "@/components/shared/ProfileHeader"; 9 | import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; 10 | 11 | import { fetchCommunityDetails } from "@/lib/actions/community.actions"; 12 | 13 | async function Page({ params }: { params: { id: string } }) { 14 | const user = await currentUser(); 15 | if (!user) return null; 16 | 17 | const communityDetails = await fetchCommunityDetails(params.id); 18 | 19 | return ( 20 |
21 | 30 | 31 |
32 | 33 | 34 | {communityTabs.map((tab) => ( 35 | 36 | {tab.label} 43 |

{tab.label}

44 | 45 | {tab.label === "Threads" && ( 46 |

47 | {communityDetails.threads.length} 48 |

49 | )} 50 |
51 | ))} 52 |
53 | 54 | 55 | {/* @ts-ignore */} 56 | 61 | 62 | 63 | 64 |
65 | {communityDetails.members.map((member: any) => ( 66 | 74 | ))} 75 |
76 |
77 | 78 | 79 | {/* @ts-ignore */} 80 | 85 | 86 |
87 |
88 |
89 | ); 90 | } 91 | 92 | export default Page; 93 | -------------------------------------------------------------------------------- /app/(root)/communities/page.tsx: -------------------------------------------------------------------------------- 1 | import { currentUser } from "@clerk/nextjs"; 2 | import { redirect } from "next/navigation"; 3 | 4 | import Searchbar from "@/components/shared/Searchbar"; 5 | import Pagination from "@/components/shared/Pagination"; 6 | import CommunityCard from "@/components/cards/CommunityCard"; 7 | 8 | import { fetchUser } from "@/lib/actions/user.actions"; 9 | import { fetchCommunities } from "@/lib/actions/community.actions"; 10 | 11 | async function Page({ 12 | searchParams, 13 | }: { 14 | searchParams: { [key: string]: string | undefined }; 15 | }) { 16 | const user = await currentUser(); 17 | if (!user) return null; 18 | 19 | const userInfo = await fetchUser(user.id); 20 | if (!userInfo?.onboarded) redirect("/onboarding"); 21 | 22 | // Fetch communities 23 | const result = await fetchCommunities({ 24 | searchTerm: searchParams.q, 25 | pageNumber: searchParams?.page ? +searchParams.page : 1, 26 | pageSize: 25, 27 | }); 28 | 29 | return ( 30 | <> 31 |

Communities

32 | 33 |
34 | 35 |
36 | 37 |
38 | {result.communities.length === 0 ? ( 39 |

No communities found

40 | ) : ( 41 | <> 42 | {result.communities.map((community) => ( 43 | 52 | ))} 53 | 54 | )} 55 |
56 | 57 | 62 | 63 | ); 64 | } 65 | 66 | export default Page; 67 | -------------------------------------------------------------------------------- /app/(root)/create-thread/page.tsx: -------------------------------------------------------------------------------- 1 | import { currentUser } from "@clerk/nextjs"; 2 | import { redirect } from "next/navigation"; 3 | 4 | import PostThread from "@/components/forms/PostThread"; 5 | import { fetchUser } from "@/lib/actions/user.actions"; 6 | 7 | async function Page() { 8 | const user = await currentUser(); 9 | if (!user) return null; 10 | 11 | // fetch organization list created by user 12 | const userInfo = await fetchUser(user.id); 13 | if (!userInfo?.onboarded) redirect("/onboarding"); 14 | 15 | return ( 16 | <> 17 |

Create Thread

18 | 19 | 20 | 21 | ); 22 | } 23 | 24 | export default Page; 25 | -------------------------------------------------------------------------------- /app/(root)/edit-thread/[id]/page.tsx: -------------------------------------------------------------------------------- 1 | import PostThread from "@/components/forms/PostThread"; 2 | import { fetchThreadById } from "@/lib/actions/thread.actions"; 3 | import { fetchUser } from "@/lib/actions/user.actions"; 4 | import { currentUser } from "@clerk/nextjs"; 5 | import { redirect } from "next/navigation"; 6 | import React from "react"; 7 | 8 | const Page = async ({ params }: { params: { id: string } }) => { 9 | if (!params.id) return null; 10 | 11 | const user = await currentUser(); 12 | if (!user) return null; 13 | 14 | const userInfo = await fetchUser(user.id); 15 | if (!userInfo?.onboarded) redirect("/onboarding"); 16 | 17 | const thread = await fetchThreadById(params.id); 18 | 19 | return ( 20 | <> 21 |

Edit Thread

22 | 23 | 28 | 29 | ); 30 | }; 31 | 32 | export default Page; 33 | -------------------------------------------------------------------------------- /app/(root)/explore/page.tsx: -------------------------------------------------------------------------------- 1 | import { currentUser } from "@clerk/nextjs"; 2 | import { redirect } from "next/navigation"; 3 | 4 | import ThreadCard from "@/components/cards/ThreadCard"; 5 | import Pagination from "@/components/shared/Pagination"; 6 | 7 | import { fetchExplore, getReactionsData } from "@/lib/actions/thread.actions"; 8 | import { fetchUser } from "@/lib/actions/user.actions"; 9 | 10 | async function Explore({ 11 | searchParams, 12 | }: { 13 | searchParams: { [key: string]: string | undefined }; 14 | }) { 15 | const user = await currentUser(); 16 | if (!user) return null; 17 | 18 | const userInfo = await fetchUser(user.id); 19 | if (!userInfo?.onboarded) redirect("/onboarding"); 20 | 21 | const result = await fetchExplore({ 22 | userId: user.id, 23 | pageNumber: searchParams?.page ? +searchParams.page : 1, 24 | pageSize: 30, 25 | }); 26 | 27 | const reactionsData = await getReactionsData({ 28 | userId: userInfo._id, 29 | posts: result.posts, 30 | }); 31 | 32 | const { childrenReactions, childrenReactionState } = reactionsData; 33 | 34 | return ( 35 | <> 36 |

Explore

37 | 38 |
39 | {result.posts.length === 0 ? ( 40 |

No threads found

41 | ) : ( 42 | <> 43 | {result.posts.map((post, idx) => ( 44 | 57 | ))} 58 | 59 | )} 60 |
61 | 62 | 67 | 68 | ); 69 | } 70 | 71 | export default Explore; 72 | -------------------------------------------------------------------------------- /app/(root)/layout.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import type { Metadata } from "next"; 3 | import { Inter } from "next/font/google"; 4 | import { ClerkProvider } from "@clerk/nextjs"; 5 | import { dark } from "@clerk/themes"; 6 | 7 | import "../globals.css"; 8 | import LeftSidebar from "@/components/shared/LeftSidebar"; 9 | import Bottombar from "@/components/shared/Bottombar"; 10 | import RightSidebar from "@/components/shared/RightSidebar"; 11 | import Topbar from "@/components/shared/Topbar"; 12 | 13 | const inter = Inter({ subsets: ["latin"] }); 14 | 15 | export const metadata: Metadata = { 16 | title: "Threads", 17 | description: "A Next.js 13 Meta Threads application", 18 | }; 19 | 20 | export default function RootLayout({ 21 | children, 22 | }: { 23 | children: React.ReactNode; 24 | }) { 25 | return ( 26 | 31 | 32 | 33 | 34 | 35 |
36 | 37 |
38 |
{children}
39 |
40 | {/* @ts-ignore */} 41 | 42 |
43 | 44 | 45 | 46 | 47 |
48 | ); 49 | } 50 | -------------------------------------------------------------------------------- /app/(root)/page.tsx: -------------------------------------------------------------------------------- 1 | import { currentUser } from "@clerk/nextjs"; 2 | import { redirect } from "next/navigation"; 3 | 4 | import ThreadCard from "@/components/cards/ThreadCard"; 5 | import Pagination from "@/components/shared/Pagination"; 6 | 7 | import { fetchPosts, getReactionsData } from "@/lib/actions/thread.actions"; 8 | import { fetchUser } from "@/lib/actions/user.actions"; 9 | 10 | async function Home({ 11 | searchParams, 12 | }: { 13 | searchParams: { [key: string]: string | undefined }; 14 | }) { 15 | const user = await currentUser(); 16 | if (!user) return null; 17 | 18 | const userInfo = await fetchUser(user.id); 19 | if (!userInfo?.onboarded) redirect("/onboarding"); 20 | 21 | const result = await fetchPosts( 22 | searchParams.page ? +searchParams.page : 1, 23 | 30 24 | ); 25 | 26 | const reactionsData = await getReactionsData({ 27 | userId: userInfo._id, 28 | posts: result.posts, 29 | }); 30 | 31 | const { childrenReactions, childrenReactionState } = reactionsData; 32 | 33 | return ( 34 | <> 35 |

Home

36 | 37 |
38 | {result.posts.length === 0 ? ( 39 |

No threads found

40 | ) : ( 41 | <> 42 | {result.posts.map((post, idx) => ( 43 | 56 | ))} 57 | 58 | )} 59 |
60 | 61 | 66 | 67 | ); 68 | } 69 | 70 | export default Home; 71 | -------------------------------------------------------------------------------- /app/(root)/profile/[id]/page.tsx: -------------------------------------------------------------------------------- 1 | import Image from "next/image"; 2 | import { currentUser } from "@clerk/nextjs"; 3 | import { redirect } from "next/navigation"; 4 | 5 | import { profileTabs } from "@/constants"; 6 | 7 | import ThreadsTab from "@/components/shared/ThreadsTab"; 8 | import ProfileHeader from "@/components/shared/ProfileHeader"; 9 | import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; 10 | 11 | import { 12 | fetchUser, 13 | fetchUsersByField, 14 | isUserFollowing, 15 | } from "@/lib/actions/user.actions"; 16 | import UserCard from "@/components/cards/UserCard"; 17 | 18 | async function Page({ params }: { params: { id: string } }) { 19 | const user = await currentUser(); 20 | if (!user) return null; 21 | 22 | const userInfo = await fetchUser(params.id); 23 | if (!userInfo?.onboarded) redirect("/onboarding"); 24 | 25 | const followers = await fetchUsersByField(params.id, "followers"); 26 | const following = await fetchUsersByField(params.id, "following"); 27 | 28 | const isFollowing = await isUserFollowing(user.id, params.id); 29 | 30 | return ( 31 |
32 | 41 | 42 |
43 | 44 | 45 | {profileTabs.map((tab) => ( 46 | 47 | {tab.label} 54 |

{tab.label}

55 | {tab.label === "Threads" && ( 56 |

57 | {userInfo.threadsCount} 58 |

59 | )} 60 | {tab.label === "Followers" && ( 61 |

62 | {userInfo.followersCount} 63 |

64 | )} 65 | {tab.label === "Following" && ( 66 |

67 | {userInfo.followingCount} 68 |

69 | )} 70 |
71 | ))} 72 |
73 | 74 | 75 | {/* @ts-ignore */}{" "} 76 | {userInfo.threadsCount === 0 ? ( 77 |
78 |

No threads found

79 |
80 | ) : ( 81 | 86 | )} 87 |
88 | 89 | 90 |
91 | {userInfo.followersCount === 0 ? ( 92 |

No users found

93 | ) : ( 94 | <> 95 | {followers.map((follower: any) => ( 96 | 104 | ))} 105 | 106 | )} 107 |
108 |
109 | 110 | 111 |
112 | {userInfo.followingCount === 0 ? ( 113 |

No users found

114 | ) : ( 115 | <> 116 | {following.map((following: any) => ( 117 | 125 | ))} 126 | 127 | )} 128 |
129 |
130 |
131 |
132 |
133 | ); 134 | } 135 | export default Page; 136 | -------------------------------------------------------------------------------- /app/(root)/profile/edit/page.tsx: -------------------------------------------------------------------------------- 1 | import { currentUser } from "@clerk/nextjs"; 2 | import { redirect } from "next/navigation"; 3 | 4 | import { fetchUser } from "@/lib/actions/user.actions"; 5 | import AccountProfile from "@/components/forms/AccountProfile"; 6 | 7 | // Copy paste most of the code as it is from the /onboarding 8 | 9 | async function Page() { 10 | const user = await currentUser(); 11 | if (!user) return null; 12 | 13 | const userInfo = await fetchUser(user.id); 14 | if (!userInfo?.onboarded) redirect("/onboarding"); 15 | 16 | const userData = { 17 | id: user.id, 18 | objectId: userInfo?._id, 19 | username: userInfo ? userInfo?.username : user.username, 20 | name: userInfo ? userInfo?.name : user.firstName ?? "", 21 | bio: userInfo ? userInfo?.bio : "", 22 | image: userInfo ? userInfo?.image : user.imageUrl, 23 | }; 24 | 25 | return ( 26 | <> 27 |

Edit Profile

28 |

Make any changes

29 | 30 |
31 | 32 |
33 | 34 | ); 35 | } 36 | 37 | export default Page; 38 | -------------------------------------------------------------------------------- /app/(root)/search/page.tsx: -------------------------------------------------------------------------------- 1 | import { redirect } from "next/navigation"; 2 | import { currentUser } from "@clerk/nextjs"; 3 | 4 | import UserCard from "@/components/cards/UserCard"; 5 | import Searchbar from "@/components/shared/Searchbar"; 6 | import Pagination from "@/components/shared/Pagination"; 7 | 8 | import { fetchUser, fetchUsers } from "@/lib/actions/user.actions"; 9 | 10 | async function Page({ 11 | searchParams, 12 | }: { 13 | searchParams: { [key: string]: string | undefined }; 14 | }) { 15 | const user = await currentUser(); 16 | if (!user) return null; 17 | 18 | const userInfo = await fetchUser(user.id); 19 | if (!userInfo?.onboarded) redirect("/onboarding"); 20 | 21 | const result = await fetchUsers({ 22 | userId: user.id, 23 | searchTerm: searchParams.q, 24 | pageNumber: searchParams?.page ? +searchParams.page : 1, 25 | pageSize: 25, 26 | }); 27 | 28 | return ( 29 |
30 |

Search

31 | 32 | 33 | 34 |
35 | {result.users.length === 0 ? ( 36 |

No users found

37 | ) : ( 38 | <> 39 | {result.users.map((person) => ( 40 | 48 | ))} 49 | 50 | )} 51 |
52 | 53 | 58 |
59 | ); 60 | } 61 | 62 | export default Page; 63 | -------------------------------------------------------------------------------- /app/(root)/thread/[id]/page.tsx: -------------------------------------------------------------------------------- 1 | import { redirect } from "next/navigation"; 2 | import { currentUser } from "@clerk/nextjs"; 3 | 4 | import Comment from "@/components/forms/Comment"; 5 | import ThreadCard from "@/components/cards/ThreadCard"; 6 | 7 | import { fetchUser } from "@/lib/actions/user.actions"; 8 | import { 9 | fetchThreadById, 10 | getReactionsData, 11 | } from "@/lib/actions/thread.actions"; 12 | 13 | async function Page({ params }: { params: { id: string } }) { 14 | if (!params.id) return null; 15 | 16 | const user = await currentUser(); 17 | if (!user) return null; 18 | 19 | const userInfo = await fetchUser(user.id); 20 | if (!userInfo?.onboarded) redirect("/onboarding"); 21 | 22 | const thread = await fetchThreadById(params.id); 23 | 24 | const reactionsData = await getReactionsData({ 25 | userId: userInfo._id, 26 | posts: thread.children, 27 | parentId: thread._id, 28 | }); 29 | 30 | const { 31 | parentReactions, 32 | parentReactionState, 33 | childrenReactions, 34 | childrenReactionState, 35 | } = reactionsData; 36 | 37 | return ( 38 |
39 |
40 | 52 |
53 | 54 |
55 | 60 |
61 | 62 |
63 | {thread.children.map((childItem: any, idx: number) => ( 64 | 78 | ))} 79 |
80 |
81 | ); 82 | } 83 | 84 | export default Page; 85 | -------------------------------------------------------------------------------- /app/(root)/thread/reactions/[id]/page.tsx: -------------------------------------------------------------------------------- 1 | import { redirect } from "next/navigation"; 2 | import { currentUser } from "@clerk/nextjs"; 3 | 4 | import Comment from "@/components/forms/Comment"; 5 | import ThreadCard from "@/components/cards/ThreadCard"; 6 | 7 | import { fetchUser } from "@/lib/actions/user.actions"; 8 | import { 9 | fetchThreadById, 10 | getReactedUsersByThread, 11 | isThreadReactedByUser, 12 | } from "@/lib/actions/thread.actions"; 13 | import UserCard from "@/components/cards/UserCard"; 14 | 15 | export const revalidate = 0; 16 | 17 | async function page({ params }: { params: { id: string } }) { 18 | if (!params.id) return null; 19 | 20 | const user = await currentUser(); 21 | if (!user) return null; 22 | 23 | const userInfo = await fetchUser(user.id); 24 | if (!userInfo?.onboarded) redirect("/onboarding"); 25 | 26 | const thread = await fetchThreadById(params.id); 27 | 28 | const reactions = await getReactedUsersByThread(thread._id); 29 | 30 | const reactionState = await isThreadReactedByUser({ 31 | threadId: thread._id, 32 | userId: userInfo._id, 33 | }); 34 | 35 | return ( 36 |
37 |
38 | 50 |
51 | 52 |
53 | 58 |
59 | 60 |
61 |

People who likes

62 | {thread.reactionsCount === 0 ? ( 63 |

No users found

64 | ) : ( 65 | <> 66 | {reactions.users.map((reaction: any) => ( 67 | 75 | ))} 76 | 77 | )} 78 |
79 |
80 | ); 81 | } 82 | 83 | export default page; 84 | -------------------------------------------------------------------------------- /app/api/uploadthing/core.ts: -------------------------------------------------------------------------------- 1 | // Resource: https://docs.uploadthing.com/nextjs/appdir#creating-your-first-fileroute 2 | // Above resource shows how to setup uploadthing. Copy paste most of it as it is. 3 | // We're changing a few things in the middleware and configs of the file upload i.e., "media", "maxFileCount" 4 | 5 | import { currentUser } from "@clerk/nextjs"; 6 | import { createUploadthing, type FileRouter } from "uploadthing/next"; 7 | 8 | const f = createUploadthing(); 9 | 10 | const getUser = async () => await currentUser(); 11 | 12 | export const ourFileRouter = { 13 | // Define as many FileRoutes as you like, each with a unique routeSlug 14 | media: f({ image: { maxFileSize: "4MB", maxFileCount: 1 } }) 15 | // Set permissions and file types for this FileRoute 16 | .middleware(async (req) => { 17 | // This code runs on your server before upload 18 | const user = await getUser(); 19 | 20 | // If you throw, the user will not be able to upload 21 | if (!user) throw new Error("Unauthorized"); 22 | 23 | // Whatever is returned here is accessible in onUploadComplete as `metadata` 24 | return { userId: user.id }; 25 | }) 26 | .onUploadComplete(async ({ metadata, file }) => { 27 | // This code RUNS ON YOUR SERVER after upload 28 | console.log("Upload complete for userId:", metadata.userId); 29 | 30 | console.log("file url", file.url); 31 | }), 32 | } satisfies FileRouter; 33 | 34 | export type OurFileRouter = typeof ourFileRouter; 35 | -------------------------------------------------------------------------------- /app/api/uploadthing/route.ts: -------------------------------------------------------------------------------- 1 | // Resource: https://docs.uploadthing.com/nextjs/appdir#create-a-nextjs-api-route-using-the-filerouter 2 | // Copy paste (be careful with imports) 3 | 4 | import { createNextRouteHandler } from "uploadthing/next"; 5 | 6 | import { ourFileRouter } from "./core"; 7 | 8 | // Export routes for Next App Router 9 | export const { GET, POST } = createNextRouteHandler({ 10 | router: ourFileRouter, 11 | }); 12 | -------------------------------------------------------------------------------- /app/api/webhook/clerk/route.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable camelcase */ 2 | // Resource: https://clerk.com/docs/users/sync-data-to-your-backend 3 | // Above article shows why we need webhooks i.e., to sync data to our backend 4 | 5 | // Resource: https://docs.svix.com/receiving/verifying-payloads/why 6 | // It's a good practice to verify webhooks. Above article shows why we should do it 7 | import { Webhook, WebhookRequiredHeaders } from "svix"; 8 | import { headers } from "next/headers"; 9 | 10 | import { IncomingHttpHeaders } from "http"; 11 | 12 | import { NextResponse } from "next/server"; 13 | import { 14 | addMemberToCommunity, 15 | createCommunity, 16 | deleteCommunity, 17 | removeUserFromCommunity, 18 | updateCommunityInfo, 19 | } from "@/lib/actions/community.actions"; 20 | 21 | // Resource: https://clerk.com/docs/integration/webhooks#supported-events 22 | // Above document lists the supported events 23 | type EventType = 24 | | "organization.created" 25 | | "organizationInvitation.created" 26 | | "organizationMembership.created" 27 | | "organizationMembership.deleted" 28 | | "organization.updated" 29 | | "organization.deleted"; 30 | 31 | type Event = { 32 | data: Record[]>; 33 | object: "event"; 34 | type: EventType; 35 | }; 36 | 37 | export const POST = async (request: Request) => { 38 | const payload = await request.json(); 39 | const header = headers(); 40 | 41 | const heads = { 42 | "svix-id": header.get("svix-id"), 43 | "svix-timestamp": header.get("svix-timestamp"), 44 | "svix-signature": header.get("svix-signature"), 45 | }; 46 | 47 | // Activitate Webhook in the Clerk Dashboard. 48 | // After adding the endpoint, you'll see the secret on the right side. 49 | const wh = new Webhook(process.env.NEXT_CLERK_WEBHOOK_SECRET || ""); 50 | 51 | let evnt: Event | null = null; 52 | 53 | try { 54 | evnt = wh.verify( 55 | JSON.stringify(payload), 56 | heads as IncomingHttpHeaders & WebhookRequiredHeaders 57 | ) as Event; 58 | } catch (err) { 59 | return NextResponse.json({ message: err }, { status: 400 }); 60 | } 61 | 62 | const eventType: EventType = evnt?.type!; 63 | 64 | // Listen organization creation event 65 | if (eventType === "organization.created") { 66 | // Resource: https://clerk.com/docs/reference/backend-api/tag/Organizations#operation/CreateOrganization 67 | // Show what evnt?.data sends from above resource 68 | const { id, name, slug, logo_url, image_url, created_by } = 69 | evnt?.data ?? {}; 70 | 71 | try { 72 | // @ts-ignore 73 | await createCommunity( 74 | // @ts-ignore 75 | id, 76 | name, 77 | slug, 78 | logo_url || image_url, 79 | "org bio", 80 | created_by 81 | ); 82 | 83 | return NextResponse.json({ message: "User created" }, { status: 201 }); 84 | } catch (err) { 85 | console.log(err); 86 | return NextResponse.json( 87 | { message: "Internal Server Error" }, 88 | { status: 500 } 89 | ); 90 | } 91 | } 92 | 93 | // Listen organization invitation creation event. 94 | // Just to show. You can avoid this or tell people that we can create a new mongoose action and 95 | // add pending invites in the database. 96 | if (eventType === "organizationInvitation.created") { 97 | try { 98 | // Resource: https://clerk.com/docs/reference/backend-api/tag/Organization-Invitations#operation/CreateOrganizationInvitation 99 | console.log("Invitation created", evnt?.data); 100 | 101 | return NextResponse.json( 102 | { message: "Invitation created" }, 103 | { status: 201 } 104 | ); 105 | } catch (err) { 106 | console.log(err); 107 | 108 | return NextResponse.json( 109 | { message: "Internal Server Error" }, 110 | { status: 500 } 111 | ); 112 | } 113 | } 114 | 115 | // Listen organization membership (member invite & accepted) creation 116 | if (eventType === "organizationMembership.created") { 117 | try { 118 | // Resource: https://clerk.com/docs/reference/backend-api/tag/Organization-Memberships#operation/CreateOrganizationMembership 119 | // Show what evnt?.data sends from above resource 120 | const { organization, public_user_data } = evnt?.data; 121 | console.log("created", evnt?.data); 122 | 123 | // @ts-ignore 124 | await addMemberToCommunity(organization.id, public_user_data.user_id); 125 | 126 | return NextResponse.json( 127 | { message: "Invitation accepted" }, 128 | { status: 201 } 129 | ); 130 | } catch (err) { 131 | console.log(err); 132 | 133 | return NextResponse.json( 134 | { message: "Internal Server Error" }, 135 | { status: 500 } 136 | ); 137 | } 138 | } 139 | 140 | // Listen member deletion event 141 | if (eventType === "organizationMembership.deleted") { 142 | try { 143 | // Resource: https://clerk.com/docs/reference/backend-api/tag/Organization-Memberships#operation/DeleteOrganizationMembership 144 | // Show what evnt?.data sends from above resource 145 | const { organization, public_user_data } = evnt?.data; 146 | console.log("removed", evnt?.data); 147 | 148 | // @ts-ignore 149 | await removeUserFromCommunity(public_user_data.user_id, organization.id); 150 | 151 | return NextResponse.json({ message: "Member removed" }, { status: 201 }); 152 | } catch (err) { 153 | console.log(err); 154 | 155 | return NextResponse.json( 156 | { message: "Internal Server Error" }, 157 | { status: 500 } 158 | ); 159 | } 160 | } 161 | 162 | // Listen organization updation event 163 | if (eventType === "organization.updated") { 164 | try { 165 | // Resource: https://clerk.com/docs/reference/backend-api/tag/Organizations#operation/UpdateOrganization 166 | // Show what evnt?.data sends from above resource 167 | const { id, logo_url, name, slug } = evnt?.data; 168 | console.log("updated", evnt?.data); 169 | 170 | // @ts-ignore 171 | await updateCommunityInfo(id, name, slug, logo_url); 172 | 173 | return NextResponse.json({ message: "Member removed" }, { status: 201 }); 174 | } catch (err) { 175 | console.log(err); 176 | 177 | return NextResponse.json( 178 | { message: "Internal Server Error" }, 179 | { status: 500 } 180 | ); 181 | } 182 | } 183 | 184 | // Listen organization deletion event 185 | if (eventType === "organization.deleted") { 186 | try { 187 | // Resource: https://clerk.com/docs/reference/backend-api/tag/Organizations#operation/DeleteOrganization 188 | // Show what evnt?.data sends from above resource 189 | const { id } = evnt?.data; 190 | console.log("deleted", evnt?.data); 191 | 192 | // @ts-ignore 193 | await deleteCommunity(id); 194 | 195 | return NextResponse.json( 196 | { message: "Organization deleted" }, 197 | { status: 201 } 198 | ); 199 | } catch (err) { 200 | console.log(err); 201 | 202 | return NextResponse.json( 203 | { message: "Internal Server Error" }, 204 | { status: 500 } 205 | ); 206 | } 207 | } 208 | }; -------------------------------------------------------------------------------- /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ladunjexa/nextjs13-threads/be6339e60fc907bd25426ac38528a4afbeb26532/app/favicon.ico -------------------------------------------------------------------------------- /app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer components { 6 | /* main */ 7 | .main-container { 8 | @apply flex min-h-screen flex-1 flex-col items-center bg-dark-1 px-6 pb-10 pt-28 max-md:pb-32 sm:px-10; 9 | } 10 | 11 | /* Head Text */ 12 | .head-text { 13 | @apply text-heading2-bold text-light-1; 14 | } 15 | 16 | /* Activity */ 17 | .activity-card { 18 | @apply flex items-center gap-2 rounded-md bg-dark-2 px-7 py-4; 19 | } 20 | 21 | /* No Result */ 22 | .no-result { 23 | @apply text-center !text-base-regular text-light-3; 24 | } 25 | 26 | /* Community Card */ 27 | .community-card { 28 | @apply w-full rounded-lg bg-dark-3 px-4 py-5 sm:w-96; 29 | } 30 | 31 | .community-card_btn { 32 | @apply rounded-lg bg-primary-500 px-5 py-1.5 text-small-regular !text-light-1 !important; 33 | } 34 | 35 | /* thread card */ 36 | .thread-card_bar { 37 | @apply relative mt-2 w-0.5 grow rounded-full bg-neutral-800; 38 | } 39 | 40 | /* User card */ 41 | .user-card { 42 | @apply flex flex-col justify-between gap-4 max-xs:rounded-xl max-xs:bg-dark-3 max-xs:p-4 xs:flex-row xs:items-center; 43 | } 44 | 45 | .user-card_avatar { 46 | @apply flex flex-1 items-start justify-start gap-3 xs:items-center; 47 | } 48 | 49 | .user-card_btn { 50 | @apply h-auto min-w-[74px] rounded-lg bg-primary-500 text-[12px] text-light-1 !important; 51 | } 52 | 53 | .follow-card_btn { 54 | @apply rounded-lg bg-primary-500 px-3 py-1.5 text-small-regular !text-light-1 !important; 55 | } 56 | 57 | .searchbar { 58 | @apply flex gap-1 rounded-lg bg-dark-3 px-4 py-2; 59 | } 60 | 61 | .searchbar_input { 62 | @apply border-none bg-dark-3 text-base-regular text-light-4 outline-none !important; 63 | } 64 | 65 | .topbar { 66 | @apply fixed top-0 z-30 flex w-full items-center justify-between bg-dark-2 px-6 py-3; 67 | } 68 | 69 | .bottombar { 70 | @apply fixed bottom-0 z-10 w-full rounded-t-3xl bg-glassmorphism p-4 backdrop-blur-lg xs:px-7 md:hidden; 71 | } 72 | 73 | .bottombar_container { 74 | @apply flex items-center justify-between gap-3 xs:gap-5; 75 | } 76 | 77 | .bottombar_link { 78 | @apply relative flex flex-col items-center gap-2 rounded-lg p-2 sm:flex-1 sm:px-2 sm:py-2.5; 79 | } 80 | 81 | .leftsidebar { 82 | @apply sticky left-0 top-0 z-20 flex h-screen w-fit flex-col justify-between overflow-auto border-r border-r-dark-4 bg-dark-2 pb-5 pt-28 max-md:hidden; 83 | } 84 | 85 | .leftsidebar_link { 86 | @apply relative flex justify-start gap-4 rounded-lg p-4; 87 | } 88 | 89 | .pagination { 90 | @apply mt-10 flex w-full items-center justify-center gap-5; 91 | } 92 | 93 | .rightsidebar { 94 | @apply sticky right-0 top-0 z-20 flex h-screen w-fit flex-col justify-between gap-12 overflow-auto border-l border-l-dark-4 bg-dark-2 px-10 pb-6 pt-28 max-xl:hidden; 95 | } 96 | } 97 | 98 | @layer utilities { 99 | .css-invert { 100 | @apply invert-[50%] brightness-200; 101 | } 102 | 103 | .custom-scrollbar::-webkit-scrollbar { 104 | width: 3px; 105 | height: 3px; 106 | border-radius: 2px; 107 | } 108 | 109 | .custom-scrollbar::-webkit-scrollbar-track { 110 | background: #09090a; 111 | } 112 | 113 | .custom-scrollbar::-webkit-scrollbar-thumb { 114 | background: #5c5c7b; 115 | border-radius: 50px; 116 | } 117 | 118 | .custom-scrollbar::-webkit-scrollbar-thumb:hover { 119 | background: #7878a3; 120 | } 121 | } 122 | 123 | /* Clerk Responsive fix */ 124 | .cl-organizationSwitcherTrigger .cl-userPreview .cl-userPreviewTextContainer { 125 | @apply max-sm:hidden; 126 | } 127 | 128 | .cl-organizationSwitcherTrigger 129 | .cl-organizationPreview 130 | .cl-organizationPreviewTextContainer { 131 | @apply max-sm:hidden; 132 | } 133 | 134 | /* Shadcn Component Styles */ 135 | 136 | /* Tab */ 137 | .tab { 138 | @apply flex min-h-[50px] flex-1 items-center gap-3 bg-dark-2 text-light-2 data-[state=active]:bg-[#0e0e12] data-[state=active]:text-light-2 !important; 139 | } 140 | 141 | .no-focus { 142 | @apply focus-visible:ring-0 focus-visible:ring-transparent focus-visible:ring-offset-0 !important; 143 | } 144 | 145 | /* Account Profile */ 146 | .account-form_image-label { 147 | @apply flex h-24 w-24 items-center justify-center rounded-full bg-dark-4 !important; 148 | } 149 | 150 | .account-form_image-input { 151 | @apply cursor-pointer border-none bg-transparent outline-none file:text-blue !important; 152 | } 153 | 154 | .account-form_input { 155 | @apply border border-dark-4 bg-dark-3 text-light-1 !important; 156 | } 157 | 158 | /* Comment Form */ 159 | .comment-form { 160 | @apply mt-10 flex items-center gap-4 border-y border-y-dark-4 py-5 max-xs:flex-col !important; 161 | } 162 | 163 | .comment-form_btn { 164 | @apply rounded-3xl bg-primary-500 px-8 py-2 !text-small-regular text-light-1 max-xs:w-full !important; 165 | } 166 | 167 | .bg-clerk-auth { 168 | @apply bg-[linear-gradient(_to_right_top,#d16ba5,#c262a6,#b25aa7,#a054a8,#8c4ea9,#775fbe,#596fce,#257dda,#00a0ef,#00c1f7,#00dff6,#5ffbf1_)]; 169 | } 170 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "default", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.js", 8 | "css": "app/globals.css", 9 | "baseColor": "slate", 10 | "cssVariables": false 11 | }, 12 | "aliases": { 13 | "components": "@/components", 14 | "utils": "@/lib/utils" 15 | } 16 | } -------------------------------------------------------------------------------- /components/atoms/EditThread.tsx: -------------------------------------------------------------------------------- 1 | import Image from "next/image"; 2 | import Link from "next/link"; 3 | import React from "react"; 4 | 5 | interface Props { 6 | threadId: string; 7 | currentUserId: string; 8 | authorId: string; 9 | } 10 | 11 | const EditThread = ({ threadId, currentUserId, authorId }: Props) => { 12 | if (currentUserId !== authorId) return null; 13 | 14 | return ( 15 | 16 | edit thread 23 | 24 | ); 25 | }; 26 | 27 | export default EditThread; 28 | -------------------------------------------------------------------------------- /components/atoms/FollowUser.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import Image from "next/image"; 4 | import React from "react"; 5 | import { Button } from "../ui/button"; 6 | import { followUser } from "@/lib/actions/user.actions"; 7 | import { usePathname } from "next/navigation"; 8 | 9 | interface Props { 10 | userId: string; 11 | currentUserId: string; 12 | isFollowing?: boolean; 13 | } 14 | 15 | const FollowUser = ({ userId, currentUserId, isFollowing = false }: Props) => { 16 | const pathname = usePathname(); 17 | 18 | const handleClick = async () => { 19 | await followUser({ 20 | followerId: currentUserId, 21 | followedId: userId, 22 | path: pathname, 23 | }); 24 | }; 25 | 26 | return ( 27 | 36 | ); 37 | }; 38 | 39 | export default FollowUser; 40 | -------------------------------------------------------------------------------- /components/atoms/ReactThread.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import Image from "next/image"; 4 | import React from "react"; 5 | import { usePathname } from "next/navigation"; 6 | import { addReactToThread } from "@/lib/actions/thread.actions"; 7 | 8 | interface Props { 9 | threadId: string; 10 | currentUserId: string; 11 | interactState?: boolean; 12 | isComment?: boolean; 13 | parentId?: string | null; 14 | } 15 | 16 | const ReactThread = ({ 17 | threadId, 18 | currentUserId, 19 | interactState = false, 20 | isComment = false, 21 | parentId = null, 22 | }: Props) => { 23 | const pathname = usePathname(); 24 | 25 | const handleClick = async () => { 26 | await addReactToThread({ 27 | threadId, 28 | userId: currentUserId, 29 | path: pathname, 30 | }); 31 | }; 32 | 33 | return ( 34 | heart 42 | ); 43 | }; 44 | 45 | export default ReactThread; 46 | -------------------------------------------------------------------------------- /components/cards/CommunityCard.tsx: -------------------------------------------------------------------------------- 1 | import Image from "next/image"; 2 | import Link from "next/link"; 3 | 4 | import { Button } from "../ui/button"; 5 | 6 | interface Props { 7 | id: string; 8 | name: string; 9 | username: string; 10 | imgUrl: string; 11 | bio: string; 12 | members: { 13 | image: string; 14 | }[]; 15 | } 16 | 17 | function CommunityCard({ id, name, username, imgUrl, bio, members }: Props) { 18 | return ( 19 |
20 |
21 | 22 | community_logo 28 | 29 | 30 |
31 | 32 |

{name}

33 | 34 |

@{username}

35 |
36 |
37 | 38 |

{bio}

39 | 40 |
41 | 42 | 45 | 46 | 47 | {members.length > 0 && ( 48 |
49 | {members.map((member, index) => ( 50 | {`user_${index}`} 60 | ))} 61 | {members.length > 3 && ( 62 |

63 | {members.length}+ Users 64 |

65 | )} 66 |
67 | )} 68 |
69 |
70 | ); 71 | } 72 | 73 | export default CommunityCard; 74 | -------------------------------------------------------------------------------- /components/cards/ThreadCard.tsx: -------------------------------------------------------------------------------- 1 | import Image from "next/image"; 2 | import Link from "next/link"; 3 | 4 | import { formatDateString } from "@/lib/utils"; 5 | import DeleteThread from "../forms/DeleteThread"; 6 | import EditThread from "../atoms/EditThread"; 7 | import ReactThread from "../atoms/ReactThread"; 8 | 9 | interface Props { 10 | id: string; 11 | currentUserId: string; 12 | parentId: string | null; 13 | content: string; 14 | author: { 15 | name: string; 16 | image: string; 17 | id: string; 18 | }; 19 | community: { 20 | id: string; 21 | name: string; 22 | image: string; 23 | } | null; 24 | createdAt: string; 25 | comments: { 26 | author: { 27 | image: string; 28 | }; 29 | }[]; 30 | reactions: { 31 | image: string; 32 | _id: string; 33 | id: string; 34 | name: string; 35 | username: string; 36 | }[]; 37 | isComment?: boolean; 38 | reactState?: boolean; 39 | } 40 | 41 | function ThreadCard({ 42 | id, 43 | currentUserId, 44 | parentId, 45 | content, 46 | author, 47 | community, 48 | createdAt, 49 | comments, 50 | reactions, 51 | isComment, 52 | reactState, 53 | }: Props) { 54 | return ( 55 |
60 |
61 |
62 |
63 | 64 | Profile image 70 | 71 | 72 |
73 |
74 | 75 |
76 | 77 |

78 | {author.name} 79 |

80 | 81 | 82 |

{content}

83 | 84 |
85 |
86 | 93 | 94 | reply 101 | 102 | repost 109 | share 116 |
117 | 118 |
119 | {isComment && ( 120 | <> 121 | {comments.length > 0 && ( 122 | 123 |

124 | {comments.length}{" "} 125 | {comments.length > 1 ? "replies" : "reply"} 126 |

127 | 128 | )} 129 | 130 | {comments.length > 0 && reactions.length > 0 && ( 131 |

132 | )} 133 | 134 | {reactions.length > 0 && ( 135 | 136 |

137 | {reactions.length}{" "} 138 | {reactions.length > 1 ? "likes" : "like"} 139 |

140 | 141 | )} 142 | 143 | )} 144 |
145 |
146 |
147 |
148 | 149 |
150 | 157 | 162 |
163 |
164 | 165 |
166 | {!isComment && ( 167 | <> 168 | {comments.length > 0 && ( 169 |
170 | {comments.slice(0, 2).map((comment, index) => ( 171 | {`user_${index}`} 181 | ))} 182 | 183 | 184 |

185 | {comments.length}{" "} 186 | {comments.length > 1 ? "replies" : "reply"} 187 |

188 | 189 |
190 | )} 191 | 192 | {/* {comments.length > 0 && reactions.length > 0 && ( 193 |
194 |

195 |
196 | )} */} 197 | 198 | {reactions.length > 0 && ( 199 |
200 | {reactions.slice(0, 2).map((reaction, index) => ( 201 | {`user_${index}`} 211 | ))} 212 | 213 | 214 |

215 | {reactions.length} {reactions.length > 1 ? "likes" : "like"} 216 |

217 | 218 |
219 | )} 220 | 221 | )} 222 |
223 | 224 | {!isComment && community && ( 225 | 229 |

230 | {formatDateString(createdAt)} 231 | {community && ` - ${community.name} Community`} 232 |

233 | 234 | {community.name} 241 | 242 | )} 243 |
244 | ); 245 | } 246 | 247 | export default ThreadCard; 248 | -------------------------------------------------------------------------------- /components/cards/UserCard.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import Image from "next/image"; 4 | import { useRouter } from "next/navigation"; 5 | 6 | import { Button } from "../ui/button"; 7 | 8 | interface Props { 9 | id: string; 10 | name: string; 11 | username: string; 12 | imgUrl: string; 13 | personType: string; 14 | } 15 | 16 | function UserCard({ id, name, username, imgUrl, personType }: Props) { 17 | const router = useRouter(); 18 | 19 | return ( 20 |
21 |
22 |
23 | user_logo 29 |
30 | 31 |
32 |

{name}

33 |

@{username}

34 |
35 |
36 | 37 | 45 |
46 | ); 47 | } 48 | 49 | export default UserCard; 50 | -------------------------------------------------------------------------------- /components/forms/AccountProfile.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as z from "zod"; 4 | import Image from "next/image"; 5 | import { useForm } from "react-hook-form"; 6 | import { usePathname, useRouter } from "next/navigation"; 7 | import { ChangeEvent, useState } from "react"; 8 | import { zodResolver } from "@hookform/resolvers/zod"; 9 | 10 | import { 11 | Form, 12 | FormControl, 13 | FormField, 14 | FormItem, 15 | FormLabel, 16 | FormMessage, 17 | } from "@/components/ui/form"; 18 | import { Input } from "@/components/ui/input"; 19 | import { Button } from "@/components/ui/button"; 20 | import { Textarea } from "@/components/ui/textarea"; 21 | 22 | import { useUploadThing } from "@/lib/uploadthing"; 23 | import { isBase64Image } from "@/lib/utils"; 24 | 25 | import { UserValidation } from "@/lib/validations/user"; 26 | import { updateUser } from "@/lib/actions/user.actions"; 27 | 28 | interface Props { 29 | user: { 30 | id: string; 31 | objectId: string; 32 | username: string; 33 | name: string; 34 | bio: string; 35 | image: string; 36 | }; 37 | btnTitle: string; 38 | } 39 | 40 | const AccountProfile = ({ user, btnTitle }: Props) => { 41 | const router = useRouter(); 42 | const pathname = usePathname(); 43 | const { startUpload } = useUploadThing("media"); 44 | 45 | const [files, setFiles] = useState([]); 46 | 47 | const form = useForm>({ 48 | resolver: zodResolver(UserValidation), 49 | defaultValues: { 50 | profile_photo: user?.image ? user.image : "", 51 | name: user?.name ? user.name : "", 52 | username: user?.username ? user.username : "", 53 | bio: user?.bio ? user.bio : "", 54 | }, 55 | }); 56 | 57 | const onSubmit = async (values: z.infer) => { 58 | const blob = values.profile_photo; 59 | 60 | const hasImageChanged = isBase64Image(blob); 61 | if (hasImageChanged) { 62 | const imgRes = await startUpload(files); 63 | 64 | if (imgRes && imgRes[0].fileUrl) { 65 | values.profile_photo = imgRes[0].fileUrl; 66 | } 67 | } 68 | 69 | await updateUser({ 70 | name: values.name, 71 | path: pathname, 72 | username: values.username, 73 | userId: user.id, 74 | bio: values.bio, 75 | image: values.profile_photo, 76 | }); 77 | 78 | if (pathname === "/profile/edit") { 79 | router.back(); 80 | } else { 81 | router.push("/"); 82 | } 83 | }; 84 | 85 | const handleImage = ( 86 | e: ChangeEvent, 87 | fieldChange: (value: string) => void 88 | ) => { 89 | e.preventDefault(); 90 | 91 | const fileReader = new FileReader(); 92 | 93 | if (e.target.files && e.target.files.length > 0) { 94 | const file = e.target.files[0]; 95 | setFiles(Array.from(e.target.files)); 96 | 97 | if (!file.type.includes("image")) return; 98 | 99 | fileReader.onload = async (event) => { 100 | const imageDataUrl = event.target?.result?.toString() || ""; 101 | fieldChange(imageDataUrl); 102 | }; 103 | 104 | fileReader.readAsDataURL(file); 105 | } 106 | }; 107 | 108 | return ( 109 |
110 | 114 | ( 118 | 119 | 120 | {field.value ? ( 121 | profile_icon 129 | ) : ( 130 | profile_icon 137 | )} 138 | 139 | 140 | handleImage(e, field.onChange)} 146 | /> 147 | 148 | 149 | )} 150 | /> 151 | 152 | ( 156 | 157 | 158 | Name 159 | 160 | 161 | 166 | 167 | 168 | 169 | )} 170 | /> 171 | 172 | ( 176 | 177 | 178 | Username 179 | 180 | 181 | 186 | 187 | 188 | 189 | )} 190 | /> 191 | 192 | ( 196 | 197 | 198 | Bio 199 | 200 | 201 |