├── .eslintrc.json
├── .gitignore
├── 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
│ ├── layout.tsx
│ ├── page.tsx
│ ├── profile
│ │ ├── [id]
│ │ │ └── page.tsx
│ │ └── edit
│ │ │ └── page.tsx
│ ├── search
│ │ └── page.tsx
│ └── thread
│ │ └── [id]
│ │ └── page.tsx
├── api
│ ├── uploadthing
│ │ ├── core.ts
│ │ └── route.ts
│ └── webhook
│ │ └── clerk
│ │ └── route.ts
├── favicon.ico
└── globals.css
├── components.json
├── components
├── 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
│ ├── menubar.tsx
│ ├── select.tsx
│ ├── tabs.tsx
│ ├── textarea.tsx
│ ├── toast.tsx
│ ├── toaster.tsx
│ └── use-toast.ts
├── 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.svg
│ ├── edit.svg
│ ├── heart-filled.svg
│ ├── heart-gray.svg
│ ├── heart.svg
│ ├── home.svg
│ ├── logout.svg
│ ├── members.svg
│ ├── more.svg
│ ├── profile.svg
│ ├── reply.svg
│ ├── repost.svg
│ ├── request.svg
│ ├── search-gray.svg
│ ├── search.svg
│ ├── share.svg
│ ├── tag.svg
│ └── user.svg
├── logo.svg
├── next.svg
└── vercel.svg
├── tailwind.config.js
└── tsconfig.json
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": [
3 | "next/core-web-vitals",
4 | "next",
5 | "standard",
6 | "plugin:tailwindcss/recommended",
7 | "prettier"
8 | ]
9 | }
10 |
--------------------------------------------------------------------------------
/.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 |
37 | # vscode
38 | .vscode
39 |
40 | # env
41 | .env
42 |
--------------------------------------------------------------------------------
/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 | const inter = Inter({ subsets: ["latin"] });
10 |
11 | export const metadata: Metadata = {
12 | title: "Auth",
13 | description: "Generated by create next app",
14 | };
15 |
16 | export default function RootLayout({
17 | children,
18 | }: {
19 | children: React.ReactNode;
20 | }) {
21 | return (
22 |
27 |
28 | {children}
29 |
30 |
31 | );
32 | }
33 |
--------------------------------------------------------------------------------
/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; // to avoid typescript warnings
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 ? userInfo?.username : user.username,
18 | name: userInfo ? userInfo?.name : user.firstName ?? "",
19 | bio: userInfo ? userInfo?.bio : "",
20 | image: userInfo ? userInfo?.image : user.imageUrl,
21 | };
22 |
23 | return (
24 |
25 | Onboarding
26 |
27 | Complete your profile now, to use Threds.
28 |
29 |
30 |
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 |
8 | async function Page() {
9 | const user = await currentUser();
10 | if (!user) return null;
11 |
12 | const userInfo = await fetchUser(user.id);
13 | if (!userInfo?.onboarded) redirect("/onboarding");
14 |
15 | const activity = await getActivity(userInfo._id);
16 |
17 | return (
18 | <>
19 |
Activity
20 |
21 |
22 | {activity.length > 0 ? (
23 | <>
24 | {activity.map((activity) => (
25 |
26 |
27 |
34 |
35 |
36 | {activity.author.name}
37 | {" "}
38 | replied to your thread
39 |
40 |
41 |
42 | ))}
43 | >
44 | ) : (
45 | No activity yet
46 | )}
47 |
48 | >
49 | );
50 | }
51 |
52 | export default Page;
53 |
--------------------------------------------------------------------------------
/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 |
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 | const result = await fetchCommunities({
23 | searchString: searchParams.q,
24 | pageNumber: searchParams?.page ? +searchParams.page : 1,
25 | pageSize: 25,
26 | });
27 |
28 | return (
29 | <>
30 | Communities
31 |
32 |
33 |
34 |
35 |
36 |
37 | {result.communities.length === 0 ? (
38 | No Result
39 | ) : (
40 | <>
41 | {result.communities.map((community) => (
42 |
51 | ))}
52 | >
53 | )}
54 |
55 |
56 |
61 | >
62 | );
63 | }
64 |
65 | export default Page;
66 |
--------------------------------------------------------------------------------
/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)/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 |
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 } 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 | return (
27 | <>
28 | Home
29 |
30 |
31 | {result.posts.length === 0 ? (
32 | No threads found
33 | ) : (
34 | <>
35 | {result.posts.map((post) => (
36 |
47 | ))}
48 | >
49 | )}
50 |
51 |
52 |
57 | >
58 | );
59 | }
60 |
61 | export default Home;
62 |
--------------------------------------------------------------------------------
/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 { fetchUser } from "@/lib/actions/user.actions";
12 |
13 | async function Page({ params }: { params: { id: string } }) {
14 | const user = await currentUser();
15 | if (!user) return null;
16 |
17 | const userInfo = await fetchUser(params.id);
18 | if (!userInfo?.onboarded) redirect("/onboarding");
19 |
20 | return (
21 |
22 |
30 |
31 |
32 |
33 |
34 | {profileTabs.map((tab) => (
35 |
36 |
43 | {tab.label}
44 |
45 | {tab.label === "Threads" && (
46 |
47 | {userInfo.threads.length}
48 |
49 | )}
50 |
51 | ))}
52 |
53 | {profileTabs.map((tab) => (
54 |
59 | {/* @ts-ignore */}
60 |
65 |
66 | ))}
67 |
68 |
69 |
70 | );
71 | }
72 | export default Page;
73 |
--------------------------------------------------------------------------------
/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 |
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 | searchString: 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 Result
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 { fetchThreadById } from "@/lib/actions/thread.actions";
9 |
10 | export const revalidate = 0;
11 |
12 | async function page({ params }: { params: { id: string } }) {
13 | if (!params.id) return null;
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 thread = await fetchThreadById(params.id);
22 |
23 | return (
24 |
25 |
26 |
36 |
37 |
38 |
39 |
44 |
45 |
46 |
47 | {thread.children.map((childItem: any) => (
48 |
60 | ))}
61 |
62 |
63 | );
64 | }
65 |
66 | export default page;
67 |
--------------------------------------------------------------------------------
/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 | };
209 |
--------------------------------------------------------------------------------
/app/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/adrianhajdin/threads/72da0d632377518985cb5ae279d84d0733520d37/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 | .searchbar {
54 | @apply flex gap-1 rounded-lg bg-dark-3 px-4 py-2;
55 | }
56 |
57 | .searchbar_input {
58 | @apply border-none bg-dark-3 text-base-regular text-light-4 outline-none !important;
59 | }
60 |
61 | .topbar {
62 | @apply fixed top-0 z-30 flex w-full items-center justify-between bg-dark-2 px-6 py-3;
63 | }
64 |
65 | .bottombar {
66 | @apply fixed bottom-0 z-10 w-full rounded-t-3xl bg-glassmorphism p-4 backdrop-blur-lg xs:px-7 md:hidden;
67 | }
68 |
69 | .bottombar_container {
70 | @apply flex items-center justify-between gap-3 xs:gap-5;
71 | }
72 |
73 | .bottombar_link {
74 | @apply relative flex flex-col items-center gap-2 rounded-lg p-2 sm:flex-1 sm:px-2 sm:py-2.5;
75 | }
76 |
77 | .leftsidebar {
78 | @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;
79 | }
80 |
81 | .leftsidebar_link {
82 | @apply relative flex justify-start gap-4 rounded-lg p-4;
83 | }
84 |
85 | .pagination {
86 | @apply mt-10 flex w-full items-center justify-center gap-5;
87 | }
88 |
89 | .rightsidebar {
90 | @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;
91 | }
92 | }
93 |
94 | @layer utilities {
95 | .css-invert {
96 | @apply invert-[50%] brightness-200;
97 | }
98 |
99 | .custom-scrollbar::-webkit-scrollbar {
100 | width: 3px;
101 | height: 3px;
102 | border-radius: 2px;
103 | }
104 |
105 | .custom-scrollbar::-webkit-scrollbar-track {
106 | background: #09090a;
107 | }
108 |
109 | .custom-scrollbar::-webkit-scrollbar-thumb {
110 | background: #5c5c7b;
111 | border-radius: 50px;
112 | }
113 |
114 | .custom-scrollbar::-webkit-scrollbar-thumb:hover {
115 | background: #7878a3;
116 | }
117 | }
118 |
119 | /* Clerk Responsive fix */
120 | .cl-organizationSwitcherTrigger .cl-userPreview .cl-userPreviewTextContainer {
121 | @apply max-sm:hidden;
122 | }
123 |
124 | .cl-organizationSwitcherTrigger
125 | .cl-organizationPreview
126 | .cl-organizationPreviewTextContainer {
127 | @apply max-sm:hidden;
128 | }
129 |
130 | /* Shadcn Component Styles */
131 |
132 | /* Tab */
133 | .tab {
134 | @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;
135 | }
136 |
137 | .no-focus {
138 | @apply focus-visible:ring-0 focus-visible:ring-transparent focus-visible:ring-offset-0 !important;
139 | }
140 |
141 | /* Account Profile */
142 | .account-form_image-label {
143 | @apply flex h-24 w-24 items-center justify-center rounded-full bg-dark-4 !important;
144 | }
145 |
146 | .account-form_image-input {
147 | @apply cursor-pointer border-none bg-transparent outline-none file:text-blue !important;
148 | }
149 |
150 | .account-form_input {
151 | @apply border border-dark-4 bg-dark-3 text-light-1 !important;
152 | }
153 |
154 | /* Comment Form */
155 | .comment-form {
156 | @apply mt-10 flex items-center gap-4 border-y border-y-dark-4 py-5 max-xs:flex-col !important;
157 | }
158 |
159 | .comment-form_btn {
160 | @apply rounded-3xl bg-primary-500 px-8 py-2 !text-small-regular text-light-1 max-xs:w-full !important;
161 | }
162 |
--------------------------------------------------------------------------------
/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/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 |
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 |
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 |
7 | interface Props {
8 | id: string;
9 | currentUserId: string;
10 | parentId: string | null;
11 | content: string;
12 | author: {
13 | name: string;
14 | image: string;
15 | id: string;
16 | };
17 | community: {
18 | id: string;
19 | name: string;
20 | image: string;
21 | } | null;
22 | createdAt: string;
23 | comments: {
24 | author: {
25 | image: string;
26 | };
27 | }[];
28 | isComment?: boolean;
29 | }
30 |
31 | function ThreadCard({
32 | id,
33 | currentUserId,
34 | parentId,
35 | content,
36 | author,
37 | community,
38 | createdAt,
39 | comments,
40 | isComment,
41 | }: Props) {
42 | return (
43 |
48 |
49 |
50 |
51 |
52 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 | {author.name}
67 |
68 |
69 |
70 |
{content}
71 |
72 |
73 |
74 |
81 |
82 |
89 |
90 |
97 |
104 |
105 |
106 | {isComment && comments.length > 0 && (
107 |
108 |
109 | {comments.length} repl{comments.length > 1 ? "ies" : "y"}
110 |
111 |
112 | )}
113 |
114 |
115 |
116 |
117 |
124 |
125 |
126 | {!isComment && comments.length > 0 && (
127 |
128 | {comments.slice(0, 2).map((comment, index) => (
129 |
137 | ))}
138 |
139 |
140 |
141 | {comments.length} repl{comments.length > 1 ? "ies" : "y"}
142 |
143 |
144 |
145 | )}
146 |
147 | {!isComment && community && (
148 |
152 |
153 | {formatDateString(createdAt)}
154 | {community && ` - ${community.name} Community`}
155 |
156 |
157 |
164 |
165 | )}
166 |
167 | );
168 | }
169 |
170 | export default ThreadCard;
171 |
--------------------------------------------------------------------------------
/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 | const isCommunity = personType === "Community";
20 |
21 | return (
22 |
23 |
24 |
25 |
31 |
32 |
33 |
34 |
{name}
35 |
@{username}
36 |
37 |
38 |
39 |
51 |
52 | );
53 | }
54 |
55 | export default UserCard;
56 |
--------------------------------------------------------------------------------
/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 |
216 |
217 | );
218 | };
219 |
220 | export default AccountProfile;
221 |
--------------------------------------------------------------------------------
/components/forms/Comment.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { z } from "zod";
4 | import Image from "next/image";
5 | import { useForm } from "react-hook-form";
6 | import { usePathname } from "next/navigation";
7 | import { zodResolver } from "@hookform/resolvers/zod";
8 |
9 | import {
10 | Form,
11 | FormControl,
12 | FormField,
13 | FormItem,
14 | FormLabel,
15 | } from "@/components/ui/form";
16 |
17 | import { Input } from "../ui/input";
18 | import { Button } from "../ui/button";
19 |
20 | import { CommentValidation } from "@/lib/validations/thread";
21 | import { addCommentToThread } from "@/lib/actions/thread.actions";
22 |
23 | interface Props {
24 | threadId: string;
25 | currentUserImg: string;
26 | currentUserId: string;
27 | }
28 |
29 | function Comment({ threadId, currentUserImg, currentUserId }: Props) {
30 | const pathname = usePathname();
31 |
32 | const form = useForm>({
33 | resolver: zodResolver(CommentValidation),
34 | defaultValues: {
35 | thread: "",
36 | },
37 | });
38 |
39 | const onSubmit = async (values: z.infer) => {
40 | await addCommentToThread(
41 | threadId,
42 | values.thread,
43 | JSON.parse(currentUserId),
44 | pathname
45 | );
46 |
47 | form.reset();
48 | };
49 |
50 | return (
51 |
83 |
84 | );
85 | }
86 |
87 | export default Comment;
88 |
--------------------------------------------------------------------------------
/components/forms/DeleteThread.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import Image from "next/image";
4 | import { usePathname, useRouter } from "next/navigation";
5 |
6 | import { deleteThread } from "@/lib/actions/thread.actions";
7 |
8 | interface Props {
9 | threadId: string;
10 | currentUserId: string;
11 | authorId: string;
12 | parentId: string | null;
13 | isComment?: boolean;
14 | }
15 |
16 | function DeleteThread({
17 | threadId,
18 | currentUserId,
19 | authorId,
20 | parentId,
21 | isComment,
22 | }: Props) {
23 | const pathname = usePathname();
24 | const router = useRouter();
25 |
26 | if (currentUserId !== authorId || pathname === "/") return null;
27 |
28 | return (
29 | {
36 | await deleteThread(JSON.parse(threadId), pathname);
37 | if (!parentId || !isComment) {
38 | router.push("/");
39 | }
40 | }}
41 | />
42 | );
43 | }
44 |
45 | export default DeleteThread;
46 |
--------------------------------------------------------------------------------
/components/forms/PostThread.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as z from "zod";
4 | import { useForm } from "react-hook-form";
5 | import { useOrganization } from "@clerk/nextjs";
6 | import { zodResolver } from "@hookform/resolvers/zod";
7 | import { usePathname, useRouter } from "next/navigation";
8 |
9 | import {
10 | Form,
11 | FormControl,
12 | FormField,
13 | FormItem,
14 | FormLabel,
15 | FormMessage,
16 | } from "@/components/ui/form";
17 | import { Button } from "@/components/ui/button";
18 | import { Textarea } from "@/components/ui/textarea";
19 |
20 | import { ThreadValidation } from "@/lib/validations/thread";
21 | import { createThread } from "@/lib/actions/thread.actions";
22 |
23 | interface Props {
24 | userId: string;
25 | }
26 |
27 | function PostThread({ userId }: Props) {
28 | const router = useRouter();
29 | const pathname = usePathname();
30 |
31 | const { organization } = useOrganization();
32 |
33 | const form = useForm>({
34 | resolver: zodResolver(ThreadValidation),
35 | defaultValues: {
36 | thread: "",
37 | accountId: userId,
38 | },
39 | });
40 |
41 | const onSubmit = async (values: z.infer) => {
42 | await createThread({
43 | text: values.thread,
44 | author: userId,
45 | communityId: organization ? organization.id : null,
46 | path: pathname,
47 | });
48 |
49 | router.push("/");
50 | };
51 |
52 | return (
53 |
78 |
79 | );
80 | }
81 |
82 | export default PostThread;
83 |
--------------------------------------------------------------------------------
/components/shared/Bottombar.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import Image from "next/image";
4 | import Link from "next/link";
5 | import { usePathname } from "next/navigation";
6 |
7 | import { sidebarLinks } from "@/constants";
8 |
9 | function Bottombar() {
10 | const pathname = usePathname();
11 |
12 | return (
13 |
14 |
15 | {sidebarLinks.map((link) => {
16 | const isActive =
17 | (pathname.includes(link.route) && link.route.length > 1) ||
18 | pathname === link.route;
19 |
20 | return (
21 |
26 |
33 |
34 |
35 | {link.label.split(/\s+/)[0]}
36 |
37 |
38 | );
39 | })}
40 |
41 |
42 | );
43 | }
44 |
45 | export default Bottombar;
46 |
--------------------------------------------------------------------------------
/components/shared/LeftSidebar.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import Image from "next/image";
4 | import Link from "next/link";
5 | import { usePathname, useRouter } from "next/navigation";
6 | import { SignOutButton, SignedIn, useAuth } from "@clerk/nextjs";
7 |
8 | import { sidebarLinks } from "@/constants";
9 |
10 | const LeftSidebar = () => {
11 | const router = useRouter();
12 | const pathname = usePathname();
13 |
14 | const { userId } = useAuth();
15 |
16 | return (
17 |
18 |
19 | {sidebarLinks.map((link) => {
20 | const isActive =
21 | (pathname.includes(link.route) && link.route.length > 1) ||
22 | pathname === link.route;
23 |
24 | if (link.route === "/profile") link.route = `${link.route}/${userId}`;
25 |
26 | return (
27 |
32 |
38 |
39 |
{link.label}
40 |
41 | );
42 | })}
43 |
44 |
45 |
46 |
47 | router.push("/sign-in")}>
48 |
49 |
55 |
56 |
Logout
57 |
58 |
59 |
60 |
61 |
62 | );
63 | };
64 |
65 | export default LeftSidebar;
66 |
--------------------------------------------------------------------------------
/components/shared/Pagination.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useRouter } from "next/navigation";
4 |
5 | import { Button } from "../ui/button";
6 |
7 | interface Props {
8 | pageNumber: number;
9 | isNext: boolean;
10 | path: string;
11 | }
12 |
13 | function Pagination({ pageNumber, isNext, path }: Props) {
14 | const router = useRouter();
15 |
16 | const handleNavigation = (type: string) => {
17 | let nextPageNumber = pageNumber;
18 |
19 | if (type === "prev") {
20 | nextPageNumber = Math.max(1, pageNumber - 1);
21 | } else if (type === "next") {
22 | nextPageNumber = pageNumber + 1;
23 | }
24 |
25 | if (nextPageNumber > 1) {
26 | router.push(`/${path}?page=${nextPageNumber}`);
27 | } else {
28 | router.push(`/${path}`);
29 | }
30 | };
31 |
32 | if (!isNext && pageNumber === 1) return null;
33 |
34 | return (
35 |
36 |
43 |
{pageNumber}
44 |
51 |
52 | );
53 | }
54 |
55 | export default Pagination;
56 |
--------------------------------------------------------------------------------
/components/shared/ProfileHeader.tsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link";
2 | import Image from "next/image";
3 |
4 | interface Props {
5 | accountId: string;
6 | authUserId: string;
7 | name: string;
8 | username: string;
9 | imgUrl: string;
10 | bio: string;
11 | type?: string;
12 | }
13 |
14 | function ProfileHeader({
15 | accountId,
16 | authUserId,
17 | name,
18 | username,
19 | imgUrl,
20 | bio,
21 | type,
22 | }: Props) {
23 | return (
24 |
25 |
26 |
27 |
28 |
34 |
35 |
36 |
37 |
38 | {name}
39 |
40 |
@{username}
41 |
42 |
43 | {accountId === authUserId && type !== "Community" && (
44 |
45 |
55 |
56 | )}
57 |
58 |
59 |
{bio}
60 |
61 |
62 |
63 | );
64 | }
65 |
66 | export default ProfileHeader;
67 |
--------------------------------------------------------------------------------
/components/shared/RightSidebar.tsx:
--------------------------------------------------------------------------------
1 | import { currentUser } from "@clerk/nextjs";
2 |
3 | import UserCard from "../cards/UserCard";
4 |
5 | import { fetchCommunities } from "@/lib/actions/community.actions";
6 | import { fetchUsers } from "@/lib/actions/user.actions";
7 |
8 | async function RightSidebar() {
9 | const user = await currentUser();
10 | if (!user) return null;
11 |
12 | const similarMinds = await fetchUsers({
13 | userId: user.id,
14 | pageSize: 4,
15 | });
16 |
17 | const suggestedCOmmunities = await fetchCommunities({ pageSize: 4 });
18 |
19 | return (
20 |
21 |
22 |
23 | Suggested Communities
24 |
25 |
26 |
27 | {suggestedCOmmunities.communities.length > 0 ? (
28 | <>
29 | {suggestedCOmmunities.communities.map((community) => (
30 |
38 | ))}
39 | >
40 | ) : (
41 |
42 | No communities yet
43 |
44 | )}
45 |
46 |
47 |
48 |
49 |
Similar Minds
50 |
51 | {similarMinds.users.length > 0 ? (
52 | <>
53 | {similarMinds.users.map((person) => (
54 |
62 | ))}
63 | >
64 | ) : (
65 |
No users yet
66 | )}
67 |
68 |
69 |
70 | );
71 | }
72 |
73 | export default RightSidebar;
74 |
--------------------------------------------------------------------------------
/components/shared/Searchbar.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import Image from "next/image";
4 | import { useRouter } from "next/navigation";
5 | import { useEffect, useState } from "react";
6 |
7 | import { Input } from "../ui/input";
8 |
9 | interface Props {
10 | routeType: string;
11 | }
12 |
13 | function Searchbar({ routeType }: Props) {
14 | const router = useRouter();
15 | const [search, setSearch] = useState("");
16 |
17 | // query after 0.3s of no input
18 | useEffect(() => {
19 | const delayDebounceFn = setTimeout(() => {
20 | if (search) {
21 | router.push(`/${routeType}?q=` + search);
22 | } else {
23 | router.push(`/${routeType}`);
24 | }
25 | }, 300);
26 |
27 | return () => clearTimeout(delayDebounceFn);
28 | }, [search, routeType]);
29 |
30 | return (
31 |
32 |
39 | setSearch(e.target.value)}
43 | placeholder={`${
44 | routeType !== "/search" ? "Search communities" : "Search creators"
45 | }`}
46 | className='no-focus searchbar_input'
47 | />
48 |
49 | );
50 | }
51 |
52 | export default Searchbar;
53 |
--------------------------------------------------------------------------------
/components/shared/ThreadsTab.tsx:
--------------------------------------------------------------------------------
1 | import { redirect } from "next/navigation";
2 |
3 | import { fetchCommunityPosts } from "@/lib/actions/community.actions";
4 | import { fetchUserPosts } from "@/lib/actions/user.actions";
5 |
6 | import ThreadCard from "../cards/ThreadCard";
7 |
8 | interface Result {
9 | name: string;
10 | image: string;
11 | id: string;
12 | threads: {
13 | _id: string;
14 | text: string;
15 | parentId: string | null;
16 | author: {
17 | name: string;
18 | image: string;
19 | id: string;
20 | };
21 | community: {
22 | id: string;
23 | name: string;
24 | image: string;
25 | } | null;
26 | createdAt: string;
27 | children: {
28 | author: {
29 | image: string;
30 | };
31 | }[];
32 | }[];
33 | }
34 |
35 | interface Props {
36 | currentUserId: string;
37 | accountId: string;
38 | accountType: string;
39 | }
40 |
41 | async function ThreadsTab({ currentUserId, accountId, accountType }: Props) {
42 | let result: Result;
43 |
44 | if (accountType === "Community") {
45 | result = await fetchCommunityPosts(accountId);
46 | } else {
47 | result = await fetchUserPosts(accountId);
48 | }
49 |
50 | if (!result) {
51 | redirect("/");
52 | }
53 |
54 | return (
55 |
56 | {result.threads.map((thread) => (
57 |
80 | ))}
81 |
82 | );
83 | }
84 |
85 | export default ThreadsTab;
86 |
--------------------------------------------------------------------------------
/components/shared/Topbar.tsx:
--------------------------------------------------------------------------------
1 | import { OrganizationSwitcher, SignedIn, SignOutButton } from "@clerk/nextjs";
2 | import { dark } from "@clerk/themes";
3 | import Image from "next/image";
4 | import Link from "next/link";
5 |
6 | function Topbar() {
7 | return (
8 |
40 | );
41 | }
42 |
43 | export default Topbar;
44 |
--------------------------------------------------------------------------------
/components/ui/button.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { Slot } from "@radix-ui/react-slot"
3 | import { cva, type VariantProps } from "class-variance-authority"
4 |
5 | import { cn } from "@/lib/utils"
6 |
7 | const buttonVariants = cva(
8 | "inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-white transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-slate-400 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 dark:ring-offset-slate-950 dark:focus-visible:ring-slate-800",
9 | {
10 | variants: {
11 | variant: {
12 | default: "bg-slate-900 text-slate-50 hover:bg-slate-900/90 dark:bg-slate-50 dark:text-slate-900 dark:hover:bg-slate-50/90",
13 | destructive:
14 | "bg-red-500 text-slate-50 hover:bg-red-500/90 dark:bg-red-900 dark:text-red-50 dark:hover:bg-red-900/90",
15 | outline:
16 | "border border-slate-200 bg-white hover:bg-slate-100 hover:text-slate-900 dark:border-slate-800 dark:bg-slate-950 dark:hover:bg-slate-800 dark:hover:text-slate-50",
17 | secondary:
18 | "bg-slate-100 text-slate-900 hover:bg-slate-100/80 dark:bg-slate-800 dark:text-slate-50 dark:hover:bg-slate-800/80",
19 | ghost: "hover:bg-slate-100 hover:text-slate-900 dark:hover:bg-slate-800 dark:hover:text-slate-50",
20 | link: "text-slate-900 underline-offset-4 hover:underline dark:text-slate-50",
21 | },
22 | size: {
23 | default: "h-10 px-4 py-2",
24 | sm: "h-9 rounded-md px-3",
25 | lg: "h-11 rounded-md px-8",
26 | icon: "h-10 w-10",
27 | },
28 | },
29 | defaultVariants: {
30 | variant: "default",
31 | size: "default",
32 | },
33 | }
34 | )
35 |
36 | export interface ButtonProps
37 | extends React.ButtonHTMLAttributes,
38 | VariantProps {
39 | asChild?: boolean
40 | }
41 |
42 | const Button = React.forwardRef(
43 | ({ className, variant, size, asChild = false, ...props }, ref) => {
44 | const Comp = asChild ? Slot : "button"
45 | return (
46 |
51 | )
52 | }
53 | )
54 | Button.displayName = "Button"
55 |
56 | export { Button, buttonVariants }
57 |
--------------------------------------------------------------------------------
/components/ui/form.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as LabelPrimitive from "@radix-ui/react-label"
3 | import { Slot } from "@radix-ui/react-slot"
4 | import {
5 | Controller,
6 | ControllerProps,
7 | FieldPath,
8 | FieldValues,
9 | FormProvider,
10 | useFormContext,
11 | } from "react-hook-form"
12 |
13 | import { cn } from "@/lib/utils"
14 | import { Label } from "@/components/ui/label"
15 |
16 | const Form = FormProvider
17 |
18 | type FormFieldContextValue<
19 | TFieldValues extends FieldValues = FieldValues,
20 | TName extends FieldPath = FieldPath
21 | > = {
22 | name: TName
23 | }
24 |
25 | const FormFieldContext = React.createContext(
26 | {} as FormFieldContextValue
27 | )
28 |
29 | const FormField = <
30 | TFieldValues extends FieldValues = FieldValues,
31 | TName extends FieldPath = FieldPath
32 | >({
33 | ...props
34 | }: ControllerProps) => {
35 | return (
36 |
37 |
38 |
39 | )
40 | }
41 |
42 | const useFormField = () => {
43 | const fieldContext = React.useContext(FormFieldContext)
44 | const itemContext = React.useContext(FormItemContext)
45 | const { getFieldState, formState } = useFormContext()
46 |
47 | const fieldState = getFieldState(fieldContext.name, formState)
48 |
49 | if (!fieldContext) {
50 | throw new Error("useFormField should be used within ")
51 | }
52 |
53 | const { id } = itemContext
54 |
55 | return {
56 | id,
57 | name: fieldContext.name,
58 | formItemId: `${id}-form-item`,
59 | formDescriptionId: `${id}-form-item-description`,
60 | formMessageId: `${id}-form-item-message`,
61 | ...fieldState,
62 | }
63 | }
64 |
65 | type FormItemContextValue = {
66 | id: string
67 | }
68 |
69 | const FormItemContext = React.createContext(
70 | {} as FormItemContextValue
71 | )
72 |
73 | const FormItem = React.forwardRef<
74 | HTMLDivElement,
75 | React.HTMLAttributes
76 | >(({ className, ...props }, ref) => {
77 | const id = React.useId()
78 |
79 | return (
80 |
81 |
82 |
83 | )
84 | })
85 | FormItem.displayName = "FormItem"
86 |
87 | const FormLabel = React.forwardRef<
88 | React.ElementRef,
89 | React.ComponentPropsWithoutRef
90 | >(({ className, ...props }, ref) => {
91 | const { error, formItemId } = useFormField()
92 |
93 | return (
94 |
100 | )
101 | })
102 | FormLabel.displayName = "FormLabel"
103 |
104 | const FormControl = React.forwardRef<
105 | React.ElementRef,
106 | React.ComponentPropsWithoutRef
107 | >(({ ...props }, ref) => {
108 | const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
109 |
110 | return (
111 |
122 | )
123 | })
124 | FormControl.displayName = "FormControl"
125 |
126 | const FormDescription = React.forwardRef<
127 | HTMLParagraphElement,
128 | React.HTMLAttributes
129 | >(({ className, ...props }, ref) => {
130 | const { formDescriptionId } = useFormField()
131 |
132 | return (
133 |
139 | )
140 | })
141 | FormDescription.displayName = "FormDescription"
142 |
143 | const FormMessage = React.forwardRef<
144 | HTMLParagraphElement,
145 | React.HTMLAttributes
146 | >(({ className, children, ...props }, ref) => {
147 | const { error, formMessageId } = useFormField()
148 | const body = error ? String(error?.message) : children
149 |
150 | if (!body) {
151 | return null
152 | }
153 |
154 | return (
155 |
161 | {body}
162 |
163 | )
164 | })
165 | FormMessage.displayName = "FormMessage"
166 |
167 | export {
168 | useFormField,
169 | Form,
170 | FormItem,
171 | FormLabel,
172 | FormControl,
173 | FormDescription,
174 | FormMessage,
175 | FormField,
176 | }
177 |
--------------------------------------------------------------------------------
/components/ui/input.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 |
3 | import { cn } from "@/lib/utils";
4 |
5 | export interface InputProps
6 | extends React.InputHTMLAttributes {}
7 |
8 | const Input = React.forwardRef(
9 | ({ className, type, ...props }, ref) => {
10 | return (
11 |
20 | );
21 | }
22 | );
23 | Input.displayName = "Input";
24 |
25 | export { Input };
26 |
--------------------------------------------------------------------------------
/components/ui/label.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as LabelPrimitive from "@radix-ui/react-label"
5 | import { cva, type VariantProps } from "class-variance-authority"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const labelVariants = cva(
10 | "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
11 | )
12 |
13 | const Label = React.forwardRef<
14 | React.ElementRef,
15 | React.ComponentPropsWithoutRef &
16 | VariantProps
17 | >(({ className, ...props }, ref) => (
18 |
23 | ))
24 | Label.displayName = LabelPrimitive.Root.displayName
25 |
26 | export { Label }
27 |
--------------------------------------------------------------------------------
/components/ui/menubar.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as MenubarPrimitive from "@radix-ui/react-menubar"
5 | import { Check, ChevronRight, Circle } from "lucide-react"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const MenubarMenu = MenubarPrimitive.Menu
10 |
11 | const MenubarGroup = MenubarPrimitive.Group
12 |
13 | const MenubarPortal = MenubarPrimitive.Portal
14 |
15 | const MenubarSub = MenubarPrimitive.Sub
16 |
17 | const MenubarRadioGroup = MenubarPrimitive.RadioGroup
18 |
19 | const Menubar = React.forwardRef<
20 | React.ElementRef,
21 | React.ComponentPropsWithoutRef
22 | >(({ className, ...props }, ref) => (
23 |
31 | ))
32 | Menubar.displayName = MenubarPrimitive.Root.displayName
33 |
34 | const MenubarTrigger = React.forwardRef<
35 | React.ElementRef,
36 | React.ComponentPropsWithoutRef
37 | >(({ className, ...props }, ref) => (
38 |
46 | ))
47 | MenubarTrigger.displayName = MenubarPrimitive.Trigger.displayName
48 |
49 | const MenubarSubTrigger = React.forwardRef<
50 | React.ElementRef,
51 | React.ComponentPropsWithoutRef & {
52 | inset?: boolean
53 | }
54 | >(({ className, inset, children, ...props }, ref) => (
55 |
64 | {children}
65 |
66 |
67 | ))
68 | MenubarSubTrigger.displayName = MenubarPrimitive.SubTrigger.displayName
69 |
70 | const MenubarSubContent = React.forwardRef<
71 | React.ElementRef,
72 | React.ComponentPropsWithoutRef
73 | >(({ className, ...props }, ref) => (
74 |
82 | ))
83 | MenubarSubContent.displayName = MenubarPrimitive.SubContent.displayName
84 |
85 | const MenubarContent = React.forwardRef<
86 | React.ElementRef,
87 | React.ComponentPropsWithoutRef
88 | >(
89 | (
90 | { className, align = "start", alignOffset = -4, sideOffset = 8, ...props },
91 | ref
92 | ) => (
93 |
94 |
105 |
106 | )
107 | )
108 | MenubarContent.displayName = MenubarPrimitive.Content.displayName
109 |
110 | const MenubarItem = React.forwardRef<
111 | React.ElementRef,
112 | React.ComponentPropsWithoutRef & {
113 | inset?: boolean
114 | }
115 | >(({ className, inset, ...props }, ref) => (
116 |
125 | ))
126 | MenubarItem.displayName = MenubarPrimitive.Item.displayName
127 |
128 | const MenubarCheckboxItem = React.forwardRef<
129 | React.ElementRef,
130 | React.ComponentPropsWithoutRef
131 | >(({ className, children, checked, ...props }, ref) => (
132 |
141 |
142 |
143 |
144 |
145 |
146 | {children}
147 |
148 | ))
149 | MenubarCheckboxItem.displayName = MenubarPrimitive.CheckboxItem.displayName
150 |
151 | const MenubarRadioItem = React.forwardRef<
152 | React.ElementRef,
153 | React.ComponentPropsWithoutRef
154 | >(({ className, children, ...props }, ref) => (
155 |
163 |
164 |
165 |
166 |
167 |
168 | {children}
169 |
170 | ))
171 | MenubarRadioItem.displayName = MenubarPrimitive.RadioItem.displayName
172 |
173 | const MenubarLabel = React.forwardRef<
174 | React.ElementRef,
175 | React.ComponentPropsWithoutRef & {
176 | inset?: boolean
177 | }
178 | >(({ className, inset, ...props }, ref) => (
179 |
188 | ))
189 | MenubarLabel.displayName = MenubarPrimitive.Label.displayName
190 |
191 | const MenubarSeparator = React.forwardRef<
192 | React.ElementRef,
193 | React.ComponentPropsWithoutRef
194 | >(({ className, ...props }, ref) => (
195 |
200 | ))
201 | MenubarSeparator.displayName = MenubarPrimitive.Separator.displayName
202 |
203 | const MenubarShortcut = ({
204 | className,
205 | ...props
206 | }: React.HTMLAttributes) => {
207 | return (
208 |
215 | )
216 | }
217 | MenubarShortcut.displayname = "MenubarShortcut"
218 |
219 | export {
220 | Menubar,
221 | MenubarMenu,
222 | MenubarTrigger,
223 | MenubarContent,
224 | MenubarItem,
225 | MenubarSeparator,
226 | MenubarLabel,
227 | MenubarCheckboxItem,
228 | MenubarRadioGroup,
229 | MenubarRadioItem,
230 | MenubarPortal,
231 | MenubarSubContent,
232 | MenubarSubTrigger,
233 | MenubarGroup,
234 | MenubarSub,
235 | MenubarShortcut,
236 | }
237 |
--------------------------------------------------------------------------------
/components/ui/select.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as SelectPrimitive from "@radix-ui/react-select"
5 | import { Check, ChevronDown } from "lucide-react"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const Select = SelectPrimitive.Root
10 |
11 | const SelectGroup = SelectPrimitive.Group
12 |
13 | const SelectValue = SelectPrimitive.Value
14 |
15 | const SelectTrigger = React.forwardRef<
16 | React.ElementRef,
17 | React.ComponentPropsWithoutRef
18 | >(({ className, children, ...props }, ref) => (
19 |
27 | {children}
28 |
29 |
30 |
31 |
32 | ))
33 | SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
34 |
35 | const SelectContent = React.forwardRef<
36 | React.ElementRef,
37 | React.ComponentPropsWithoutRef
38 | >(({ className, children, position = "popper", ...props }, ref) => (
39 |
40 |
51 |
58 | {children}
59 |
60 |
61 |
62 | ))
63 | SelectContent.displayName = SelectPrimitive.Content.displayName
64 |
65 | const SelectLabel = React.forwardRef<
66 | React.ElementRef,
67 | React.ComponentPropsWithoutRef
68 | >(({ className, ...props }, ref) => (
69 |
74 | ))
75 | SelectLabel.displayName = SelectPrimitive.Label.displayName
76 |
77 | const SelectItem = React.forwardRef<
78 | React.ElementRef,
79 | React.ComponentPropsWithoutRef
80 | >(({ className, children, ...props }, ref) => (
81 |
89 |
90 |
91 |
92 |
93 |
94 |
95 | {children}
96 |
97 | ))
98 | SelectItem.displayName = SelectPrimitive.Item.displayName
99 |
100 | const SelectSeparator = React.forwardRef<
101 | React.ElementRef,
102 | React.ComponentPropsWithoutRef
103 | >(({ className, ...props }, ref) => (
104 |
109 | ))
110 | SelectSeparator.displayName = SelectPrimitive.Separator.displayName
111 |
112 | export {
113 | Select,
114 | SelectGroup,
115 | SelectValue,
116 | SelectTrigger,
117 | SelectContent,
118 | SelectLabel,
119 | SelectItem,
120 | SelectSeparator,
121 | }
122 |
--------------------------------------------------------------------------------
/components/ui/tabs.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import * as TabsPrimitive from "@radix-ui/react-tabs";
5 |
6 | import { cn } from "@/lib/utils";
7 |
8 | const Tabs = TabsPrimitive.Root;
9 |
10 | const TabsList = React.forwardRef<
11 | React.ElementRef,
12 | React.ComponentPropsWithoutRef
13 | >(({ className, ...props }, ref) => (
14 |
22 | ));
23 | TabsList.displayName = TabsPrimitive.List.displayName;
24 |
25 | const TabsTrigger = React.forwardRef<
26 | React.ElementRef,
27 | React.ComponentPropsWithoutRef
28 | >(({ className, ...props }, ref) => (
29 |
37 | ));
38 | TabsTrigger.displayName = TabsPrimitive.Trigger.displayName;
39 |
40 | const TabsContent = React.forwardRef<
41 | React.ElementRef,
42 | React.ComponentPropsWithoutRef
43 | >(({ className, ...props }, ref) => (
44 |
52 | ));
53 | TabsContent.displayName = TabsPrimitive.Content.displayName;
54 |
55 | export { Tabs, TabsList, TabsTrigger, TabsContent };
56 |
--------------------------------------------------------------------------------
/components/ui/textarea.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | export interface TextareaProps
6 | extends React.TextareaHTMLAttributes {}
7 |
8 | const Textarea = React.forwardRef(
9 | ({ className, ...props }, ref) => {
10 | return (
11 |
19 | )
20 | }
21 | )
22 | Textarea.displayName = "Textarea"
23 |
24 | export { Textarea }
25 |
--------------------------------------------------------------------------------
/components/ui/toast.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as ToastPrimitives from "@radix-ui/react-toast"
3 | import { cva, type VariantProps } from "class-variance-authority"
4 | import { X } from "lucide-react"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const ToastProvider = ToastPrimitives.Provider
9 |
10 | const ToastViewport = React.forwardRef<
11 | React.ElementRef,
12 | React.ComponentPropsWithoutRef
13 | >(({ className, ...props }, ref) => (
14 |
22 | ))
23 | ToastViewport.displayName = ToastPrimitives.Viewport.displayName
24 |
25 | const toastVariants = cva(
26 | "group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border border-slate-200 p-6 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full dark:border-slate-800",
27 | {
28 | variants: {
29 | variant: {
30 | default: "border bg-white dark:bg-slate-950",
31 | destructive:
32 | "destructive group border-red-500 bg-red-500 text-slate-50 dark:border-red-900 dark:bg-red-900 dark:text-red-50",
33 | },
34 | },
35 | defaultVariants: {
36 | variant: "default",
37 | },
38 | }
39 | )
40 |
41 | const Toast = React.forwardRef<
42 | React.ElementRef,
43 | React.ComponentPropsWithoutRef &
44 | VariantProps
45 | >(({ className, variant, ...props }, ref) => {
46 | return (
47 |
52 | )
53 | })
54 | Toast.displayName = ToastPrimitives.Root.displayName
55 |
56 | const ToastAction = React.forwardRef<
57 | React.ElementRef,
58 | React.ComponentPropsWithoutRef
59 | >(({ className, ...props }, ref) => (
60 |
68 | ))
69 | ToastAction.displayName = ToastPrimitives.Action.displayName
70 |
71 | const ToastClose = React.forwardRef<
72 | React.ElementRef,
73 | React.ComponentPropsWithoutRef
74 | >(({ className, ...props }, ref) => (
75 |
84 |
85 |
86 | ))
87 | ToastClose.displayName = ToastPrimitives.Close.displayName
88 |
89 | const ToastTitle = React.forwardRef<
90 | React.ElementRef,
91 | React.ComponentPropsWithoutRef
92 | >(({ className, ...props }, ref) => (
93 |
98 | ))
99 | ToastTitle.displayName = ToastPrimitives.Title.displayName
100 |
101 | const ToastDescription = React.forwardRef<
102 | React.ElementRef,
103 | React.ComponentPropsWithoutRef
104 | >(({ className, ...props }, ref) => (
105 |
110 | ))
111 | ToastDescription.displayName = ToastPrimitives.Description.displayName
112 |
113 | type ToastProps = React.ComponentPropsWithoutRef
114 |
115 | type ToastActionElement = React.ReactElement
116 |
117 | export {
118 | type ToastProps,
119 | type ToastActionElement,
120 | ToastProvider,
121 | ToastViewport,
122 | Toast,
123 | ToastTitle,
124 | ToastDescription,
125 | ToastClose,
126 | ToastAction,
127 | }
128 |
--------------------------------------------------------------------------------
/components/ui/toaster.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import {
4 | Toast,
5 | ToastClose,
6 | ToastDescription,
7 | ToastProvider,
8 | ToastTitle,
9 | ToastViewport,
10 | } from "@/components/ui/toast"
11 | import { useToast } from "@/components/ui/use-toast"
12 |
13 | export function Toaster() {
14 | const { toasts } = useToast()
15 |
16 | return (
17 |
18 | {toasts.map(function ({ id, title, description, action, ...props }) {
19 | return (
20 |
21 |
22 | {title && {title}}
23 | {description && (
24 | {description}
25 | )}
26 |
27 | {action}
28 |
29 |
30 | )
31 | })}
32 |
33 |
34 | )
35 | }
36 |
--------------------------------------------------------------------------------
/components/ui/use-toast.ts:
--------------------------------------------------------------------------------
1 | // Inspired by react-hot-toast library
2 | import * as React from "react"
3 |
4 | import type {
5 | ToastActionElement,
6 | ToastProps,
7 | } from "@/components/ui/toast"
8 |
9 | const TOAST_LIMIT = 1
10 | const TOAST_REMOVE_DELAY = 1000000
11 |
12 | type ToasterToast = ToastProps & {
13 | id: string
14 | title?: React.ReactNode
15 | description?: React.ReactNode
16 | action?: ToastActionElement
17 | }
18 |
19 | const actionTypes = {
20 | ADD_TOAST: "ADD_TOAST",
21 | UPDATE_TOAST: "UPDATE_TOAST",
22 | DISMISS_TOAST: "DISMISS_TOAST",
23 | REMOVE_TOAST: "REMOVE_TOAST",
24 | } as const
25 |
26 | let count = 0
27 |
28 | function genId() {
29 | count = (count + 1) % Number.MAX_VALUE
30 | return count.toString()
31 | }
32 |
33 | type ActionType = typeof actionTypes
34 |
35 | type Action =
36 | | {
37 | type: ActionType["ADD_TOAST"]
38 | toast: ToasterToast
39 | }
40 | | {
41 | type: ActionType["UPDATE_TOAST"]
42 | toast: Partial
43 | }
44 | | {
45 | type: ActionType["DISMISS_TOAST"]
46 | toastId?: ToasterToast["id"]
47 | }
48 | | {
49 | type: ActionType["REMOVE_TOAST"]
50 | toastId?: ToasterToast["id"]
51 | }
52 |
53 | interface State {
54 | toasts: ToasterToast[]
55 | }
56 |
57 | const toastTimeouts = new Map>()
58 |
59 | const addToRemoveQueue = (toastId: string) => {
60 | if (toastTimeouts.has(toastId)) {
61 | return
62 | }
63 |
64 | const timeout = setTimeout(() => {
65 | toastTimeouts.delete(toastId)
66 | dispatch({
67 | type: "REMOVE_TOAST",
68 | toastId: toastId,
69 | })
70 | }, TOAST_REMOVE_DELAY)
71 |
72 | toastTimeouts.set(toastId, timeout)
73 | }
74 |
75 | export const reducer = (state: State, action: Action): State => {
76 | switch (action.type) {
77 | case "ADD_TOAST":
78 | return {
79 | ...state,
80 | toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
81 | }
82 |
83 | case "UPDATE_TOAST":
84 | return {
85 | ...state,
86 | toasts: state.toasts.map((t) =>
87 | t.id === action.toast.id ? { ...t, ...action.toast } : t
88 | ),
89 | }
90 |
91 | case "DISMISS_TOAST": {
92 | const { toastId } = action
93 |
94 | // ! Side effects ! - This could be extracted into a dismissToast() action,
95 | // but I'll keep it here for simplicity
96 | if (toastId) {
97 | addToRemoveQueue(toastId)
98 | } else {
99 | state.toasts.forEach((toast) => {
100 | addToRemoveQueue(toast.id)
101 | })
102 | }
103 |
104 | return {
105 | ...state,
106 | toasts: state.toasts.map((t) =>
107 | t.id === toastId || toastId === undefined
108 | ? {
109 | ...t,
110 | open: false,
111 | }
112 | : t
113 | ),
114 | }
115 | }
116 | case "REMOVE_TOAST":
117 | if (action.toastId === undefined) {
118 | return {
119 | ...state,
120 | toasts: [],
121 | }
122 | }
123 | return {
124 | ...state,
125 | toasts: state.toasts.filter((t) => t.id !== action.toastId),
126 | }
127 | }
128 | }
129 |
130 | const listeners: Array<(state: State) => void> = []
131 |
132 | let memoryState: State = { toasts: [] }
133 |
134 | function dispatch(action: Action) {
135 | memoryState = reducer(memoryState, action)
136 | listeners.forEach((listener) => {
137 | listener(memoryState)
138 | })
139 | }
140 |
141 | type Toast = Omit
142 |
143 | function toast({ ...props }: Toast) {
144 | const id = genId()
145 |
146 | const update = (props: ToasterToast) =>
147 | dispatch({
148 | type: "UPDATE_TOAST",
149 | toast: { ...props, id },
150 | })
151 | const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id })
152 |
153 | dispatch({
154 | type: "ADD_TOAST",
155 | toast: {
156 | ...props,
157 | id,
158 | open: true,
159 | onOpenChange: (open) => {
160 | if (!open) dismiss()
161 | },
162 | },
163 | })
164 |
165 | return {
166 | id: id,
167 | dismiss,
168 | update,
169 | }
170 | }
171 |
172 | function useToast() {
173 | const [state, setState] = React.useState(memoryState)
174 |
175 | React.useEffect(() => {
176 | listeners.push(setState)
177 | return () => {
178 | const index = listeners.indexOf(setState)
179 | if (index > -1) {
180 | listeners.splice(index, 1)
181 | }
182 | }
183 | }, [state])
184 |
185 | return {
186 | ...state,
187 | toast,
188 | dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
189 | }
190 | }
191 |
192 | export { useToast, toast }
193 |
--------------------------------------------------------------------------------
/constants/index.js:
--------------------------------------------------------------------------------
1 | export const sidebarLinks = [
2 | {
3 | imgURL: "/assets/home.svg",
4 | route: "/",
5 | label: "Home",
6 | },
7 | {
8 | imgURL: "/assets/search.svg",
9 | route: "/search",
10 | label: "Search",
11 | },
12 | {
13 | imgURL: "/assets/heart.svg",
14 | route: "/activity",
15 | label: "Activity",
16 | },
17 | {
18 | imgURL: "/assets/create.svg",
19 | route: "/create-thread",
20 | label: "Create Thread",
21 | },
22 | {
23 | imgURL: "/assets/community.svg",
24 | route: "/communities",
25 | label: "Communities",
26 | },
27 | {
28 | imgURL: "/assets/user.svg",
29 | route: "/profile",
30 | label: "Profile",
31 | },
32 | ];
33 |
34 | export const profileTabs = [
35 | { value: "threads", label: "Threads", icon: "/assets/reply.svg" },
36 | { value: "replies", label: "Replies", icon: "/assets/members.svg" },
37 | { value: "tagged", label: "Tagged", icon: "/assets/tag.svg" },
38 | ];
39 |
40 | export const communityTabs = [
41 | { value: "threads", label: "Threads", icon: "/assets/reply.svg" },
42 | { value: "members", label: "Members", icon: "/assets/members.svg" },
43 | { value: "requests", label: "Requests", icon: "/assets/request.svg" },
44 | ];
45 |
--------------------------------------------------------------------------------
/lib/actions/community.actions.ts:
--------------------------------------------------------------------------------
1 | "use server";
2 |
3 | import { FilterQuery, SortOrder } from "mongoose";
4 |
5 | import Community from "../models/community.model";
6 | import Thread from "../models/thread.model";
7 | import User from "../models/user.model";
8 |
9 | import { connectToDB } from "../mongoose";
10 |
11 | export async function createCommunity(
12 | id: string,
13 | name: string,
14 | username: string,
15 | image: string,
16 | bio: string,
17 | createdById: string // Change the parameter name to reflect it's an id
18 | ) {
19 | try {
20 | connectToDB();
21 |
22 | // Find the user with the provided unique id
23 | const user = await User.findOne({ id: createdById });
24 |
25 | if (!user) {
26 | throw new Error("User not found"); // Handle the case if the user with the id is not found
27 | }
28 |
29 | const newCommunity = new Community({
30 | id,
31 | name,
32 | username,
33 | image,
34 | bio,
35 | createdBy: user._id, // Use the mongoose ID of the user
36 | });
37 |
38 | const createdCommunity = await newCommunity.save();
39 |
40 | // Update User model
41 | user.communities.push(createdCommunity._id);
42 | await user.save();
43 |
44 | return createdCommunity;
45 | } catch (error) {
46 | // Handle any errors
47 | console.error("Error creating community:", error);
48 | throw error;
49 | }
50 | }
51 |
52 | export async function fetchCommunityDetails(id: string) {
53 | try {
54 | connectToDB();
55 |
56 | const communityDetails = await Community.findOne({ id }).populate([
57 | "createdBy",
58 | {
59 | path: "members",
60 | model: User,
61 | select: "name username image _id id",
62 | },
63 | ]);
64 |
65 | return communityDetails;
66 | } catch (error) {
67 | // Handle any errors
68 | console.error("Error fetching community details:", error);
69 | throw error;
70 | }
71 | }
72 |
73 | export async function fetchCommunityPosts(id: string) {
74 | try {
75 | connectToDB();
76 |
77 | const communityPosts = await Community.findById(id).populate({
78 | path: "threads",
79 | model: Thread,
80 | populate: [
81 | {
82 | path: "author",
83 | model: User,
84 | select: "name image id", // Select the "name" and "_id" fields from the "User" model
85 | },
86 | {
87 | path: "children",
88 | model: Thread,
89 | populate: {
90 | path: "author",
91 | model: User,
92 | select: "image _id", // Select the "name" and "_id" fields from the "User" model
93 | },
94 | },
95 | ],
96 | });
97 |
98 | return communityPosts;
99 | } catch (error) {
100 | // Handle any errors
101 | console.error("Error fetching community posts:", error);
102 | throw error;
103 | }
104 | }
105 |
106 | export async function fetchCommunities({
107 | searchString = "",
108 | pageNumber = 1,
109 | pageSize = 20,
110 | sortBy = "desc",
111 | }: {
112 | searchString?: string;
113 | pageNumber?: number;
114 | pageSize?: number;
115 | sortBy?: SortOrder;
116 | }) {
117 | try {
118 | connectToDB();
119 |
120 | // Calculate the number of communities to skip based on the page number and page size.
121 | const skipAmount = (pageNumber - 1) * pageSize;
122 |
123 | // Create a case-insensitive regular expression for the provided search string.
124 | const regex = new RegExp(searchString, "i");
125 |
126 | // Create an initial query object to filter communities.
127 | const query: FilterQuery = {};
128 |
129 | // If the search string is not empty, add the $or operator to match either username or name fields.
130 | if (searchString.trim() !== "") {
131 | query.$or = [
132 | { username: { $regex: regex } },
133 | { name: { $regex: regex } },
134 | ];
135 | }
136 |
137 | // Define the sort options for the fetched communities based on createdAt field and provided sort order.
138 | const sortOptions = { createdAt: sortBy };
139 |
140 | // Create a query to fetch the communities based on the search and sort criteria.
141 | const communitiesQuery = Community.find(query)
142 | .sort(sortOptions)
143 | .skip(skipAmount)
144 | .limit(pageSize)
145 | .populate("members");
146 |
147 | // Count the total number of communities that match the search criteria (without pagination).
148 | const totalCommunitiesCount = await Community.countDocuments(query);
149 |
150 | const communities = await communitiesQuery.exec();
151 |
152 | // Check if there are more communities beyond the current page.
153 | const isNext = totalCommunitiesCount > skipAmount + communities.length;
154 |
155 | return { communities, isNext };
156 | } catch (error) {
157 | console.error("Error fetching communities:", error);
158 | throw error;
159 | }
160 | }
161 |
162 | export async function addMemberToCommunity(
163 | communityId: string,
164 | memberId: string
165 | ) {
166 | try {
167 | connectToDB();
168 |
169 | // Find the community by its unique id
170 | const community = await Community.findOne({ id: communityId });
171 |
172 | if (!community) {
173 | throw new Error("Community not found");
174 | }
175 |
176 | // Find the user by their unique id
177 | const user = await User.findOne({ id: memberId });
178 |
179 | if (!user) {
180 | throw new Error("User not found");
181 | }
182 |
183 | // Check if the user is already a member of the community
184 | if (community.members.includes(user._id)) {
185 | throw new Error("User is already a member of the community");
186 | }
187 |
188 | // Add the user's _id to the members array in the community
189 | community.members.push(user._id);
190 | await community.save();
191 |
192 | // Add the community's _id to the communities array in the user
193 | user.communities.push(community._id);
194 | await user.save();
195 |
196 | return community;
197 | } catch (error) {
198 | // Handle any errors
199 | console.error("Error adding member to community:", error);
200 | throw error;
201 | }
202 | }
203 |
204 | export async function removeUserFromCommunity(
205 | userId: string,
206 | communityId: string
207 | ) {
208 | try {
209 | connectToDB();
210 |
211 | const userIdObject = await User.findOne({ id: userId }, { _id: 1 });
212 | const communityIdObject = await Community.findOne(
213 | { id: communityId },
214 | { _id: 1 }
215 | );
216 |
217 | if (!userIdObject) {
218 | throw new Error("User not found");
219 | }
220 |
221 | if (!communityIdObject) {
222 | throw new Error("Community not found");
223 | }
224 |
225 | // Remove the user's _id from the members array in the community
226 | await Community.updateOne(
227 | { _id: communityIdObject._id },
228 | { $pull: { members: userIdObject._id } }
229 | );
230 |
231 | // Remove the community's _id from the communities array in the user
232 | await User.updateOne(
233 | { _id: userIdObject._id },
234 | { $pull: { communities: communityIdObject._id } }
235 | );
236 |
237 | return { success: true };
238 | } catch (error) {
239 | // Handle any errors
240 | console.error("Error removing user from community:", error);
241 | throw error;
242 | }
243 | }
244 |
245 | export async function updateCommunityInfo(
246 | communityId: string,
247 | name: string,
248 | username: string,
249 | image: string
250 | ) {
251 | try {
252 | connectToDB();
253 |
254 | // Find the community by its _id and update the information
255 | const updatedCommunity = await Community.findOneAndUpdate(
256 | { id: communityId },
257 | { name, username, image }
258 | );
259 |
260 | if (!updatedCommunity) {
261 | throw new Error("Community not found");
262 | }
263 |
264 | return updatedCommunity;
265 | } catch (error) {
266 | // Handle any errors
267 | console.error("Error updating community information:", error);
268 | throw error;
269 | }
270 | }
271 |
272 | export async function deleteCommunity(communityId: string) {
273 | try {
274 | connectToDB();
275 |
276 | // Find the community by its ID and delete it
277 | const deletedCommunity = await Community.findOneAndDelete({
278 | id: communityId,
279 | });
280 |
281 | if (!deletedCommunity) {
282 | throw new Error("Community not found");
283 | }
284 |
285 | // Delete all threads associated with the community
286 | await Thread.deleteMany({ community: communityId });
287 |
288 | // Find all users who are part of the community
289 | const communityUsers = await User.find({ communities: communityId });
290 |
291 | // Remove the community from the 'communities' array for each user
292 | const updateUserPromises = communityUsers.map((user) => {
293 | user.communities.pull(communityId);
294 | return user.save();
295 | });
296 |
297 | await Promise.all(updateUserPromises);
298 |
299 | return deletedCommunity;
300 | } catch (error) {
301 | console.error("Error deleting community: ", error);
302 | throw error;
303 | }
304 | }
305 |
--------------------------------------------------------------------------------
/lib/actions/thread.actions.ts:
--------------------------------------------------------------------------------
1 | "use server";
2 |
3 | import { revalidatePath } from "next/cache";
4 |
5 | import { connectToDB } from "../mongoose";
6 |
7 | import User from "../models/user.model";
8 | import Thread from "../models/thread.model";
9 | import Community from "../models/community.model";
10 |
11 | export async function fetchPosts(pageNumber = 1, pageSize = 20) {
12 | connectToDB();
13 |
14 | // Calculate the number of posts to skip based on the page number and page size.
15 | const skipAmount = (pageNumber - 1) * pageSize;
16 |
17 | // Create a query to fetch the posts that have no parent (top-level threads) (a thread that is not a comment/reply).
18 | const postsQuery = Thread.find({ parentId: { $in: [null, undefined] } })
19 | .sort({ createdAt: "desc" })
20 | .skip(skipAmount)
21 | .limit(pageSize)
22 | .populate({
23 | path: "author",
24 | model: User,
25 | })
26 | .populate({
27 | path: "community",
28 | model: Community,
29 | })
30 | .populate({
31 | path: "children", // Populate the children field
32 | populate: {
33 | path: "author", // Populate the author field within children
34 | model: User,
35 | select: "_id name parentId image", // Select only _id and username fields of the author
36 | },
37 | });
38 |
39 | // Count the total number of top-level posts (threads) i.e., threads that are not comments.
40 | const totalPostsCount = await Thread.countDocuments({
41 | parentId: { $in: [null, undefined] },
42 | }); // Get the total count of posts
43 |
44 | const posts = await postsQuery.exec();
45 |
46 | const isNext = totalPostsCount > skipAmount + posts.length;
47 |
48 | return { posts, isNext };
49 | }
50 |
51 | interface Params {
52 | text: string,
53 | author: string,
54 | communityId: string | null,
55 | path: string,
56 | }
57 |
58 | export async function createThread({ text, author, communityId, path }: Params
59 | ) {
60 | try {
61 | connectToDB();
62 |
63 | const communityIdObject = await Community.findOne(
64 | { id: communityId },
65 | { _id: 1 }
66 | );
67 |
68 | const createdThread = await Thread.create({
69 | text,
70 | author,
71 | community: communityIdObject, // Assign communityId if provided, or leave it null for personal account
72 | });
73 |
74 | // Update User model
75 | await User.findByIdAndUpdate(author, {
76 | $push: { threads: createdThread._id },
77 | });
78 |
79 | if (communityIdObject) {
80 | // Update Community model
81 | await Community.findByIdAndUpdate(communityIdObject, {
82 | $push: { threads: createdThread._id },
83 | });
84 | }
85 |
86 | revalidatePath(path);
87 | } catch (error: any) {
88 | throw new Error(`Failed to create thread: ${error.message}`);
89 | }
90 | }
91 |
92 | async function fetchAllChildThreads(threadId: string): Promise {
93 | const childThreads = await Thread.find({ parentId: threadId });
94 |
95 | const descendantThreads = [];
96 | for (const childThread of childThreads) {
97 | const descendants = await fetchAllChildThreads(childThread._id);
98 | descendantThreads.push(childThread, ...descendants);
99 | }
100 |
101 | return descendantThreads;
102 | }
103 |
104 | export async function deleteThread(id: string, path: string): Promise {
105 | try {
106 | connectToDB();
107 |
108 | // Find the thread to be deleted (the main thread)
109 | const mainThread = await Thread.findById(id).populate("author community");
110 |
111 | if (!mainThread) {
112 | throw new Error("Thread not found");
113 | }
114 |
115 | // Fetch all child threads and their descendants recursively
116 | const descendantThreads = await fetchAllChildThreads(id);
117 |
118 | // Get all descendant thread IDs including the main thread ID and child thread IDs
119 | const descendantThreadIds = [
120 | id,
121 | ...descendantThreads.map((thread) => thread._id),
122 | ];
123 |
124 | // Extract the authorIds and communityIds to update User and Community models respectively
125 | const uniqueAuthorIds = new Set(
126 | [
127 | ...descendantThreads.map((thread) => thread.author?._id?.toString()), // Use optional chaining to handle possible undefined values
128 | mainThread.author?._id?.toString(),
129 | ].filter((id) => id !== undefined)
130 | );
131 |
132 | const uniqueCommunityIds = new Set(
133 | [
134 | ...descendantThreads.map((thread) => thread.community?._id?.toString()), // Use optional chaining to handle possible undefined values
135 | mainThread.community?._id?.toString(),
136 | ].filter((id) => id !== undefined)
137 | );
138 |
139 | // Recursively delete child threads and their descendants
140 | await Thread.deleteMany({ _id: { $in: descendantThreadIds } });
141 |
142 | // Update User model
143 | await User.updateMany(
144 | { _id: { $in: Array.from(uniqueAuthorIds) } },
145 | { $pull: { threads: { $in: descendantThreadIds } } }
146 | );
147 |
148 | // Update Community model
149 | await Community.updateMany(
150 | { _id: { $in: Array.from(uniqueCommunityIds) } },
151 | { $pull: { threads: { $in: descendantThreadIds } } }
152 | );
153 |
154 | revalidatePath(path);
155 | } catch (error: any) {
156 | throw new Error(`Failed to delete thread: ${error.message}`);
157 | }
158 | }
159 |
160 | export async function fetchThreadById(threadId: string) {
161 | connectToDB();
162 |
163 | try {
164 | const thread = await Thread.findById(threadId)
165 | .populate({
166 | path: "author",
167 | model: User,
168 | select: "_id id name image",
169 | }) // Populate the author field with _id and username
170 | .populate({
171 | path: "community",
172 | model: Community,
173 | select: "_id id name image",
174 | }) // Populate the community field with _id and name
175 | .populate({
176 | path: "children", // Populate the children field
177 | populate: [
178 | {
179 | path: "author", // Populate the author field within children
180 | model: User,
181 | select: "_id id name parentId image", // Select only _id and username fields of the author
182 | },
183 | {
184 | path: "children", // Populate the children field within children
185 | model: Thread, // The model of the nested children (assuming it's the same "Thread" model)
186 | populate: {
187 | path: "author", // Populate the author field within nested children
188 | model: User,
189 | select: "_id id name parentId image", // Select only _id and username fields of the author
190 | },
191 | },
192 | ],
193 | })
194 | .exec();
195 |
196 | return thread;
197 | } catch (err) {
198 | console.error("Error while fetching thread:", err);
199 | throw new Error("Unable to fetch thread");
200 | }
201 | }
202 |
203 | export async function addCommentToThread(
204 | threadId: string,
205 | commentText: string,
206 | userId: string,
207 | path: string
208 | ) {
209 | connectToDB();
210 |
211 | try {
212 | // Find the original thread by its ID
213 | const originalThread = await Thread.findById(threadId);
214 |
215 | if (!originalThread) {
216 | throw new Error("Thread not found");
217 | }
218 |
219 | // Create the new comment thread
220 | const commentThread = new Thread({
221 | text: commentText,
222 | author: userId,
223 | parentId: threadId, // Set the parentId to the original thread's ID
224 | });
225 |
226 | // Save the comment thread to the database
227 | const savedCommentThread = await commentThread.save();
228 |
229 | // Add the comment thread's ID to the original thread's children array
230 | originalThread.children.push(savedCommentThread._id);
231 |
232 | // Save the updated original thread to the database
233 | await originalThread.save();
234 |
235 | revalidatePath(path);
236 | } catch (err) {
237 | console.error("Error while adding comment:", err);
238 | throw new Error("Unable to add comment");
239 | }
240 | }
241 |
--------------------------------------------------------------------------------
/lib/actions/user.actions.ts:
--------------------------------------------------------------------------------
1 | "use server";
2 |
3 | import { FilterQuery, SortOrder } from "mongoose";
4 | import { revalidatePath } from "next/cache";
5 |
6 | import Community from "../models/community.model";
7 | import Thread from "../models/thread.model";
8 | import User from "../models/user.model";
9 |
10 | import { connectToDB } from "../mongoose";
11 |
12 | export async function fetchUser(userId: string) {
13 | try {
14 | connectToDB();
15 |
16 | return await User.findOne({ id: userId }).populate({
17 | path: "communities",
18 | model: Community,
19 | });
20 | } catch (error: any) {
21 | throw new Error(`Failed to fetch user: ${error.message}`);
22 | }
23 | }
24 |
25 | interface Params {
26 | userId: string;
27 | username: string;
28 | name: string;
29 | bio: string;
30 | image: string;
31 | path: string;
32 | }
33 |
34 | export async function updateUser({
35 | userId,
36 | bio,
37 | name,
38 | path,
39 | username,
40 | image,
41 | }: Params): Promise {
42 | try {
43 | connectToDB();
44 |
45 | await User.findOneAndUpdate(
46 | { id: userId },
47 | {
48 | username: username.toLowerCase(),
49 | name,
50 | bio,
51 | image,
52 | onboarded: true,
53 | },
54 | { upsert: true }
55 | );
56 |
57 | if (path === "/profile/edit") {
58 | revalidatePath(path);
59 | }
60 | } catch (error: any) {
61 | throw new Error(`Failed to create/update user: ${error.message}`);
62 | }
63 | }
64 |
65 | export async function fetchUserPosts(userId: string) {
66 | try {
67 | connectToDB();
68 |
69 | // Find all threads authored by the user with the given userId
70 | const threads = await User.findOne({ id: userId }).populate({
71 | path: "threads",
72 | model: Thread,
73 | populate: [
74 | {
75 | path: "community",
76 | model: Community,
77 | select: "name id image _id", // Select the "name" and "_id" fields from the "Community" model
78 | },
79 | {
80 | path: "children",
81 | model: Thread,
82 | populate: {
83 | path: "author",
84 | model: User,
85 | select: "name image id", // Select the "name" and "_id" fields from the "User" model
86 | },
87 | },
88 | ],
89 | });
90 | return threads;
91 | } catch (error) {
92 | console.error("Error fetching user threads:", error);
93 | throw error;
94 | }
95 | }
96 |
97 | // Almost similar to Thead (search + pagination) and Community (search + pagination)
98 | export async function fetchUsers({
99 | userId,
100 | searchString = "",
101 | pageNumber = 1,
102 | pageSize = 20,
103 | sortBy = "desc",
104 | }: {
105 | userId: string;
106 | searchString?: string;
107 | pageNumber?: number;
108 | pageSize?: number;
109 | sortBy?: SortOrder;
110 | }) {
111 | try {
112 | connectToDB();
113 |
114 | // Calculate the number of users to skip based on the page number and page size.
115 | const skipAmount = (pageNumber - 1) * pageSize;
116 |
117 | // Create a case-insensitive regular expression for the provided search string.
118 | const regex = new RegExp(searchString, "i");
119 |
120 | // Create an initial query object to filter users.
121 | const query: FilterQuery = {
122 | id: { $ne: userId }, // Exclude the current user from the results.
123 | };
124 |
125 | // If the search string is not empty, add the $or operator to match either username or name fields.
126 | if (searchString.trim() !== "") {
127 | query.$or = [
128 | { username: { $regex: regex } },
129 | { name: { $regex: regex } },
130 | ];
131 | }
132 |
133 | // Define the sort options for the fetched users based on createdAt field and provided sort order.
134 | const sortOptions = { createdAt: sortBy };
135 |
136 | const usersQuery = User.find(query)
137 | .sort(sortOptions)
138 | .skip(skipAmount)
139 | .limit(pageSize);
140 |
141 | // Count the total number of users that match the search criteria (without pagination).
142 | const totalUsersCount = await User.countDocuments(query);
143 |
144 | const users = await usersQuery.exec();
145 |
146 | // Check if there are more users beyond the current page.
147 | const isNext = totalUsersCount > skipAmount + users.length;
148 |
149 | return { users, isNext };
150 | } catch (error) {
151 | console.error("Error fetching users:", error);
152 | throw error;
153 | }
154 | }
155 |
156 | export async function getActivity(userId: string) {
157 | try {
158 | connectToDB();
159 |
160 | // Find all threads created by the user
161 | const userThreads = await Thread.find({ author: userId });
162 |
163 | // Collect all the child thread ids (replies) from the 'children' field of each user thread
164 | const childThreadIds = userThreads.reduce((acc, userThread) => {
165 | return acc.concat(userThread.children);
166 | }, []);
167 |
168 | // Find and return the child threads (replies) excluding the ones created by the same user
169 | const replies = await Thread.find({
170 | _id: { $in: childThreadIds },
171 | author: { $ne: userId }, // Exclude threads authored by the same user
172 | }).populate({
173 | path: "author",
174 | model: User,
175 | select: "name image _id",
176 | });
177 |
178 | return replies;
179 | } catch (error) {
180 | console.error("Error fetching replies: ", error);
181 | throw error;
182 | }
183 | }
184 |
--------------------------------------------------------------------------------
/lib/models/community.model.ts:
--------------------------------------------------------------------------------
1 | import mongoose from "mongoose";
2 |
3 | const communitySchema = new mongoose.Schema({
4 | id: {
5 | type: String,
6 | required: true,
7 | },
8 | username: {
9 | type: String,
10 | unique: true,
11 | required: true,
12 | },
13 | name: {
14 | type: String,
15 | required: true,
16 | },
17 | image: String,
18 | bio: String,
19 | createdBy: {
20 | type: mongoose.Schema.Types.ObjectId,
21 | ref: "User",
22 | },
23 | threads: [
24 | {
25 | type: mongoose.Schema.Types.ObjectId,
26 | ref: "Thread",
27 | },
28 | ],
29 | members: [
30 | {
31 | type: mongoose.Schema.Types.ObjectId,
32 | ref: "User",
33 | },
34 | ],
35 | });
36 |
37 | const Community =
38 | mongoose.models.Community || mongoose.model("Community", communitySchema);
39 |
40 | export default Community;
41 |
--------------------------------------------------------------------------------
/lib/models/thread.model.ts:
--------------------------------------------------------------------------------
1 | import mongoose from "mongoose";
2 |
3 | const threadSchema = new mongoose.Schema({
4 | text: {
5 | type: String,
6 | required: true,
7 | },
8 | author: {
9 | type: mongoose.Schema.Types.ObjectId,
10 | ref: "User",
11 | required: true,
12 | },
13 | community: {
14 | type: mongoose.Schema.Types.ObjectId,
15 | ref: "Community",
16 | },
17 | createdAt: {
18 | type: Date,
19 | default: Date.now,
20 | },
21 | parentId: {
22 | type: String,
23 | },
24 | children: [
25 | {
26 | type: mongoose.Schema.Types.ObjectId,
27 | ref: "Thread",
28 | },
29 | ],
30 | });
31 |
32 | const Thread = mongoose.models.Thread || mongoose.model("Thread", threadSchema);
33 |
34 | export default Thread;
35 |
--------------------------------------------------------------------------------
/lib/models/user.model.ts:
--------------------------------------------------------------------------------
1 | import mongoose from "mongoose";
2 |
3 | const userSchema = new mongoose.Schema({
4 | id: {
5 | type: String,
6 | required: true,
7 | },
8 | username: {
9 | type: String,
10 | unique: true,
11 | required: true,
12 | },
13 | name: {
14 | type: String,
15 | required: true,
16 | },
17 | image: String,
18 | bio: String,
19 | threads: [
20 | {
21 | type: mongoose.Schema.Types.ObjectId,
22 | ref: "Thread",
23 | },
24 | ],
25 | onboarded: {
26 | type: Boolean,
27 | default: false,
28 | },
29 | communities: [
30 | {
31 | type: mongoose.Schema.Types.ObjectId,
32 | ref: "Community",
33 | },
34 | ],
35 | });
36 |
37 | const User = mongoose.models.User || mongoose.model("User", userSchema);
38 |
39 | export default User;
40 |
--------------------------------------------------------------------------------
/lib/mongoose.ts:
--------------------------------------------------------------------------------
1 | import mongoose from "mongoose";
2 |
3 | let isConnected = false; // Variable to track the connection status
4 |
5 | export const connectToDB = async () => {
6 | // Set strict query mode for Mongoose to prevent unknown field queries.
7 | mongoose.set("strictQuery", true);
8 |
9 | if (!process.env.MONGODB_URL) return console.log("Missing MongoDB URL");
10 |
11 | // If the connection is already established, return without creating a new connection.
12 | if (isConnected) {
13 | console.log("MongoDB connection already established");
14 | return;
15 | }
16 |
17 | try {
18 | await mongoose.connect(process.env.MONGODB_URL);
19 |
20 | isConnected = true; // Set the connection status to true
21 | console.log("MongoDB connected");
22 | } catch (error) {
23 | console.log(error);
24 | }
25 | };
26 |
--------------------------------------------------------------------------------
/lib/uploadthing.ts:
--------------------------------------------------------------------------------
1 | // Resource: https://docs.uploadthing.com/api-reference/react#generatereacthelpers
2 | // Copy paste (be careful with imports)
3 |
4 | import { generateReactHelpers } from "@uploadthing/react/hooks";
5 |
6 | import type { OurFileRouter } from "@/app/api/uploadthing/core";
7 |
8 | export const { useUploadThing, uploadFiles } =
9 | generateReactHelpers();
10 |
--------------------------------------------------------------------------------
/lib/utils.ts:
--------------------------------------------------------------------------------
1 | import { type ClassValue, clsx } from "clsx";
2 | import { twMerge } from "tailwind-merge";
3 |
4 | // generated by shadcn
5 | export function cn(...inputs: ClassValue[]) {
6 | return twMerge(clsx(inputs));
7 | }
8 |
9 | // created by chatgpt
10 | export function isBase64Image(imageData: string) {
11 | const base64Regex = /^data:image\/(png|jpe?g|gif|webp);base64,/;
12 | return base64Regex.test(imageData);
13 | }
14 |
15 | // created by chatgpt
16 | export function formatDateString(dateString: string) {
17 | const options: Intl.DateTimeFormatOptions = {
18 | year: "numeric",
19 | month: "short",
20 | day: "numeric",
21 | };
22 |
23 | const date = new Date(dateString);
24 | const formattedDate = date.toLocaleDateString(undefined, options);
25 |
26 | const time = date.toLocaleTimeString([], {
27 | hour: "numeric",
28 | minute: "2-digit",
29 | });
30 |
31 | return `${time} - ${formattedDate}`;
32 | }
33 |
34 | // created by chatgpt
35 | export function formatThreadCount(count: number): string {
36 | if (count === 0) {
37 | return "No Threads";
38 | } else {
39 | const threadCount = count.toString().padStart(2, "0");
40 | const threadWord = count === 1 ? "Thread" : "Threads";
41 | return `${threadCount} ${threadWord}`;
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/lib/validations/thread.ts:
--------------------------------------------------------------------------------
1 | import * as z from "zod";
2 |
3 | export const ThreadValidation = z.object({
4 | thread: z.string().nonempty().min(3, { message: "Minimum 3 characters." }),
5 | accountId: z.string(),
6 | });
7 |
8 | export const CommentValidation = z.object({
9 | thread: z.string().nonempty().min(3, { message: "Minimum 3 characters." }),
10 | });
11 |
--------------------------------------------------------------------------------
/lib/validations/user.ts:
--------------------------------------------------------------------------------
1 | import * as z from "zod";
2 |
3 | export const UserValidation = z.object({
4 | profile_photo: z.string().url().nonempty(),
5 | name: z
6 | .string()
7 | .min(3, { message: "Minimum 3 characters." })
8 | .max(30, { message: "Maximum 30 caracters." }),
9 | username: z
10 | .string()
11 | .min(3, { message: "Minimum 3 characters." })
12 | .max(30, { message: "Maximum 30 caracters." }),
13 | bio: z
14 | .string()
15 | .min(3, { message: "Minimum 3 characters." })
16 | .max(1000, { message: "Maximum 1000 caracters." }),
17 | });
18 |
--------------------------------------------------------------------------------
/middleware.ts:
--------------------------------------------------------------------------------
1 | // Resource: https://clerk.com/docs/nextjs/middleware#auth-middleware
2 | // Copy the middleware code as it is from the above resource
3 |
4 | import { authMiddleware } from "@clerk/nextjs";
5 |
6 | export default authMiddleware({
7 | // An array of public routes that don't require authentication.
8 | publicRoutes: ["/api/webhook/clerk"],
9 |
10 | // An array of routes to be ignored by the authentication middleware.
11 | ignoredRoutes: ["/api/webhook/clerk"],
12 | });
13 |
14 | export const config = {
15 | matcher: ["/((?!.*\\..*|_next).*)", "/", "/(api|trpc)(.*)"],
16 | };
17 |
--------------------------------------------------------------------------------
/next.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {
3 | experimental: {
4 | serverActions: true,
5 | serverComponentsExternalPackages: ["mongoose"],
6 | },
7 | eslint: {
8 | // Warning: This allows production builds to successfully complete even if
9 | // your project has ESLint errors.
10 | ignoreDuringBuilds: true,
11 | },
12 | images: {
13 | remotePatterns: [
14 | {
15 | protocol: "https",
16 | hostname: "img.clerk.com",
17 | },
18 | {
19 | protocol: "https",
20 | hostname: "images.clerk.dev",
21 | },
22 | {
23 | protocol: "https",
24 | hostname: "uploadthing.com",
25 | },
26 | {
27 | protocol: "https",
28 | hostname: "placehold.co",
29 | },
30 | ],
31 | },
32 | };
33 |
34 | module.exports = nextConfig;
35 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "threads",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev",
7 | "build": "next build",
8 | "start": "next start",
9 | "lint": "next lint"
10 | },
11 | "dependencies": {
12 | "@clerk/nextjs": "^4.21.15",
13 | "@clerk/themes": "^1.7.5",
14 | "@hookform/resolvers": "^3.1.1",
15 | "@radix-ui/react-label": "^2.0.2",
16 | "@radix-ui/react-menubar": "^1.0.3",
17 | "@radix-ui/react-select": "^1.2.2",
18 | "@radix-ui/react-slot": "^1.0.2",
19 | "@radix-ui/react-tabs": "^1.0.4",
20 | "@radix-ui/react-toast": "^1.1.4",
21 | "@types/node": "20.4.2",
22 | "@types/react": "18.2.15",
23 | "@types/react-dom": "18.2.7",
24 | "@uploadthing/react": "^5.2.0",
25 | "autoprefixer": "10.4.14",
26 | "class-variance-authority": "^0.6.1",
27 | "clsx": "^1.2.1",
28 | "eslint": "8.45.0",
29 | "eslint-config-next": "13.4.10",
30 | "eslint-config-prettier": "^8.8.0",
31 | "eslint-config-standard": "^17.1.0",
32 | "eslint-plugin-tailwindcss": "^3.13.0",
33 | "lucide-react": "^0.260.0",
34 | "micro": "^10.0.1",
35 | "mongoose": "^7.3.4",
36 | "next": "13.4.10",
37 | "postcss": "8.4.26",
38 | "prettier": "^3.0.0",
39 | "react": "18.2.0",
40 | "react-dom": "18.2.0",
41 | "react-hook-form": "^7.45.1",
42 | "svix": "^1.7.0",
43 | "tailwind-merge": "^1.13.2",
44 | "tailwindcss": "3.3.3",
45 | "tailwindcss-animate": "^1.0.6",
46 | "typescript": "5.1.6",
47 | "uploadthing": "^5.2.0",
48 | "zod": "^3.21.4"
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/public/assets/community.svg:
--------------------------------------------------------------------------------
1 |
9 |
--------------------------------------------------------------------------------
/public/assets/create.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/public/assets/delete.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/assets/edit.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/assets/heart-filled.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/assets/heart-gray.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/assets/heart.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/public/assets/home.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/public/assets/logout.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/public/assets/members.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/assets/more.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/assets/profile.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/assets/reply.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/assets/repost.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/assets/request.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/assets/search-gray.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/assets/search.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/public/assets/share.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/assets/tag.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/assets/user.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/public/logo.svg:
--------------------------------------------------------------------------------
1 |
11 |
--------------------------------------------------------------------------------
/public/next.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/vercel.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | module.exports = {
3 | darkMode: ["class"],
4 | content: [
5 | "./pages/**/*.{ts,tsx}",
6 | "./components/**/*.{ts,tsx}",
7 | "./app/**/*.{ts,tsx}",
8 | "./src/**/*.{ts,tsx}",
9 | ],
10 | theme: {
11 | container: {
12 | center: true,
13 | padding: "2rem",
14 | screens: {
15 | "2xl": "1400px",
16 | },
17 | },
18 | fontSize: {
19 | "heading1-bold": [
20 | "36px",
21 | {
22 | lineHeight: "140%",
23 | fontWeight: "700",
24 | },
25 | ],
26 | "heading1-semibold": [
27 | "36px",
28 | {
29 | lineHeight: "140%",
30 | fontWeight: "600",
31 | },
32 | ],
33 | "heading2-bold": [
34 | "30px",
35 | {
36 | lineHeight: "140%",
37 | fontWeight: "700",
38 | },
39 | ],
40 | "heading2-semibold": [
41 | "30px",
42 | {
43 | lineHeight: "140%",
44 | fontWeight: "600",
45 | },
46 | ],
47 | "heading3-bold": [
48 | "24px",
49 | {
50 | lineHeight: "140%",
51 | fontWeight: "700",
52 | },
53 | ],
54 | "heading4-medium": [
55 | "20px",
56 | {
57 | lineHeight: "140%",
58 | fontWeight: "500",
59 | },
60 | ],
61 | "body-bold": [
62 | "18px",
63 | {
64 | lineHeight: "140%",
65 | fontWeight: "700",
66 | },
67 | ],
68 | "body-semibold": [
69 | "18px",
70 | {
71 | lineHeight: "140%",
72 | fontWeight: "600",
73 | },
74 | ],
75 | "body-medium": [
76 | "18px",
77 | {
78 | lineHeight: "140%",
79 | fontWeight: "500",
80 | },
81 | ],
82 | "body-normal": [
83 | "18px",
84 | {
85 | lineHeight: "140%",
86 | fontWeight: "400",
87 | },
88 | ],
89 | "body1-bold": [
90 | "18px",
91 | {
92 | lineHeight: "140%",
93 | fontWeight: "700",
94 | },
95 | ],
96 | "base-regular": [
97 | "16px",
98 | {
99 | lineHeight: "140%",
100 | fontWeight: "400",
101 | },
102 | ],
103 | "base-medium": [
104 | "16px",
105 | {
106 | lineHeight: "140%",
107 | fontWeight: "500",
108 | },
109 | ],
110 | "base-semibold": [
111 | "16px",
112 | {
113 | lineHeight: "140%",
114 | fontWeight: "600",
115 | },
116 | ],
117 | "base1-semibold": [
118 | "16px",
119 | {
120 | lineHeight: "140%",
121 | fontWeight: "600",
122 | },
123 | ],
124 | "small-regular": [
125 | "14px",
126 | {
127 | lineHeight: "140%",
128 | fontWeight: "400",
129 | },
130 | ],
131 | "small-medium": [
132 | "14px",
133 | {
134 | lineHeight: "140%",
135 | fontWeight: "500",
136 | },
137 | ],
138 | "small-semibold": [
139 | "14px",
140 | {
141 | lineHeight: "140%",
142 | fontWeight: "600",
143 | },
144 | ],
145 | "subtle-medium": [
146 | "12px",
147 | {
148 | lineHeight: "16px",
149 | fontWeight: "500",
150 | },
151 | ],
152 | "subtle-semibold": [
153 | "12px",
154 | {
155 | lineHeight: "16px",
156 | fontWeight: "600",
157 | },
158 | ],
159 | "tiny-medium": [
160 | "10px",
161 | {
162 | lineHeight: "140%",
163 | fontWeight: "500",
164 | },
165 | ],
166 | "x-small-semibold": [
167 | "7px",
168 | {
169 | lineHeight: "9.318px",
170 | fontWeight: "600",
171 | },
172 | ],
173 | },
174 | extend: {
175 | colors: {
176 | "primary-500": "#877EFF",
177 | "secondary-500": "#FFB620",
178 | blue: "#0095F6",
179 | "logout-btn": "#FF5A5A",
180 | "navbar-menu": "rgba(16, 16, 18, 0.6)",
181 | "dark-1": "#000000",
182 | "dark-2": "#121417",
183 | "dark-3": "#101012",
184 | "dark-4": "#1F1F22",
185 | "light-1": "#FFFFFF",
186 | "light-2": "#EFEFEF",
187 | "light-3": "#7878A3",
188 | "light-4": "#5C5C7B",
189 | "gray-1": "#697C89",
190 | glassmorphism: "rgba(16, 16, 18, 0.60)",
191 | },
192 | boxShadow: {
193 | "count-badge": "0px 0px 6px 2px rgba(219, 188, 159, 0.30)",
194 | "groups-sidebar": "-30px 0px 60px 0px rgba(28, 28, 31, 0.50)",
195 | },
196 | screens: {
197 | xs: "400px",
198 | },
199 | keyframes: {
200 | "accordion-down": {
201 | from: { height: 0 },
202 | to: { height: "var(--radix-accordion-content-height)" },
203 | },
204 | "accordion-up": {
205 | from: { height: "var(--radix-accordion-content-height)" },
206 | to: { height: 0 },
207 | },
208 | },
209 | animation: {
210 | "accordion-down": "accordion-down 0.2s ease-out",
211 | "accordion-up": "accordion-up 0.2s ease-out",
212 | },
213 | },
214 | },
215 | plugins: [require("tailwindcss-animate")],
216 | };
217 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": true,
6 | "skipLibCheck": true,
7 | "strict": true,
8 | "forceConsistentCasingInFileNames": true,
9 | "noEmit": true,
10 | "esModuleInterop": true,
11 | "module": "esnext",
12 | "moduleResolution": "node",
13 | "resolveJsonModule": true,
14 | "isolatedModules": true,
15 | "jsx": "preserve",
16 | "incremental": true,
17 | "plugins": [
18 | {
19 | "name": "next"
20 | }
21 | ],
22 | "paths": {
23 | "@/*": ["./*"]
24 | }
25 | },
26 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
27 | "exclude": ["node_modules"]
28 | }
29 |
--------------------------------------------------------------------------------