├── app ├── favicon.ico ├── (auth) │ ├── sign-in │ │ └── [[...sign-in]] │ │ │ └── page.tsx │ ├── sign-up │ │ └── [[...sign-up]] │ │ │ └── page.tsx │ ├── layout.tsx │ └── onboarding │ │ └── page.tsx ├── api │ ├── uploadthing │ │ ├── route.ts │ │ └── core.ts │ └── webhook │ │ └── clerk │ │ └── route.ts ├── (root) │ ├── create-thread │ │ └── page.tsx │ ├── page.tsx │ ├── layout.tsx │ ├── search │ │ └── page.tsx │ ├── activity │ │ └── page.tsx │ ├── communities │ │ ├── page.tsx │ │ └── [id] │ │ │ └── page.tsx │ ├── thread │ │ └── [id] │ │ │ └── page.tsx │ └── profile │ │ └── [id] │ │ └── page.tsx └── globals.css ├── postcss.config.js ├── lib ├── validations │ ├── thread.ts │ └── user.ts ├── uploadthing.ts ├── models │ ├── thread.model.ts │ ├── user.model.ts │ └── community.model.ts ├── mongoose.ts ├── utils.ts └── actions │ ├── user.actions.ts │ ├── thread.actions.ts │ └── community.actions.ts ├── components.json ├── public ├── assets │ ├── heart-filled.svg │ ├── search-gray.svg │ ├── search.svg │ ├── more.svg │ ├── tag.svg │ ├── delete.svg │ ├── user.svg │ ├── reply.svg │ ├── heart.svg │ ├── heart-gray.svg │ ├── repost.svg │ ├── logo.svg │ ├── share.svg │ ├── home.svg │ ├── create.svg │ ├── members.svg │ ├── profile.svg │ ├── community.svg │ ├── logout.svg │ ├── edit.svg │ └── request.svg ├── vercel.svg └── next.svg ├── .gitignore ├── middleware.ts ├── tsconfig.json ├── components ├── ui │ ├── label.tsx │ ├── textarea.tsx │ ├── input.tsx │ ├── tabs.tsx │ ├── button.tsx │ └── form.tsx ├── forms │ ├── DeleteThread.tsx │ ├── PostThread.tsx │ ├── Comment.tsx │ └── AccountProfile.tsx ├── shared │ ├── Topbar.tsx │ ├── Searchbar.tsx │ ├── Bottombar.tsx │ ├── Pagination.tsx │ ├── ProfileHeader.tsx │ ├── LeftSidebar.tsx │ ├── ThreadsTab.tsx │ └── RightSidebar.tsx └── cards │ ├── UserCard.tsx │ ├── CommunityCard.tsx │ └── ThreadCard.tsx ├── next.config.js ├── constants └── index.js ├── package.json ├── README.md └── tailwind.config.js /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/omjaju18/Threads-Clone/HEAD/app/favicon.ico -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /app/(auth)/sign-in/[[...sign-in]]/page.tsx: -------------------------------------------------------------------------------- 1 | import { SignIn } from "@clerk/nextjs"; 2 | 3 | export default function Page() { 4 | return ; 5 | } -------------------------------------------------------------------------------- /app/(auth)/sign-up/[[...sign-up]]/page.tsx: -------------------------------------------------------------------------------- 1 | import { SignUp } from "@clerk/nextjs"; 2 | 3 | export default function Page() { 4 | return ; 5 | } -------------------------------------------------------------------------------- /lib/validations/thread.ts: -------------------------------------------------------------------------------- 1 | import * as z from "zod"; 2 | 3 | export const ThreadValidation = z.object({ 4 | thread: z.string().nonempty().min(3, { message: "Minimum 3 characters." }), 5 | accountId: z.string(), 6 | }); 7 | 8 | export const CommentValidation = z.object({ 9 | thread: z.string().nonempty().min(3, { message: "Minimum 3 characters." }), 10 | }); 11 | -------------------------------------------------------------------------------- /lib/uploadthing.ts: -------------------------------------------------------------------------------- 1 | // Resource: https://docs.uploadthing.com/api-reference/react#generatereacthelpers 2 | // Copy paste (be careful with imports) 3 | 4 | import { generateReactHelpers } from "@uploadthing/react/hooks"; 5 | 6 | import type { OurFileRouter } from "@/app/api/uploadthing/core"; 7 | 8 | export const { useUploadThing, uploadFiles } = 9 | generateReactHelpers(); 10 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "default", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.js", 8 | "css": "app/globals.css", 9 | "baseColor": "slate", 10 | "cssVariables": false 11 | }, 12 | "aliases": { 13 | "components": "@/components", 14 | "utils": "@/lib/utils" 15 | } 16 | } -------------------------------------------------------------------------------- /public/assets/heart-filled.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /app/api/uploadthing/route.ts: -------------------------------------------------------------------------------- 1 | // Resource: https://docs.uploadthing.com/nextjs/appdir#create-a-nextjs-api-route-using-the-filerouter 2 | // Copy paste (be careful with imports) 3 | 4 | import { createNextRouteHandler } from "uploadthing/next"; 5 | 6 | import { OurFileRouter } from "./core"; 7 | 8 | // Export routes for Next App Router 9 | export const { GET, POST } = createNextRouteHandler({ 10 | router: OurFileRouter, 11 | }); 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env*.local 29 | 30 | # vercel 31 | .vercel 32 | 33 | # typescript 34 | *.tsbuildinfo 35 | next-env.d.ts 36 | -------------------------------------------------------------------------------- /middleware.ts: -------------------------------------------------------------------------------- 1 | import { authMiddleware } from "@clerk/nextjs"; 2 | 3 | // This example protects all routes including api/trpc routes 4 | // Please edit this to allow other routes to be public as needed. 5 | // See https://clerk.com/docs/nextjs/middleware for more information about configuring your middleware 6 | export default authMiddleware({ 7 | publicRoutes: ["/", "/api/webhook/clerk"], 8 | ignoredRoutes: ["/api/webhook/clerk"], 9 | }); 10 | 11 | export const config = { 12 | matcher: ["/((?!.*\\..*|_next).*)", "/", "/(api|trpc)(.*)"], 13 | }; 14 | -------------------------------------------------------------------------------- /lib/validations/user.ts: -------------------------------------------------------------------------------- 1 | import * as z from "zod"; 2 | 3 | export const UserValidation = z.object({ 4 | profile_photo: z.string().url().nonempty(), 5 | name: z 6 | .string() 7 | .min(3, { message: "Minimum 3 characters." }) 8 | .max(30, { message: "Maximum 30 caracters." }), 9 | username: z 10 | .string() 11 | .min(3, { message: "Minimum 3 characters." }) 12 | .max(30, { message: "Maximum 30 caracters." }), 13 | bio: z 14 | .string() 15 | .min(3, { message: "Minimum 3 characters." }) 16 | .max(1000, { message: "Maximum 1000 caracters." }), 17 | }); 18 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/assets/search-gray.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /public/assets/search.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /app/(root)/create-thread/page.tsx: -------------------------------------------------------------------------------- 1 | import { currentUser } from "@clerk/nextjs"; 2 | import { redirect } from "next/navigation"; 3 | 4 | import PostThread from "@/components/forms/PostThread"; 5 | import { fetchUser } from "@/lib/actions/user.actions"; 6 | 7 | async function Page() { 8 | const user = await currentUser(); 9 | if (!user) return null; 10 | 11 | // fetch organization list created by user 12 | const userInfo = await fetchUser(user.id); 13 | if (!userInfo?.onboarded) redirect("/onboarding"); 14 | 15 | return ( 16 | <> 17 |

Create Thread

18 | 19 | 20 | 21 | ); 22 | } 23 | 24 | export default Page; -------------------------------------------------------------------------------- /public/assets/more.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /lib/models/thread.model.ts: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose"; 2 | 3 | const threadSchema = new mongoose.Schema({ 4 | text: { 5 | type: String, 6 | required: true, 7 | }, 8 | author: { 9 | type: mongoose.Schema.Types.ObjectId, 10 | ref: "User", 11 | required: true, 12 | }, 13 | community: { 14 | type: mongoose.Schema.Types.ObjectId, 15 | ref: "Community", 16 | }, 17 | createdAt: { 18 | type: Date, 19 | default: Date.now, 20 | }, 21 | parentId: { 22 | type: String, 23 | }, 24 | children: [ 25 | { 26 | type: mongoose.Schema.Types.ObjectId, 27 | ref: "Thread", 28 | }, 29 | ], 30 | }); 31 | 32 | const Thread = mongoose.models.Thread || mongoose.model("Thread", threadSchema); 33 | 34 | export default Thread; 35 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "bundler", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true, 17 | "plugins": [ 18 | { 19 | "name": "next" 20 | } 21 | ], 22 | "paths": { 23 | "@/*": ["./*"] 24 | } 25 | }, 26 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", "tailwind.config.js"], 27 | "exclude": ["node_modules"] 28 | } 29 | -------------------------------------------------------------------------------- /lib/models/user.model.ts: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose"; 2 | 3 | const userSchema = new mongoose.Schema({ 4 | id: { 5 | type: String, 6 | required: true, 7 | }, 8 | username: { 9 | type: String, 10 | unique: true, 11 | required: true, 12 | }, 13 | name: { 14 | type: String, 15 | required: true, 16 | }, 17 | image: String, 18 | bio: String, 19 | threads: [ 20 | { 21 | type: mongoose.Schema.Types.ObjectId, 22 | ref: "Thread", 23 | }, 24 | ], 25 | onboarded: { 26 | type: Boolean, 27 | default: false, 28 | }, 29 | communities: [ 30 | { 31 | type: mongoose.Schema.Types.ObjectId, 32 | ref: "Community", 33 | }, 34 | ], 35 | }); 36 | 37 | const User = mongoose.models.User || mongoose.model("User", userSchema); 38 | 39 | export default User; 40 | -------------------------------------------------------------------------------- /lib/mongoose.ts: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose"; 2 | 3 | let isConnected = false; // Variable to track the connection status 4 | 5 | export const connectToDB = async () => { 6 | // Set strict query mode for Mongoose to prevent unknown field queries. 7 | mongoose.set("strictQuery", true); 8 | 9 | if (!process.env.MONGODB_URL) return console.log("Missing MongoDB URL"); 10 | 11 | // If the connection is already established, return without creating a new connection. 12 | if (isConnected) { 13 | console.log("MongoDB connection already established"); 14 | return; 15 | } 16 | 17 | try { 18 | await mongoose.connect(process.env.MONGODB_URL); 19 | 20 | isConnected = true; // Set the connection status to true 21 | console.log("MongoDB connected"); 22 | } catch (error) { 23 | console.log(error); 24 | } 25 | }; 26 | -------------------------------------------------------------------------------- /components/ui/label.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as LabelPrimitive from "@radix-ui/react-label" 5 | import { cva, type VariantProps } from "class-variance-authority" 6 | 7 | import { cn } from "@/lib/utils" 8 | 9 | const labelVariants = cva( 10 | "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" 11 | ) 12 | 13 | const Label = React.forwardRef< 14 | React.ElementRef, 15 | React.ComponentPropsWithoutRef & 16 | VariantProps 17 | >(({ className, ...props }, ref) => ( 18 | 23 | )) 24 | Label.displayName = LabelPrimitive.Root.displayName 25 | 26 | export { Label } 27 | -------------------------------------------------------------------------------- /lib/models/community.model.ts: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose"; 2 | 3 | const communitySchema = new mongoose.Schema({ 4 | id: { 5 | type: String, 6 | required: true, 7 | }, 8 | username: { 9 | type: String, 10 | unique: true, 11 | required: true, 12 | }, 13 | name: { 14 | type: String, 15 | required: true, 16 | }, 17 | image: String, 18 | bio: String, 19 | createdBy: { 20 | type: mongoose.Schema.Types.ObjectId, 21 | ref: "User", 22 | }, 23 | threads: [ 24 | { 25 | type: mongoose.Schema.Types.ObjectId, 26 | ref: "Thread", 27 | }, 28 | ], 29 | members: [ 30 | { 31 | type: mongoose.Schema.Types.ObjectId, 32 | ref: "User", 33 | }, 34 | ], 35 | }); 36 | 37 | const Community = 38 | mongoose.models.Community || mongoose.model("Community", communitySchema); 39 | 40 | export default Community; 41 | -------------------------------------------------------------------------------- /app/(auth)/layout.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import type { Metadata } from "next"; 3 | import { Inter } from "next/font/google"; 4 | import { ClerkProvider } from "@clerk/nextjs"; 5 | 6 | 7 | import "../globals.css"; 8 | 9 | const inter = Inter({ subsets: ["latin"] }); 10 | 11 | export const metadata: Metadata = { 12 | title: "Threads", 13 | description: "Generated by create next app", 14 | }; 15 | 16 | export default function RootLayout({ 17 | children, 18 | }: { 19 | children: React.ReactNode; 20 | }) { 21 | return ( 22 | 23 | 24 | 25 |
26 | {children} 27 |
28 | 29 | 30 |
31 | ); 32 | } -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | typescript: { 4 | ignoreBuildErrors: true, 5 | }, 6 | experimental: { 7 | serverActions: true, 8 | serverComponentsExternalPackages: ["mongoose"], 9 | }, 10 | eslint: { 11 | // Warning: This allows production builds to successfully complete even if 12 | // your project has ESLint errors. 13 | ignoreDuringBuilds: true, 14 | }, 15 | images: { 16 | remotePatterns: [ 17 | { 18 | protocol: "https", 19 | hostname: "img.clerk.com", 20 | }, 21 | { 22 | protocol: "https", 23 | hostname: "images.clerk.dev", 24 | }, 25 | { 26 | protocol: "https", 27 | hostname: "uploadthing.com", 28 | }, 29 | { 30 | protocol: "https", 31 | hostname: "placehold.co", 32 | }, 33 | ], 34 | }, 35 | }; 36 | 37 | module.exports = nextConfig; 38 | -------------------------------------------------------------------------------- /public/assets/tag.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /components/ui/textarea.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | export interface TextareaProps 6 | extends React.TextareaHTMLAttributes {} 7 | 8 | const Textarea = React.forwardRef( 9 | ({ className, ...props }, ref) => { 10 | return ( 11 |