Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

https://github.com/strange1108/thread-app

This project provides users with a platform to create threads, post comments, upvote/downvote content, and engage in threaded discussions on various topics of interest.
https://github.com/strange1108/thread-app

Last synced: about 2 months ago
JSON representation

This project provides users with a platform to create threads, post comments, upvote/downvote content, and engage in threaded discussions on various topics of interest.

Awesome Lists containing this project

README

        




Project Banner


nextdotjs
mongodb
tailwindcss
clerk
shadcnui
zod
typescript


A full stack Threads Clone


## πŸ“‹ Table of Contents

1. πŸ€– [Introduction](#introduction)
2. βš™οΈ [Tech Stack](#tech-stack)
3. πŸ”‹ [Features](#features)
4. 🀸 [Quick Start](#quick-start)
5. πŸ•ΈοΈ [Snippets](#snippets)
6. πŸ”— [Links](#links)

## πŸ€– Introduction

Build a full stack Threads clone using Next.js 14+ with a redesigned look transformed from a Figma design, user interaction to community management, technical implementation, and various features, including nested deep comments, notifications, real-time-search, and more.

## βš™οΈ Tech Stack

- Next.js
- MongoDB
- Shadcn UI
- TailwindCSS
- Clerk
- Webhooks
- Serverless APIs
- React Hook Form
- Zod
- TypeScript

## πŸ”‹ Features

πŸ‘‰ **Authentication**: Authentication using Clerk for email, password, and social logins (Google and GitHub) with a comprehensive profile management system.

πŸ‘‰ **Visually Appealing Home Page**: A visually appealing home page showcasing the latest threads for an engaging user experience.

πŸ‘‰ **Create Thread Page**: A dedicated page for users to create threads, fostering community engagement

πŸ‘‰ **Commenting Feature**: A commenting feature to facilitate discussions within threads.

πŸ‘‰ **Nested Commenting**: Commenting system with nested threads, providing a structured conversation flow.

πŸ‘‰ **User Search with Pagination**: A user search feature with pagination for easy exploration and discovery of other users.

πŸ‘‰ **Activity Page**: Display notifications on the activity page when someone comments on a user's thread, enhancing user engagement.

πŸ‘‰ **Profile Page**: User profile pages for showcasing information and enabling modification of profile settings.

πŸ‘‰ **Create and Invite to Communities**: Allow users to create new communities and invite others using customizable template emails.

πŸ‘‰ **Community Member Management**: A user-friendly interface to manage community members, allowing role changes and removals.

πŸ‘‰ **Admin-Specific Community Threads**: Enable admins to create threads specifically for their community.

πŸ‘‰ **Community Search with Pagination**: A community search feature with pagination for exploring different communities.

πŸ‘‰ **Community Profiles**: Display community profiles showcasing threads and members for a comprehensive overview.

πŸ‘‰ **Figma Design Implementation**: Transform Figma designs into a fully functional application with pixel-perfect and responsive design.

πŸ‘‰ **Blazing-Fast Performance**: Optimal performance and instantaneous page switching for a seamless user experience.

πŸ‘‰ **Server Side Rendering**: Utilize Next.js with Server Side Rendering for enhanced performance and SEO benefits.

πŸ‘‰ **MongoDB with Complex Schemas**: Handle complex schemas and multiple data populations using MongoDB.

πŸ‘‰ **File Uploads with UploadThing**: File uploads using UploadThing for a seamless media sharing experience.

πŸ‘‰ **Real-Time Events Listening**: Real-time events listening with webhooks to keep users updated.

πŸ‘‰ **Middleware, API Actions, and Authorization**: Utilize middleware, API actions, and authorization for robust application security.

πŸ‘‰ **Next.js Layout Route Groups**: New Next.js layout route groups for efficient routing

πŸ‘‰ **Data Validation with Zod**: Data integrity with data validation using Zod

πŸ‘‰ **Form Management with React Hook Form**: Efficient management of forms with React Hook Form for a streamlined user input experience.

and many more, including code architecture and reusability

## 🀸 Quick Start

Follow these steps to set up the project locally on your machine.

**Prerequisites**

Make sure you have the following installed on your machine:

- [Git](https://git-scm.com/)
- [Node.js](https://nodejs.org/en)
- [npm](https://www.npmjs.com/) (Node Package Manager)

**Cloning the Repository**

```bash
git clone https://github.com/Strange1108/Thread-App.git
cd Thread-App
```

**Installation**

Install the project dependencies using npm:

```bash
npm install
```

**Set Up Environment Variables**

Create a new file named `.env` in the root of your project and add the following content:

```env
MONGODB_URL=
CLERK_SECRET_KEY=
UPLOADTHING_SECRET=
UPLOADTHING_APP_ID=
NEXT_CLERK_WEBHOOK_SECRET=
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=
```

Replace the placeholder values with your actual credentials. You can obtain these credentials by signing up for the corresponding websites on [MongoDB](https://www.mongodb.com/), [Clerk](https://clerk.com/), and [Uploadthing](https://uploadthing.com/).

**Running the Project**

```bash
npm run dev
```

Open [http://localhost:3000](http://localhost:3000) in your browser to view the project.

## πŸ•ΈοΈ Snippets

clerk.route.ts

```typescript
/* eslint-disable camelcase */
// Resource: https://clerk.com/docs/users/sync-data-to-your-backend
// Above article shows why we need webhooks i.e., to sync data to our backend

// Resource: https://docs.svix.com/receiving/verifying-payloads/why
// It's a good practice to verify webhooks. Above article shows why we should do it
import { Webhook, WebhookRequiredHeaders } from "svix";
import { headers } from "next/headers";

import { IncomingHttpHeaders } from "http";

import { NextResponse } from "next/server";
import {
addMemberToCommunity,
createCommunity,
deleteCommunity,
removeUserFromCommunity,
updateCommunityInfo,
} from "@/lib/actions/community.actions";

// Resource: https://clerk.com/docs/integration/webhooks#supported-events
// Above document lists the supported events
type EventType =
| "organization.created"
| "organizationInvitation.created"
| "organizationMembership.created"
| "organizationMembership.deleted"
| "organization.updated"
| "organization.deleted";

type Event = {
data: Record[]>;
object: "event";
type: EventType;
};

export const POST = async (request: Request) => {
const payload = await request.json();
const header = headers();

const heads = {
"svix-id": header.get("svix-id"),
"svix-timestamp": header.get("svix-timestamp"),
"svix-signature": header.get("svix-signature"),
};

// Activitate Webhook in the Clerk Dashboard.
// After adding the endpoint, you'll see the secret on the right side.
const wh = new Webhook(process.env.NEXT_CLERK_WEBHOOK_SECRET || "");

let evnt: Event | null = null;

try {
evnt = wh.verify(
JSON.stringify(payload),
heads as IncomingHttpHeaders & WebhookRequiredHeaders
) as Event;
} catch (err) {
return NextResponse.json({ message: err }, { status: 400 });
}

const eventType: EventType = evnt?.type!;

// Listen organization creation event
if (eventType === "organization.created") {
// Resource: https://clerk.com/docs/reference/backend-api/tag/Organizations#operation/CreateOrganization
// Show what evnt?.data sends from above resource
const { id, name, slug, logo_url, image_url, created_by } =
evnt?.data ?? {};

try {
// @ts-ignore
await createCommunity(
// @ts-ignore
id,
name,
slug,
logo_url || image_url,
"org bio",
created_by
);

return NextResponse.json({ message: "User created" }, { status: 201 });
} catch (err) {
console.log(err);
return NextResponse.json(
{ message: "Internal Server Error" },
{ status: 500 }
);
}
}

// Listen organization invitation creation event.
// Just to show. You can avoid this or tell people that we can create a new mongoose action and
// add pending invites in the database.
if (eventType === "organizationInvitation.created") {
try {
// Resource: https://clerk.com/docs/reference/backend-api/tag/Organization-Invitations#operation/CreateOrganizationInvitation
console.log("Invitation created", evnt?.data);

return NextResponse.json(
{ message: "Invitation created" },
{ status: 201 }
);
} catch (err) {
console.log(err);

return NextResponse.json(
{ message: "Internal Server Error" },
{ status: 500 }
);
}
}

// Listen organization membership (member invite & accepted) creation
if (eventType === "organizationMembership.created") {
try {
// Resource: https://clerk.com/docs/reference/backend-api/tag/Organization-Memberships#operation/CreateOrganizationMembership
// Show what evnt?.data sends from above resource
const { organization, public_user_data } = evnt?.data;
console.log("created", evnt?.data);

// @ts-ignore
await addMemberToCommunity(organization.id, public_user_data.user_id);

return NextResponse.json(
{ message: "Invitation accepted" },
{ status: 201 }
);
} catch (err) {
console.log(err);

return NextResponse.json(
{ message: "Internal Server Error" },
{ status: 500 }
);
}
}

// Listen member deletion event
if (eventType === "organizationMembership.deleted") {
try {
// Resource: https://clerk.com/docs/reference/backend-api/tag/Organization-Memberships#operation/DeleteOrganizationMembership
// Show what evnt?.data sends from above resource
const { organization, public_user_data } = evnt?.data;
console.log("removed", evnt?.data);

// @ts-ignore
await removeUserFromCommunity(public_user_data.user_id, organization.id);

return NextResponse.json({ message: "Member removed" }, { status: 201 });
} catch (err) {
console.log(err);

return NextResponse.json(
{ message: "Internal Server Error" },
{ status: 500 }
);
}
}

// Listen organization updation event
if (eventType === "organization.updated") {
try {
// Resource: https://clerk.com/docs/reference/backend-api/tag/Organizations#operation/UpdateOrganization
// Show what evnt?.data sends from above resource
const { id, logo_url, name, slug } = evnt?.data;
console.log("updated", evnt?.data);

// @ts-ignore
await updateCommunityInfo(id, name, slug, logo_url);

return NextResponse.json({ message: "Member removed" }, { status: 201 });
} catch (err) {
console.log(err);

return NextResponse.json(
{ message: "Internal Server Error" },
{ status: 500 }
);
}
}

// Listen organization deletion event
if (eventType === "organization.deleted") {
try {
// Resource: https://clerk.com/docs/reference/backend-api/tag/Organizations#operation/DeleteOrganization
// Show what evnt?.data sends from above resource
const { id } = evnt?.data;
console.log("deleted", evnt?.data);

// @ts-ignore
await deleteCommunity(id);

return NextResponse.json(
{ message: "Organization deleted" },
{ status: 201 }
);
} catch (err) {
console.log(err);

return NextResponse.json(
{ message: "Internal Server Error" },
{ status: 500 }
);
}
}
};
```

community.actions.ts

```typescript
"use server";

import { FilterQuery, SortOrder } from "mongoose";

import Community from "../models/community.model";
import Thread from "../models/thread.model";
import User from "../models/user.model";

import { connectToDB } from "../mongoose";

export async function createCommunity(
id: string,
name: string,
username: string,
image: string,
bio: string,
createdById: string // Change the parameter name to reflect it's an id
) {
try {
connectToDB();

// Find the user with the provided unique id
const user = await User.findOne({ id: createdById });

if (!user) {
throw new Error("User not found"); // Handle the case if the user with the id is not found
}

const newCommunity = new Community({
id,
name,
username,
image,
bio,
createdBy: user._id, // Use the mongoose ID of the user
});

const createdCommunity = await newCommunity.save();

// Update User model
user.communities.push(createdCommunity._id);
await user.save();

return createdCommunity;
} catch (error) {
// Handle any errors
console.error("Error creating community:", error);
throw error;
}
}

export async function fetchCommunityDetails(id: string) {
try {
connectToDB();

const communityDetails = await Community.findOne({ id }).populate([
"createdBy",
{
path: "members",
model: User,
select: "name username image _id id",
},
]);

return communityDetails;
} catch (error) {
// Handle any errors
console.error("Error fetching community details:", error);
throw error;
}
}

export async function fetchCommunityPosts(id: string) {
try {
connectToDB();

const communityPosts = await Community.findById(id).populate({
path: "threads",
model: Thread,
populate: [
{
path: "author",
model: User,
select: "name image id", // Select the "name" and "_id" fields from the "User" model
},
{
path: "children",
model: Thread,
populate: {
path: "author",
model: User,
select: "image _id", // Select the "name" and "_id" fields from the "User" model
},
},
],
});

return communityPosts;
} catch (error) {
// Handle any errors
console.error("Error fetching community posts:", error);
throw error;
}
}

export async function fetchCommunities({
searchString = "",
pageNumber = 1,
pageSize = 20,
sortBy = "desc",
}: {
searchString?: string;
pageNumber?: number;
pageSize?: number;
sortBy?: SortOrder;
}) {
try {
connectToDB();

// Calculate the number of communities to skip based on the page number and page size.
const skipAmount = (pageNumber - 1) * pageSize;

// Create a case-insensitive regular expression for the provided search string.
const regex = new RegExp(searchString, "i");

// Create an initial query object to filter communities.
const query: FilterQuery = {};

// If the search string is not empty, add the $or operator to match either username or name fields.
if (searchString.trim() !== "") {
query.$or = [
{ username: { $regex: regex } },
{ name: { $regex: regex } },
];
}

// Define the sort options for the fetched communities based on createdAt field and provided sort order.
const sortOptions = { createdAt: sortBy };

// Create a query to fetch the communities based on the search and sort criteria.
const communitiesQuery = Community.find(query)
.sort(sortOptions)
.skip(skipAmount)
.limit(pageSize)
.populate("members");

// Count the total number of communities that match the search criteria (without pagination).
const totalCommunitiesCount = await Community.countDocuments(query);

const communities = await communitiesQuery.exec();

// Check if there are more communities beyond the current page.
const isNext = totalCommunitiesCount > skipAmount + communities.length;

return { communities, isNext };
} catch (error) {
console.error("Error fetching communities:", error);
throw error;
}
}

export async function addMemberToCommunity(
communityId: string,
memberId: string
) {
try {
connectToDB();

// Find the community by its unique id
const community = await Community.findOne({ id: communityId });

if (!community) {
throw new Error("Community not found");
}

// Find the user by their unique id
const user = await User.findOne({ id: memberId });

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

// Check if the user is already a member of the community
if (community.members.includes(user._id)) {
throw new Error("User is already a member of the community");
}

// Add the user's _id to the members array in the community
community.members.push(user._id);
await community.save();

// Add the community's _id to the communities array in the user
user.communities.push(community._id);
await user.save();

return community;
} catch (error) {
// Handle any errors
console.error("Error adding member to community:", error);
throw error;
}
}

export async function removeUserFromCommunity(
userId: string,
communityId: string
) {
try {
connectToDB();

const userIdObject = await User.findOne({ id: userId }, { _id: 1 });
const communityIdObject = await Community.findOne(
{ id: communityId },
{ _id: 1 }
);

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

if (!communityIdObject) {
throw new Error("Community not found");
}

// Remove the user's _id from the members array in the community
await Community.updateOne(
{ _id: communityIdObject._id },
{ $pull: { members: userIdObject._id } }
);

// Remove the community's _id from the communities array in the user
await User.updateOne(
{ _id: userIdObject._id },
{ $pull: { communities: communityIdObject._id } }
);

return { success: true };
} catch (error) {
// Handle any errors
console.error("Error removing user from community:", error);
throw error;
}
}

export async function updateCommunityInfo(
communityId: string,
name: string,
username: string,
image: string
) {
try {
connectToDB();

// Find the community by its _id and update the information
const updatedCommunity = await Community.findOneAndUpdate(
{ id: communityId },
{ name, username, image }
);

if (!updatedCommunity) {
throw new Error("Community not found");
}

return updatedCommunity;
} catch (error) {
// Handle any errors
console.error("Error updating community information:", error);
throw error;
}
}

export async function deleteCommunity(communityId: string) {
try {
connectToDB();

// Find the community by its ID and delete it
const deletedCommunity = await Community.findOneAndDelete({
id: communityId,
});

if (!deletedCommunity) {
throw new Error("Community not found");
}

// Delete all threads associated with the community
await Thread.deleteMany({ community: communityId });

// Find all users who are part of the community
const communityUsers = await User.find({ communities: communityId });

// Remove the community from the 'communities' array for each user
const updateUserPromises = communityUsers.map((user) => {
user.communities.pull(communityId);
return user.save();
});

await Promise.all(updateUserPromises);

return deletedCommunity;
} catch (error) {
console.error("Error deleting community: ", error);
throw error;
}
}
```

CommunityCard.tsx

```typescript
import Image from "next/image";
import Link from "next/link";

import { Button } from "../ui/button";

interface Props {
id: string;
name: string;
username: string;
imgUrl: string;
bio: string;
members: {
image: string;
}[];
}

function CommunityCard({ id, name, username, imgUrl, bio, members }: Props) {
return (






{name}



@{username}



{bio}




View

{members.length > 0 && (


{members.map((member, index) => (

))}
{members.length > 3 && (


{members.length}+ Users


)}

)}


);
}

export default CommunityCard;
```

constants.index.ts

```typescript
export const sidebarLinks = [
{
imgURL: "/assets/home.svg",
route: "/",
label: "Home",
},
{
imgURL: "/assets/search.svg",
route: "/search",
label: "Search",
},
{
imgURL: "/assets/heart.svg",
route: "/activity",
label: "Activity",
},
{
imgURL: "/assets/create.svg",
route: "/create-thread",
label: "Create Thread",
},
{
imgURL: "/assets/community.svg",
route: "/communities",
label: "Communities",
},
{
imgURL: "/assets/user.svg",
route: "/profile",
label: "Profile",
},
];

export const profileTabs = [
{ value: "threads", label: "Threads", icon: "/assets/reply.svg" },
{ value: "replies", label: "Replies", icon: "/assets/members.svg" },
{ value: "tagged", label: "Tagged", icon: "/assets/tag.svg" },
];

export const communityTabs = [
{ value: "threads", label: "Threads", icon: "/assets/reply.svg" },
{ value: "members", label: "Members", icon: "/assets/members.svg" },
{ value: "requests", label: "Requests", icon: "/assets/request.svg" },
];
```

globals.css

```css
@tailwind base;
@tailwind components;
@tailwind utilities;

@layer components {
/* main */
.main-container {
@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;
}

/* Head Text */
.head-text {
@apply text-heading2-bold text-light-1;
}

/* Activity */
.activity-card {
@apply flex items-center gap-2 rounded-md bg-dark-2 px-7 py-4;
}

/* No Result */
.no-result {
@apply text-center !text-base-regular text-light-3;
}

/* Community Card */
.community-card {
@apply w-full rounded-lg bg-dark-3 px-4 py-5 sm:w-96;
}

.community-card_btn {
@apply rounded-lg bg-primary-500 px-5 py-1.5 text-small-regular !text-light-1 !important;
}

/* thread card */
.thread-card_bar {
@apply relative mt-2 w-0.5 grow rounded-full bg-neutral-800;
}

/* User card */
.user-card {
@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;
}

.user-card_avatar {
@apply flex flex-1 items-start justify-start gap-3 xs:items-center;
}

.user-card_btn {
@apply h-auto min-w-[74px] rounded-lg bg-primary-500 text-[12px] text-light-1 !important;
}

.searchbar {
@apply flex gap-1 rounded-lg bg-dark-3 px-4 py-2;
}

.searchbar_input {
@apply border-none bg-dark-3 text-base-regular text-light-4 outline-none !important;
}

.topbar {
@apply fixed top-0 z-30 flex w-full items-center justify-between bg-dark-2 px-6 py-3;
}

.bottombar {
@apply fixed bottom-0 z-10 w-full rounded-t-3xl bg-glassmorphism p-4 backdrop-blur-lg xs:px-7 md:hidden;
}

.bottombar_container {
@apply flex items-center justify-between gap-3 xs:gap-5;
}

.bottombar_link {
@apply relative flex flex-col items-center gap-2 rounded-lg p-2 sm:flex-1 sm:px-2 sm:py-2.5;
}

.leftsidebar {
@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;
}

.leftsidebar_link {
@apply relative flex justify-start gap-4 rounded-lg p-4;
}

.pagination {
@apply mt-10 flex w-full items-center justify-center gap-5;
}

.rightsidebar {
@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;
}
}

@layer utilities {
.css-invert {
@apply invert-[50%] brightness-200;
}

.custom-scrollbar::-webkit-scrollbar {
width: 3px;
height: 3px;
border-radius: 2px;
}

.custom-scrollbar::-webkit-scrollbar-track {
background: #09090a;
}

.custom-scrollbar::-webkit-scrollbar-thumb {
background: #5c5c7b;
border-radius: 50px;
}

.custom-scrollbar::-webkit-scrollbar-thumb:hover {
background: #7878a3;
}
}

/* Clerk Responsive fix */
.cl-organizationSwitcherTrigger .cl-userPreview .cl-userPreviewTextContainer {
@apply max-sm:hidden;
}

.cl-organizationSwitcherTrigger
.cl-organizationPreview
.cl-organizationPreviewTextContainer {
@apply max-sm:hidden;
}

/* Shadcn Component Styles */

/* Tab */
.tab {
@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;
}

.no-focus {
@apply focus-visible:ring-0 focus-visible:ring-transparent focus-visible:ring-offset-0 !important;
}

/* Account Profile */
.account-form_image-label {
@apply flex h-24 w-24 items-center justify-center rounded-full bg-dark-4 !important;
}

.account-form_image-input {
@apply cursor-pointer border-none bg-transparent outline-none file:text-blue !important;
}

.account-form_input {
@apply border border-dark-4 bg-dark-3 text-light-1 !important;
}

/* Comment Form */
.comment-form {
@apply mt-10 flex items-center gap-4 border-y border-y-dark-4 py-5 max-xs:flex-col !important;
}

.comment-form_btn {
@apply rounded-3xl bg-primary-500 px-8 py-2 !text-small-regular text-light-1 max-xs:w-full !important;
}
```

next.config.js

```javascript
/** @type {import('next').NextConfig} */
const nextConfig = {
experimental: {
serverActions: true,
serverComponentsExternalPackages: ["mongoose"],
},
images: {
remotePatterns: [
{
protocol: "https",
hostname: "img.clerk.com",
},
{
protocol: "https",
hostname: "images.clerk.dev",
},
{
protocol: "https",
hostname: "uploadthing.com",
},
{
protocol: "https",
hostname: "placehold.co",
},
],
typescript: {
ignoreBuildErrors: true,
},
},
};

module.exports = nextConfig;
```

tailwind.config.js

```javascript
/** @type {import('tailwindcss').Config} */
module.exports = {
darkMode: ["class"],
content: [
"./pages/**/*.{ts,tsx}",
"./components/**/*.{ts,tsx}",
"./app/**/*.{ts,tsx}",
"./src/**/*.{ts,tsx}",
],
theme: {
container: {
center: true,
padding: "2rem",
screens: {
"2xl": "1400px",
},
},
fontSize: {
"heading1-bold": [
"36px",
{
lineHeight: "140%",
fontWeight: "700",
},
],
"heading1-semibold": [
"36px",
{
lineHeight: "140%",
fontWeight: "600",
},
],
"heading2-bold": [
"30px",
{
lineHeight: "140%",
fontWeight: "700",
},
],
"heading2-semibold": [
"30px",
{
lineHeight: "140%",
fontWeight: "600",
},
],
"heading3-bold": [
"24px",
{
lineHeight: "140%",
fontWeight: "700",
},
],
"heading4-medium": [
"20px",
{
lineHeight: "140%",
fontWeight: "500",
},
],
"body-bold": [
"18px",
{
lineHeight: "140%",
fontWeight: "700",
},
],
"body-semibold": [
"18px",
{
lineHeight: "140%",
fontWeight: "600",
},
],
"body-medium": [
"18px",
{
lineHeight: "140%",
fontWeight: "500",
},
],
"body-normal": [
"18px",
{
lineHeight: "140%",
fontWeight: "400",
},
],
"body1-bold": [
"18px",
{
lineHeight: "140%",
fontWeight: "700",
},
],
"base-regular": [
"16px",
{
lineHeight: "140%",
fontWeight: "400",
},
],
"base-medium": [
"16px",
{
lineHeight: "140%",
fontWeight: "500",
},
],
"base-semibold": [
"16px",
{
lineHeight: "140%",
fontWeight: "600",
},
],
"base1-semibold": [
"16px",
{
lineHeight: "140%",
fontWeight: "600",
},
],
"small-regular": [
"14px",
{
lineHeight: "140%",
fontWeight: "400",
},
],
"small-medium": [
"14px",
{
lineHeight: "140%",
fontWeight: "500",
},
],
"small-semibold": [
"14px",
{
lineHeight: "140%",
fontWeight: "600",
},
],
"subtle-medium": [
"12px",
{
lineHeight: "16px",
fontWeight: "500",
},
],
"subtle-semibold": [
"12px",
{
lineHeight: "16px",
fontWeight: "600",
},
],
"tiny-medium": [
"10px",
{
lineHeight: "140%",
fontWeight: "500",
},
],
"x-small-semibold": [
"7px",
{
lineHeight: "9.318px",
fontWeight: "600",
},
],
},
extend: {
colors: {
"primary-500": "#877EFF",
"secondary-500": "#FFB620",
blue: "#0095F6",
"logout-btn": "#FF5A5A",
"navbar-menu": "rgba(16, 16, 18, 0.6)",
"dark-1": "#000000",
"dark-2": "#121417",
"dark-3": "#101012",
"dark-4": "#1F1F22",
"light-1": "#FFFFFF",
"light-2": "#EFEFEF",
"light-3": "#7878A3",
"light-4": "#5C5C7B",
"gray-1": "#697C89",
glassmorphism: "rgba(16, 16, 18, 0.60)",
},
boxShadow: {
"count-badge": "0px 0px 6px 2px rgba(219, 188, 159, 0.30)",
"groups-sidebar": "-30px 0px 60px 0px rgba(28, 28, 31, 0.50)",
},
screens: {
xs: "400px",
},
keyframes: {
"accordion-down": {
from: { height: 0 },
to: { height: "var(--radix-accordion-content-height)" },
},
"accordion-up": {
from: { height: "var(--radix-accordion-content-height)" },
to: { height: 0 },
},
},
animation: {
"accordion-down": "accordion-down 0.2s ease-out",
"accordion-up": "accordion-up 0.2s ease-out",
},
},
},
plugins: [require("tailwindcss-animate")],
};
```

thread.actions.ts

```typescript
"use server";

import { revalidatePath } from "next/cache";

import { connectToDB } from "../mongoose";

import User from "../models/user.model";
import Thread from "../models/thread.model";
import Community from "../models/community.model";

export async function fetchPosts(pageNumber = 1, pageSize = 20) {
connectToDB();

// Calculate the number of posts to skip based on the page number and page size.
const skipAmount = (pageNumber - 1) * pageSize;

// Create a query to fetch the posts that have no parent (top-level threads) (a thread that is not a comment/reply).
const postsQuery = Thread.find({ parentId: { $in: [null, undefined] } })
.sort({ createdAt: "desc" })
.skip(skipAmount)
.limit(pageSize)
.populate({
path: "author",
model: User,
})
.populate({
path: "community",
model: Community,
})
.populate({
path: "children", // Populate the children field
populate: {
path: "author", // Populate the author field within children
model: User,
select: "_id name parentId image", // Select only _id and username fields of the author
},
});

// Count the total number of top-level posts (threads) i.e., threads that are not comments.
const totalPostsCount = await Thread.countDocuments({
parentId: { $in: [null, undefined] },
}); // Get the total count of posts

const posts = await postsQuery.exec();

const isNext = totalPostsCount > skipAmount + posts.length;

return { posts, isNext };
}

interface Params {
text: string,
author: string,
communityId: string | null,
path: string,
}

export async function createThread({ text, author, communityId, path }: Params
) {
try {
connectToDB();

const communityIdObject = await Community.findOne(
{ id: communityId },
{ _id: 1 }
);

const createdThread = await Thread.create({
text,
author,
community: communityIdObject, // Assign communityId if provided, or leave it null for personal account
});

// Update User model
await User.findByIdAndUpdate(author, {
$push: { threads: createdThread._id },
});

if (communityIdObject) {
// Update Community model
await Community.findByIdAndUpdate(communityIdObject, {
$push: { threads: createdThread._id },
});
}

revalidatePath(path);
} catch (error: any) {
throw new Error(`Failed to create thread: ${error.message}`);
}
}

async function fetchAllChildThreads(threadId: string): Promise {
const childThreads = await Thread.find({ parentId: threadId });

const descendantThreads = [];
for (const childThread of childThreads) {
const descendants = await fetchAllChildThreads(childThread._id);
descendantThreads.push(childThread, ...descendants);
}

return descendantThreads;
}

export async function deleteThread(id: string, path: string): Promise {
try {
connectToDB();

// Find the thread to be deleted (the main thread)
const mainThread = await Thread.findById(id).populate("author community");

if (!mainThread) {
throw new Error("Thread not found");
}

// Fetch all child threads and their descendants recursively
const descendantThreads = await fetchAllChildThreads(id);

// Get all descendant thread IDs including the main thread ID and child thread IDs
const descendantThreadIds = [
id,
...descendantThreads.map((thread) => thread._id),
];

// Extract the authorIds and communityIds to update User and Community models respectively
const uniqueAuthorIds = new Set(
[
...descendantThreads.map((thread) => thread.author?._id?.toString()), // Use optional chaining to handle possible undefined values
mainThread.author?._id?.toString(),
].filter((id) => id !== undefined)
);

const uniqueCommunityIds = new Set(
[
...descendantThreads.map((thread) => thread.community?._id?.toString()), // Use optional chaining to handle possible undefined values
mainThread.community?._id?.toString(),
].filter((id) => id !== undefined)
);

// Recursively delete child threads and their descendants
await Thread.deleteMany({ _id: { $in: descendantThreadIds } });

// Update User model
await User.updateMany(
{ _id: { $in: Array.from(uniqueAuthorIds) } },
{ $pull: { threads: { $in: descendantThreadIds } } }
);

// Update Community model
await Community.updateMany(
{ _id: { $in: Array.from(uniqueCommunityIds) } },
{ $pull: { threads: { $in: descendantThreadIds } } }
);

revalidatePath(path);
} catch (error: any) {
throw new Error(`Failed to delete thread: ${error.message}`);
}
}

export async function fetchThreadById(threadId: string) {
connectToDB();

try {
const thread = await Thread.findById(threadId)
.populate({
path: "author",
model: User,
select: "_id id name image",
}) // Populate the author field with _id and username
.populate({
path: "community",
model: Community,
select: "_id id name image",
}) // Populate the community field with _id and name
.populate({
path: "children", // Populate the children field
populate: [
{
path: "author", // Populate the author field within children
model: User,
select: "_id id name parentId image", // Select only _id and username fields of the author
},
{
path: "children", // Populate the children field within children
model: Thread, // The model of the nested children (assuming it's the same "Thread" model)
populate: {
path: "author", // Populate the author field within nested children
model: User,
select: "_id id name parentId image", // Select only _id and username fields of the author
},
},
],
})
.exec();

return thread;
} catch (err) {
console.error("Error while fetching thread:", err);
throw new Error("Unable to fetch thread");
}
}

export async function addCommentToThread(
threadId: string,
commentText: string,
userId: string,
path: string
) {
connectToDB();

try {
// Find the original thread by its ID
const originalThread = await Thread.findById(threadId);

if (!originalThread) {
throw new Error("Thread not found");
}

// Create the new comment thread
const commentThread = new Thread({
text: commentText,
author: userId,
parentId: threadId, // Set the parentId to the original thread's ID
});

// Save the comment thread to the database
const savedCommentThread = await commentThread.save();

// Add the comment thread's ID to the original thread's children array
originalThread.children.push(savedCommentThread._id);

// Save the updated original thread to the database
await originalThread.save();

revalidatePath(path);
} catch (err) {
console.error("Error while adding comment:", err);
throw new Error("Unable to add comment");
}
}
```

uploadthing.ts

```typescript
// Resource: https://docs.uploadthing.com/api-reference/react#generatereacthelpers
// Copy paste (be careful with imports)

import { generateReactHelpers } from "@uploadthing/react/hooks";

import type { OurFileRouter } from "@/app/api/uploadthing/core";

export const { useUploadThing, uploadFiles } = generateReactHelpers();
```

user.actions.ts

```typescript
"use server";

import { FilterQuery, SortOrder } from "mongoose";
import { revalidatePath } from "next/cache";

import Community from "../models/community.model";
import Thread from "../models/thread.model";
import User from "../models/user.model";

import { connectToDB } from "../mongoose";

export async function fetchUser(userId: string) {
try {
connectToDB();

return await User.findOne({ id: userId }).populate({
path: "communities",
model: Community,
});
} catch (error: any) {
throw new Error(`Failed to fetch user: ${error.message}`);
}
}

interface Params {
userId: string;
username: string;
name: string;
bio: string;
image: string;
path: string;
}

export async function updateUser({
userId,
bio,
name,
path,
username,
image,
}: Params): Promise {
try {
connectToDB();

await User.findOneAndUpdate(
{ id: userId },
{
username: username.toLowerCase(),
name,
bio,
image,
onboarded: true,
},
{ upsert: true }
);

if (path === "/profile/edit") {
revalidatePath(path);
}
} catch (error: any) {
throw new Error(`Failed to create/update user: ${error.message}`);
}
}

export async function fetchUserPosts(userId: string) {
try {
connectToDB();

// Find all threads authored by the user with the given userId
const threads = await User.findOne({ id: userId }).populate({
path: "threads",
model: Thread,
populate: [
{
path: "community",
model: Community,
select: "name id image _id", // Select the "name" and "_id" fields from the "Community" model
},
{
path: "children",
model: Thread,
populate: {
path: "author",
model: User,
select: "name image id", // Select the "name" and "_id" fields from the "User" model
},
},
],
});
return threads;
} catch (error) {
console.error("Error fetching user threads:", error);
throw error;
}
}

// Almost similar to Thead (search + pagination) and Community (search + pagination)
export async function fetchUsers({
userId,
searchString = "",
pageNumber = 1,
pageSize = 20,
sortBy = "desc",
}: {
userId: string;
searchString?: string;
pageNumber?: number;
pageSize?: number;
sortBy?: SortOrder;
}) {
try {
connectToDB();

// Calculate the number of users to skip based on the page number and page size.
const skipAmount = (pageNumber - 1) * pageSize;

// Create a case-insensitive regular expression for the provided search string.
const regex = new RegExp(searchString, "i");

// Create an initial query object to filter users.
const query: FilterQuery = {
id: { $ne: userId }, // Exclude the current user from the results.
};

// If the search string is not empty, add the $or operator to match either username or name fields.
if (searchString.trim() !== "") {
query.$or = [
{ username: { $regex: regex } },
{ name: { $regex: regex } },
];
}

// Define the sort options for the fetched users based on createdAt field and provided sort order.
const sortOptions = { createdAt: sortBy };

const usersQuery = User.find(query)
.sort(sortOptions)
.skip(skipAmount)
.limit(pageSize);

// Count the total number of users that match the search criteria (without pagination).
const totalUsersCount = await User.countDocuments(query);

const users = await usersQuery.exec();

// Check if there are more users beyond the current page.
const isNext = totalUsersCount > skipAmount + users.length;

return { users, isNext };
} catch (error) {
console.error("Error fetching users:", error);
throw error;
}
}

export async function getActivity(userId: string) {
try {
connectToDB();

// Find all threads created by the user
const userThreads = await Thread.find({ author: userId });

// Collect all the child thread ids (replies) from the 'children' field of each user thread
const childThreadIds = userThreads.reduce((acc, userThread) => {
return acc.concat(userThread.children);
}, []);

// Find and return the child threads (replies) excluding the ones created by the same user
const replies = await Thread.find({
_id: { $in: childThreadIds },
author: { $ne: userId }, // Exclude threads authored by the same user
}).populate({
path: "author",
model: User,
select: "name image _id",
});

return replies;
} catch (error) {
console.error("Error fetching replies: ", error);
throw error;
}
}
```

utils.ts

```typescript
import { type ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";

// generated by shadcn
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}

// created by chatgpt
export function isBase64Image(imageData: string) {
const base64Regex = /^data:image\/(png|jpe?g|gif|webp);base64,/;
return base64Regex.test(imageData);
}

// created by chatgpt
export function formatDateString(dateString: string) {
const options: Intl.DateTimeFormatOptions = {
year: "numeric",
month: "short",
day: "numeric",
};

const date = new Date(dateString);
const formattedDate = date.toLocaleDateString(undefined, options);

const time = date.toLocaleTimeString([], {
hour: "numeric",
minute: "2-digit",
});

return `${time} - ${formattedDate}`;
}

// created by chatgpt
export function formatThreadCount(count: number): string {
if (count === 0) {
return "No Threads";
} else {
const threadCount = count.toString().padStart(2, "0");
const threadWord = count === 1 ? "Thread" : "Threads";
return `${threadCount} ${threadWord}`;
}
}
```

## πŸ”— Links

Assets used in the project are [here](https://drive.google.com/file/d/1lg7MMKgXwFabymHi1qxRYMxWVXiZPM9l/view)