Skip to content

Instantly share code, notes, and snippets.

@adrianhajdin
Last active June 8, 2024 07:46
Show Gist options
  • 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}`;
}
}
@NaydenBanchevski
Copy link

I got the same issue today in the AccountProfile. Property 'fileUrl' does not exist on type 'ClientUploadedFileData<{ uploadedBy: string; }>'.

Use file instead of fileUrl. Its deprecated.

I get the same error.

Your issue is most likely in your uploadthing core.ts on line 12 it should read media: f({ image: { maxFileSize: "4MB", maxFileCount: 1 } })

I appreciate your response. Unfortunately I still get the same error : Property 'fileUrl' does not exist on type 'ClientUploadedFileData<{ uploadedBy: string; }>'.ts(2339)

@Ayushhgupta39
Copy link

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

how was your issue solved?

@designsbyluis
Copy link

designsbyluis commented May 29, 2024

I got the same issue today in the AccountProfile. Property 'fileUrl' does not exist on type 'ClientUploadedFileData<{ uploadedBy: string; }>'.

Use file instead of fileUrl. Its deprecated.

I get the same error.

Your issue is most likely in your uploadthing core.ts on line 12 it should read media: f({ image: { maxFileSize: "4MB", maxFileCount: 1 } })

I appreciate your response. Unfortunately I still get the same error : Property 'fileUrl' does not exist on type 'ClientUploadedFileData<{ uploadedBy: string; }>'.ts(2339)

Sorry meant "url", try (.url) instead and remove File. It should fix your issue.

@RichValero
Copy link

I got the same issue today in the AccountProfile. Property 'fileUrl' does not exist on type 'ClientUploadedFileData<{ uploadedBy: string; }>'.

Use file instead of fileUrl. Its deprecated.

I get the same error.

Your issue is most likely in your uploadthing core.ts on line 12 it should read media: f({ image: { maxFileSize: "4MB", maxFileCount: 1 } })

I appreciate your response. Unfortunately I still get the same error : Property 'fileUrl' does not exist on type 'ClientUploadedFileData<{ uploadedBy: string; }>'.ts(2339)

Sorry meant "url", try (.url) instead and remove File. It should fix your issue.

That worked for me. Thank you!

@Ayushhgupta39
Copy link

Hey guys, I just restarted my dev server today and getting this error:
image

Everything was working fine last time I worked on this, but suddenly getting this. Can anyone please help?

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