├── .github ├── assets │ ├── README.md │ ├── home.png │ ├── search.png │ ├── activity.png │ ├── explore.png │ ├── sign-in.png │ ├── sign-up.png │ ├── communities.png │ ├── my-profile.png │ ├── onboarding.png │ ├── thread-page.png │ ├── create-thread.png │ ├── edit-profile.png │ ├── user-profile.png │ ├── community-profile.png │ ├── create-organization.png │ ├── thread-likes-page.png │ └── my-profile-followers-tab.png └── FUNDING.yml ├── 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 │ ├── edit-thread │ │ └── [id] │ │ │ └── page.tsx │ ├── profile │ │ ├── edit │ │ │ └── page.tsx │ │ └── [id] │ │ │ └── page.tsx │ ├── layout.tsx │ ├── search │ │ └── page.tsx │ ├── communities │ │ ├── page.tsx │ │ └── [id] │ │ │ └── page.tsx │ ├── page.tsx │ ├── explore │ │ └── page.tsx │ ├── thread │ │ ├── reactions │ │ │ └── [id] │ │ │ │ └── page.tsx │ │ └── [id] │ │ │ └── page.tsx │ └── activity │ │ └── page.tsx └── globals.css ├── postcss.config.js ├── lib ├── validations │ ├── thread.ts │ └── user.ts ├── uploadthing.ts ├── mongoose.ts ├── models │ ├── thread.model.ts │ ├── community.model.ts │ └── user.model.ts ├── utils.ts └── actions │ ├── community.actions.ts │ └── user.actions.ts ├── components.json ├── public ├── assets │ ├── heart-filled.svg │ ├── search-gray.svg │ ├── search.svg │ ├── more.svg │ ├── tag.svg │ ├── delete.svg │ ├── delete-purple.svg │ ├── user.svg │ ├── reply.svg │ ├── heart.svg │ ├── followers.svg │ ├── following.svg │ ├── heart-gray.svg │ ├── unfollow.svg │ ├── logo.svg │ ├── repost.svg │ ├── follow.svg │ ├── share.svg │ ├── home.svg │ ├── create.svg │ ├── explore.svg │ ├── members.svg │ ├── profile.svg │ ├── community.svg │ ├── logout.svg │ ├── edit.svg │ └── request.svg ├── vercel.svg └── next.svg ├── .gitignore ├── middleware.ts ├── components ├── atoms │ ├── EditThread.tsx │ ├── ReactThread.tsx │ └── FollowUser.tsx ├── ui │ ├── label.tsx │ ├── textarea.tsx │ ├── input.tsx │ ├── tabs.tsx │ ├── button.tsx │ └── form.tsx ├── forms │ ├── DeleteThread.tsx │ ├── Comment.tsx │ ├── PostThread.tsx │ └── AccountProfile.tsx ├── cards │ ├── UserCard.tsx │ ├── CommunityCard.tsx │ └── ThreadCard.tsx └── shared │ ├── Bottombar.tsx │ ├── Topbar.tsx │ ├── Searchbar.tsx │ ├── Pagination.tsx │ ├── LeftSidebar.tsx │ ├── ProfileHeader.tsx │ ├── RightSidebar.tsx │ └── ThreadsTab.tsx ├── next.config.js ├── tsconfig.json ├── LICENSE ├── package.json ├── constants └── index.js ├── tailwind.config.js └── tailwind.config.ts /.github/assets/README.md: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ladunjexa/nextjs13-threads/HEAD/app/favicon.ico -------------------------------------------------------------------------------- /.github/assets/home.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ladunjexa/nextjs13-threads/HEAD/.github/assets/home.png -------------------------------------------------------------------------------- /.github/assets/search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ladunjexa/nextjs13-threads/HEAD/.github/assets/search.png -------------------------------------------------------------------------------- /.github/assets/activity.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ladunjexa/nextjs13-threads/HEAD/.github/assets/activity.png -------------------------------------------------------------------------------- /.github/assets/explore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ladunjexa/nextjs13-threads/HEAD/.github/assets/explore.png -------------------------------------------------------------------------------- /.github/assets/sign-in.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ladunjexa/nextjs13-threads/HEAD/.github/assets/sign-in.png -------------------------------------------------------------------------------- /.github/assets/sign-up.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ladunjexa/nextjs13-threads/HEAD/.github/assets/sign-up.png -------------------------------------------------------------------------------- /.github/assets/communities.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ladunjexa/nextjs13-threads/HEAD/.github/assets/communities.png -------------------------------------------------------------------------------- /.github/assets/my-profile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ladunjexa/nextjs13-threads/HEAD/.github/assets/my-profile.png -------------------------------------------------------------------------------- /.github/assets/onboarding.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ladunjexa/nextjs13-threads/HEAD/.github/assets/onboarding.png -------------------------------------------------------------------------------- /.github/assets/thread-page.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ladunjexa/nextjs13-threads/HEAD/.github/assets/thread-page.png -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /.github/assets/create-thread.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ladunjexa/nextjs13-threads/HEAD/.github/assets/create-thread.png -------------------------------------------------------------------------------- /.github/assets/edit-profile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ladunjexa/nextjs13-threads/HEAD/.github/assets/edit-profile.png -------------------------------------------------------------------------------- /.github/assets/user-profile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ladunjexa/nextjs13-threads/HEAD/.github/assets/user-profile.png -------------------------------------------------------------------------------- /.github/assets/community-profile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ladunjexa/nextjs13-threads/HEAD/.github/assets/community-profile.png -------------------------------------------------------------------------------- /.github/assets/create-organization.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ladunjexa/nextjs13-threads/HEAD/.github/assets/create-organization.png -------------------------------------------------------------------------------- /.github/assets/thread-likes-page.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ladunjexa/nextjs13-threads/HEAD/.github/assets/thread-likes-page.png -------------------------------------------------------------------------------- /.github/assets/my-profile-followers-tab.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ladunjexa/nextjs13-threads/HEAD/.github/assets/my-profile-followers-tab.png -------------------------------------------------------------------------------- /app/(auth)/sign-in/[[...sign-in]]/page.tsx: -------------------------------------------------------------------------------- 1 | import { SignIn } from "@clerk/nextjs"; 2 | 3 | export default function Page() { 4 | return ; 5 | } 6 | -------------------------------------------------------------------------------- /app/(auth)/sign-up/[[...sign-up]]/page.tsx: -------------------------------------------------------------------------------- 1 | import { SignUp } from "@clerk/nextjs"; 2 | 3 | export default function Page() { 4 | return ; 5 | } 6 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /lib/validations/user.ts: -------------------------------------------------------------------------------- 1 | import * as z from "zod"; 2 | 3 | export const UserValidation = z.object({ 4 | profile_photo: z.string().url().nonempty(), 5 | name: z 6 | .string() 7 | .min(3, { message: "Minimum 3 characters." }) 8 | .max(30, { message: "Maximum 30 caracters." }), 9 | username: z 10 | .string() 11 | .min(3, { message: "Minimum 3 characters." }) 12 | .max(30, { message: "Maximum 30 caracters." }), 13 | bio: z 14 | .string() 15 | .min(3, { message: "Minimum 3 characters." }) 16 | .max(1000, { message: "Maximum 1000 caracters." }), 17 | }); 18 | -------------------------------------------------------------------------------- /middleware.ts: -------------------------------------------------------------------------------- 1 | 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 | publicRoutes: [".", "/api/webhook/clerk"], 9 | ignoredRoutes: ["/api/webhook/clerk"], 10 | }); 11 | 12 | export const config = { 13 | matcher: ["/((?!.*\\..*|_next).*)", "/", "/(api|trpc)(.*)"], 14 | }; -------------------------------------------------------------------------------- /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; 25 | -------------------------------------------------------------------------------- /components/atoms/EditThread.tsx: -------------------------------------------------------------------------------- 1 | import Image from "next/image"; 2 | import Link from "next/link"; 3 | import React from "react"; 4 | 5 | interface Props { 6 | threadId: string; 7 | currentUserId: string; 8 | authorId: string; 9 | } 10 | 11 | const EditThread = ({ threadId, currentUserId, authorId }: Props) => { 12 | if (currentUserId !== authorId) return null; 13 | 14 | return ( 15 | 16 | edit thread 23 | 24 | ); 25 | }; 26 | 27 | export default EditThread; 28 | -------------------------------------------------------------------------------- /public/assets/more.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /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 | images: { 11 | remotePatterns: [ 12 | { 13 | protocol: "https", 14 | hostname: "img.clerk.com", 15 | }, 16 | { 17 | protocol: "https", 18 | hostname: "images.clerk.dev", 19 | }, 20 | { 21 | protocol: "https", 22 | hostname: "uploadthing.com", 23 | }, 24 | { 25 | protocol: "https", 26 | hostname: "placehold.co", 27 | }, 28 | ], 29 | }, 30 | }; 31 | 32 | module.exports = nextConfig; 33 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: ladunjexa 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 13 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 14 | -------------------------------------------------------------------------------- /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": [ 27 | "next-env.d.ts", 28 | "**/*.ts", 29 | "**/*.tsx", 30 | ".next/types/**/*.ts", 31 | "constants/index.js" 32 | ], 33 | "exclude": ["node_modules"] 34 | } 35 | -------------------------------------------------------------------------------- /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/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("Already connected to MongoDB"); 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("Connected to MongoDB"); 22 | } catch (error) { 23 | console.log("Error connecting to MongoDB"); 24 | console.log(error); 25 | } 26 | }; 27 | -------------------------------------------------------------------------------- /public/assets/tag.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/(root)/edit-thread/[id]/page.tsx: -------------------------------------------------------------------------------- 1 | import PostThread from "@/components/forms/PostThread"; 2 | import { fetchThreadById } from "@/lib/actions/thread.actions"; 3 | import { fetchUser } from "@/lib/actions/user.actions"; 4 | import { currentUser } from "@clerk/nextjs"; 5 | import { redirect } from "next/navigation"; 6 | import React from "react"; 7 | 8 | const Page = async ({ params }: { params: { id: string } }) => { 9 | if (!params.id) return null; 10 | 11 | const user = await currentUser(); 12 | if (!user) return null; 13 | 14 | const userInfo = await fetchUser(user.id); 15 | if (!userInfo?.onboarded) redirect("/onboarding"); 16 | 17 | const thread = await fetchThreadById(params.id); 18 | 19 | return ( 20 | <> 21 |

Edit Thread

22 | 23 | 28 | 29 | ); 30 | }; 31 | 32 | export default Page; 33 | -------------------------------------------------------------------------------- /app/(auth)/layout.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import type { Metadata } from "next"; 3 | import { Inter } from "next/font/google"; 4 | import { ClerkProvider } from "@clerk/nextjs"; 5 | import { dark } from "@clerk/themes"; 6 | 7 | import "../globals.css"; 8 | 9 | export const metadata: Metadata = { 10 | title: "Threads", 11 | description: "A Next.js 13 Meta Threads Application clone", 12 | }; 13 | 14 | const inter = Inter({ subsets: ["latin"] }); 15 | 16 | export default function RootLayout({ 17 | children, 18 | }: { 19 | children: React.ReactNode; 20 | }) { 21 | return ( 22 | 27 | 28 | 29 |
30 | {children} 31 |
32 | 33 | 34 |
35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /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 |