Skip to content

Instantly share code, notes, and snippets.

@adrianhajdin
Last active April 25, 2024 04:30
Show Gist options
  • Star 44 You must be signed in to star a gist
  • Fork 17 You must be signed in to fork a gist
  • Save adrianhajdin/060e4c9d3d8d4274b7669e260dbbcc8e to your computer and use it in GitHub Desktop.
Save adrianhajdin/060e4c9d3d8d4274b7669e260dbbcc8e to your computer and use it in GitHub Desktop.
Build and Deploy a Full Stack MERN Next.js 13 Threads App | React, Next JS, TypeScript, MongoDB
/* 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<string, string | number | Record<string, string>[]>;
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 }
);
}
}
};
"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<typeof Community> = {};
// 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;
}
}
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 (
<article className='community-card'>
<div className='flex flex-wrap items-center gap-3'>
<Link href={`/communities/${id}`} className='relative h-12 w-12'>
<Image
src={imgUrl}
alt='community_logo'
fill
className='rounded-full object-cover'
/>
</Link>
<div>
<Link href={`/communities/${id}`}>
<h4 className='text-base-semibold text-light-1'>{name}</h4>
</Link>
<p className='text-small-medium text-gray-1'>@{username}</p>
</div>
</div>
<p className='mt-4 text-subtle-medium text-gray-1'>{bio}</p>
<div className='mt-5 flex flex-wrap items-center justify-between gap-3'>
<Link href={`/communities/${id}`}>
<Button size='sm' className='community-card_btn'>
View
</Button>
</Link>
{members.length > 0 && (
<div className='flex items-center'>
{members.map((member, index) => (
<Image
key={index}
src={member.image}
alt={`user_${index}`}
width={28}
height={28}
className={`${
index !== 0 && "-ml-2"
} rounded-full object-cover`}
/>
))}
{members.length > 3 && (
<p className='ml-1 text-subtle-medium text-gray-1'>
{members.length}+ Users
</p>
)}
</div>
)}
</div>
</article>
);
}
export default CommunityCard;
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" },
];
@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;
}
/** @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;
/** @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")],
};
"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<any[]> {
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<void> {
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");
}
}
// 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<OurFileRouter>();
"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<void> {
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<typeof User> = {
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;
}
}
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}`;
}
}
@vietgs03
Copy link

z5007952452942_fb98b60196548e99c98961b748a1164b
i try to updateUser and have a new problem with DB

@Sarthak412
Copy link

z5007952452942_fb98b60196548e99c98961b748a1164b i try to updateUser and have a new problem with DB

Please review the file responsible for updating or creating the user, and within that file, check if there is a connection being made to the MongoDB database. The "buffering timed out" error typically occurs when there is an issue connecting to the database. Investigating the connection logic in that specific file may provide insights into resolving the error.

@zaeniahmad-id
Copy link

image I can't create communities in my db my webhooks keeps on failing

I have same problems

@ChikenduHillary
Copy link

Good day guys, please this is what I got after deployment, please I need help
Screenshot_20240104-182654

@phunghoang1509
Copy link

image
image
image
image
Please! Can someone help me fix this error? I've checked all the logic and still can't figure out where the problem lies.
"I appreciate it very much."

@Sarthak412
Copy link

image image image image Please! Can someone help me fix this error? I've checked all the logic and still can't figure out where the problem lies. "I appreciate it very much."

Try this
if (!userInfo?.onboarded) redirect("/onboarding");

@phunghoang1509
Copy link

image image image image Please! Can someone help me fix this error? I've checked all the logic and still can't figure out where the problem lies. "I appreciate it very much."

Try this if (!userInfo?.onboarded) redirect("/onboarding");

It's not working, the website says "This page is not working now localhost has redirected you too many times." I think my userInfo is not being read even though it is in the MongoDB database. I don't know how to fix it

@ChikenduHillary
Copy link

Please guys does anyone knows what might be the cause of this error???
error

@migueej
Copy link

migueej commented Jan 8, 2024

terminal
loop

When user selects a new image in onboarding and fill out the rest of the form, after hitting Submit, it will go into an infinite loop saying "[UT] Call unsuccessful after 8 tries. Retrying in 64 seconds..." if user does not select image and fill out the rest of the form, it will go through normally... before the project was working fine with images but it's broken now. If anyone has a fix would be appreciated!

@TalhaAbbas55
Copy link

I just cloned the repo from github into my PC and then installed all the required things..as per video. Then I also changed the global and tailwindconfig.js as per the video after shadcn init. and also made env.local and all the API needed. so after doing all the steps from video. I have not yet replayed the website or hosted it on server. It is still in my PC. After running it , I got this error. Screenshot (242) and this in the vs code terminal: ▲ Next.js 14.0.3

⚠ Invalid next.config.js options detected: ⚠ Expected object, received boolean at "experimental.serverActions" ⚠ See more info here: https://nextjs.org/docs/messages/invalid-next-config ⚠ Server Actions are available by default now, experimental.serverActions option can be safely removed. ✓ Ready in 4.6s ○ Compiling /middleware ... ✓ Compiled /middleware in 563ms (273 modules) ○ Compiling / ... ✓ Compiled / in 3.6s (1105 modules) (node:9496) [DEP0040] DeprecationWarning: The punycode module is deprecated. Please use a userland alternative instead. (Use node --trace-deprecation ... to show where the warning was created) ✓ Compiled in 1599ms (404 modules) MongoDB connected MongoServerError: bad auth : authentication failed at Connection.onMessage (C:\Users\karti\Documents\MY_PROJECTS\test1\node_modules\mongoose\node_modules\mongodb\lib\cmap\connection.js:202:26) at MessageStream. (C:\Users\karti\Documents\MY_PROJECTS\test1\node_modules\mongoose\node_modules\mongodb\lib\cmap\connection.js:61:60) at MessageStream.emit (node:events:519:28) at processIncomingData (C:\Users\karti\Documents\MY_PROJECTS\test1\node_modules\mongoose\node_modules\mongodb\lib\cmap\message_stream.js:124:16) at MessageStream._write (C:\Users\karti\Documents\MY_PROJECTS\test1\node_modules\mongoose\node_modules\mongodb\lib\cmap\message_stream.js:33:9) at writeOrBuffer (node:internal/streams/writable:564:12) at _write (node:internal/streams/writable:493:10) at Writable.write (node:internal/streams/writable:502:10) at TLSSocket.ondata (node:internal/streams/readable:1007:22) at TLSSocket.emit (node:events:519:28) at addChunk (node:internal/streams/readable:559:12) at readableAddChunkPushByteMode (node:internal/streams/readable:510:3) at Readable.push (node:internal/streams/readable:390:5) at TLSWrap.onStreamRead (node:internal/stream_base_commons:190:23) at TLSWrap.callbackTrampoline (node:internal/async_hooks:130:17) { ok: 0, code: 8000, codeName: 'AtlasError', connectionGeneration: 0, [Symbol(errorLabels)]: Set(2) { 'HandshakeError', 'ResetPool' } } Error fetching users: MongooseError: Operation users.countDocuments() buffering timed out after 10000ms at Timeout. (C:\Users\karti\Documents\MY_PROJECTS\test1\node_modules\mongoose\lib\drivers\node-mongodb-native\collection.js:186:23) at listOnTimeout (node:internal/timers:573:17) at process.processTimers (node:internal/timers:514:7) ⨯ Internal error: MongooseError: Operation users.countDocuments() buffering timed out after 10000ms at Timeout. (C:\Users\karti\Documents\MY_PROJECTS\test1\node_modules\mongoose\lib\drivers\node-mongodb-native\collection.js:186:23) at listOnTimeout (node:internal/timers:573:17) at process.processTimers (node:internal/timers:514:7) ⨯ Internal error: MongooseError: Operation users.countDocuments() buffering timed out after 10000ms at Timeout. (C:\Users\karti\Documents\MY_PROJECTS\test1\node_modules\mongoose\lib\drivers\node-mongodb-native\collection.js:186:23) at listOnTimeout (node:internal/timers:573:17) at process.processTimers (node:internal/timers:514:7) digest: "3882132667" ⨯ lib\actions\user.actions.ts (21:10) @ fetchUser ⨯ Error: Failed to fetch user: Operation users.findOne() buffering timed out after 10000ms at fetchUser (./lib/actions/user.actions.ts:36:15) at async Home (page.tsx:24:22) 19 | }); 20 | } catch (error: any) {

21 | throw new Error(Failed to fetch user: ${error.message});
| ^
22 | }
23 | }
24 |
⨯ lib\actions\user.actions.ts (21:10) @ fetchUser
⨯ Error: Failed to fetch user: Operation users.findOne() buffering timed out after 10000ms
at fetchUser (./lib/actions/user.actions.ts:36:15)
at async Home (page.tsx:24:22)
digest: "2116173799"
19 | });
20 | } catch (error: any) {
21 | throw new Error(Failed to fetch user: ${error.message});
| ^
22 | }
23 | }
24 |

Please do help me with this

just apply this configuration and it will work

"

/** @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;
"

@waltodd
Copy link

waltodd commented Jan 21, 2024

Good day guys, please this is what I got after deployment, please I need help Screenshot_20240104-182654

fix-error

@stmoerman
Copy link

Just a note for people having issues with the next config file.

If you need ECMAScript modules, you can use next.config.mjs and with Next.js 14 you no longer need to mark server actions as experimental:

/** @type {import('next').NextConfig} */
const nextConfig = {
  experimental: {
    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",
      },
    ],
  },
};

module.exports = nextConfig;

@Victorosayame
Copy link

Screenshot (66)
i keep getting this error and it doesn't connect to db,please can some one help me out

@Sarthak412
Copy link

Screenshot (66) i keep getting this error and it doesn't connect to db,please can some one help me out

I got this error because I was not connecting to my mongodb check if you are connecting to mongoDB before performing the findOneAndUpdate() operation.

@igor-hara
Copy link

Did you find solution for this?
now i'm stuck with it :D

@Victorosayame
Copy link

Victorosayame commented Jan 26, 2024 via email

@kravitexx
Copy link

Hi i need help about this, I made a an organization in this but it doesnt show up on communitites tab as well as suggested community area. I m making the threads clone app using the video from javascriptmastery. I have installed all the necessary dependencies. if you are available please do help me with this
Screenshot 2024-01-31 011956

@kravitexx
Copy link

The next js version has something to do with the error?

i think that too...i wnted to make this project for my clg exam and here i am stuck now

@vipul7447
Copy link

Screenshot (48)
Can anyone please help me out...!!

@vipul7447
Copy link

Screenshot (49)
Hii everyone, can anyone resolve this issue ?

@Fy50167
Copy link

Fy50167 commented Mar 2, 2024

When I try to create a user, I receive this error:
Failed to simulate callback for file. Is your webhook configured correctly? 9aae6779-a988-418c-a87f-f0437ef3e149-hgmxm8.jpg

I've already tried adding 'api/uploadthing' to my public routes in my middleware. I also tried adding the routes to both of the exact files (api/uploading/route, api/uploadthing/core) just in case that might help but no luck. It seems like the error occurs randomly, I've been able to create three users so far but every other time I've received the error. Unfortunately no idea what the common thread between those three instances was.

@kkunal026
Copy link

My profile page is not working. When I make the profile folder -> id -> page.tsx and run it, it says "404-not found" and the profile page is not opening what should i do ?

311332378-207be27b-a94e-40dc-939d-8807f730d7c0
311332488-925de9a3-7d56-4937-9889-73b98666fe92
311416447-71da7633-a472-4c59-b490-8ef378ed1bba

@BestOlumese
Copy link

Screenshot (48) Can anyone please help me out...!!

bro check where you define the type of the props in your tsx it is very important

@TalhaAbbas55
Copy link

terminal loop

When user selects a new image in onboarding and fill out the rest of the form, after hitting Submit, it will go into an infinite loop saying "[UT] Call unsuccessful after 8 tries. Retrying in 64 seconds..." if user does not select image and fill out the rest of the form, it will go through normally... before the project was working fine with images but it's broken now. If anyone has a fix would be appreciated!

getting same error @adrianhajdin please help

@Atharvasurya
Copy link

Screenshot_4

@Atharvasurya
Copy link

Screenshot_5
Having this error in the terminal, while submitting the onboarding page and changes are not reflecting in 'test.users in mongodb'

@MaxT6
Copy link

MaxT6 commented Apr 4, 2024

Just a note for people having issues with the next config file.

If you need ECMAScript modules, you can use next.config.mjs and with Next.js 14 you no longer need to mark server actions as experimental:

/** @type {import('next').NextConfig} */
const nextConfig = {
  experimental: {
    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",
      },
    ],
  },
};

module.exports = nextConfig;

Thank you to stmoerman! This solved my next.config.mjs issue, however I had to make one small change and switched module.exports = nextConfig; to export default nextConfig;

@orlando-guy
Copy link

orlando-guy commented Apr 7, 2024

Hi everyone, I hope you have a good day, I'm stuck on this, it's what I got after deployment on Vercel, please I need help.
Screen Shot 2024-04-07 at 12 56 40

@luidev0
Copy link

luidev0 commented Apr 25, 2024

I've encountered this problem. Have anyone fixed it?
image

@designsbyluis
Copy link

I've encountered this problem. Have anyone fixed it? image

if you hover over it, it will say to use file instead of fileUrl.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment