├── .env.example ├── .gitignore ├── LICENSE ├── README.md ├── eslint.config.mjs ├── next.config.ts ├── package-lock.json ├── package.json ├── postcss.config.mjs ├── prisma ├── migrations │ ├── 20250117125553_init │ │ └── migration.sql │ ├── 20250117131400_delete_user_name │ │ └── migration.sql │ ├── 20250120154832_add_user_dates │ │ └── migration.sql │ ├── 20250121133759_add_img_height │ │ └── migration.sql │ └── migration_lock.toml ├── schema.prisma └── seed.ts ├── public ├── general │ ├── avatar.png │ ├── cover.jpg │ └── post.jpeg ├── icons │ ├── back.svg │ ├── bookmark.svg │ ├── community.svg │ ├── date.svg │ ├── emoji.svg │ ├── explore.svg │ ├── gif.svg │ ├── home.svg │ ├── image.svg │ ├── infoMore.svg │ ├── job.svg │ ├── location.svg │ ├── logo.svg │ ├── message.svg │ ├── more.svg │ ├── notification.svg │ ├── poll.svg │ ├── post.svg │ ├── profile.svg │ ├── schedule.svg │ └── userLocation.svg └── svg │ ├── arrow.svg │ ├── comment.svg │ ├── like.svg │ ├── original.svg │ ├── repost.svg │ ├── save.svg │ ├── share.svg │ ├── square.svg │ └── wide.svg ├── server.js ├── src ├── action.ts ├── app │ ├── (board) │ │ ├── @modal │ │ │ ├── compose │ │ │ │ └── post │ │ │ │ │ └── page.tsx │ │ │ └── default.tsx │ │ ├── [username] │ │ │ ├── page.tsx │ │ │ └── status │ │ │ │ └── [postId] │ │ │ │ └── page.tsx │ │ ├── layout.tsx │ │ └── page.tsx │ ├── api │ │ ├── posts │ │ │ └── route.ts │ │ └── webhooks │ │ │ └── clerk │ │ │ └── route.ts │ ├── favicon.ico │ ├── globals.css │ ├── layout.tsx │ ├── sign-in │ │ ├── [[...sign-in]] │ │ │ └── page.tsx │ │ └── layout.tsx │ └── sign-up │ │ └── [[...sign-up]] │ │ └── page.tsx ├── components │ ├── Comments.tsx │ ├── Feed.tsx │ ├── FollowButton.tsx │ ├── Image.tsx │ ├── ImageEditor.tsx │ ├── InfiniteFeed.tsx │ ├── LeftBar.tsx │ ├── Logout.tsx │ ├── Notification.tsx │ ├── PopularTags.tsx │ ├── Post.tsx │ ├── PostInfo.tsx │ ├── PostInteractions.tsx │ ├── Recommendations.tsx │ ├── RightBar.tsx │ ├── Search.tsx │ ├── Share.tsx │ ├── Socket.tsx │ └── Video.tsx ├── middleware.ts ├── prisma.ts ├── providers │ └── QueryProvider.tsx ├── socket.ts └── utils.ts ├── tailwind.config.ts └── tsconfig.json /.env.example: -------------------------------------------------------------------------------- 1 | NEXT_PUBLIC_PUBLIC_KEY= 2 | NEXT_PUBLIC_URL_ENDPOINT= 3 | PRIVATE_KEY= 4 | 5 | DATABASE_URL= 6 | 7 | NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY= 8 | CLERK_SECRET_KEY= 9 | SIGNING_SECRET= -------------------------------------------------------------------------------- /.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.* 7 | .yarn/* 8 | !.yarn/patches 9 | !.yarn/plugins 10 | !.yarn/releases 11 | !.yarn/versions 12 | 13 | # testing 14 | /coverage 15 | 16 | # next.js 17 | /.next/ 18 | /out/ 19 | 20 | # production 21 | /build 22 | 23 | # misc 24 | .DS_Store 25 | *.pem 26 | 27 | # debug 28 | npm-debug.log* 29 | yarn-debug.log* 30 | yarn-error.log* 31 | .pnpm-debug.log* 32 | 33 | # env files (can opt-in for committing if needed) 34 | .env 35 | 36 | # vercel 37 | .vercel 38 | 39 | # typescript 40 | *.tsbuildinfo 41 | next-env.d.ts 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Safak 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Getting Started 2 | 3 | First, run the development server: 4 | 5 | ```bash 6 | npm run dev 7 | # or 8 | yarn dev 9 | # or 10 | pnpm dev 11 | # or 12 | bun dev 13 | ``` 14 | 15 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 16 | 17 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import { dirname } from "path"; 2 | import { fileURLToPath } from "url"; 3 | import { FlatCompat } from "@eslint/eslintrc"; 4 | 5 | const __filename = fileURLToPath(import.meta.url); 6 | const __dirname = dirname(__filename); 7 | 8 | const compat = new FlatCompat({ 9 | baseDirectory: __dirname, 10 | }); 11 | 12 | const eslintConfig = [ 13 | ...compat.extends("next/core-web-vitals", "next/typescript"), 14 | { 15 | rules: { 16 | "no-unused-vars": "warn", 17 | "@typescript-eslint/no-unused-vars": ["warn", { argsIgnorePattern: "^_", varsIgnorePattern: "^_" }], 18 | }, 19 | }, 20 | ]; 21 | 22 | export default eslintConfig; 23 | -------------------------------------------------------------------------------- /next.config.ts: -------------------------------------------------------------------------------- 1 | import type { NextConfig } from "next"; 2 | 3 | const nextConfig: NextConfig = { 4 | /* config options here */ 5 | images: { 6 | remotePatterns: [ 7 | { 8 | protocol: "https", 9 | hostname: "ik.imagekit.io", 10 | port: "", 11 | }, 12 | ], 13 | }, 14 | experimental: { 15 | serverActions: { 16 | bodySizeLimit: '50mb', 17 | }, 18 | }, 19 | }; 20 | 21 | export default nextConfig; 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "xui", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "node server.js", 7 | "build": "next build", 8 | "start": "NODE_ENV=production node server.js", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@clerk/elements": "^0.22.9", 13 | "@clerk/nextjs": "^6.9.12", 14 | "@prisma/client": "^6.2.1", 15 | "@tanstack/react-query": "^5.64.2", 16 | "imagekit": "^6.0.0", 17 | "imagekitio-next": "^1.0.1", 18 | "next": "15.1.0", 19 | "prisma": "^6.2.1", 20 | "react": "^19.0.0", 21 | "react-dom": "^19.0.0", 22 | "react-infinite-scroll-component": "^6.1.0", 23 | "socket.io": "^4.8.1", 24 | "socket.io-client": "^4.8.1", 25 | "svix": "^1.45.1", 26 | "timeago.js": "^4.0.2", 27 | "ts-node": "^10.9.2", 28 | "uuid": "^11.0.5", 29 | "zod": "^3.24.1" 30 | }, 31 | "devDependencies": { 32 | "@eslint/eslintrc": "^3", 33 | "@types/node": "^20", 34 | "@types/react": "^19", 35 | "@types/react-dom": "^19", 36 | "eslint": "^9", 37 | "eslint-config-next": "15.1.0", 38 | "postcss": "^8", 39 | "tailwindcss": "^3.4.1", 40 | "typescript": "^5" 41 | }, 42 | "prisma": { 43 | "seed": "ts-node --compiler-options {\"module\":\"CommonJS\"} prisma/seed.ts" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /postcss.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('postcss-load-config').Config} */ 2 | const config = { 3 | plugins: { 4 | tailwindcss: {}, 5 | }, 6 | }; 7 | 8 | export default config; 9 | -------------------------------------------------------------------------------- /prisma/migrations/20250117125553_init/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE `User` ( 3 | `id` VARCHAR(191) NOT NULL, 4 | `email` VARCHAR(191) NOT NULL, 5 | `username` VARCHAR(191) NOT NULL, 6 | `displayName` VARCHAR(191) NULL, 7 | `name` VARCHAR(191) NULL, 8 | `bio` VARCHAR(191) NULL, 9 | `location` VARCHAR(191) NULL, 10 | `job` VARCHAR(191) NULL, 11 | `website` VARCHAR(191) NULL, 12 | `img` VARCHAR(191) NULL, 13 | `cover` VARCHAR(191) NULL, 14 | 15 | UNIQUE INDEX `User_email_key`(`email`), 16 | UNIQUE INDEX `User_username_key`(`username`), 17 | PRIMARY KEY (`id`) 18 | ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; 19 | 20 | -- CreateTable 21 | CREATE TABLE `Post` ( 22 | `id` INTEGER NOT NULL AUTO_INCREMENT, 23 | `createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), 24 | `updatedAt` DATETIME(3) NOT NULL, 25 | `desc` VARCHAR(255) NULL, 26 | `img` VARCHAR(191) NULL, 27 | `video` VARCHAR(191) NULL, 28 | `isSensitive` BOOLEAN NOT NULL DEFAULT false, 29 | `userId` VARCHAR(191) NOT NULL, 30 | `rePostId` INTEGER NULL, 31 | `parentPostId` INTEGER NULL, 32 | 33 | PRIMARY KEY (`id`) 34 | ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; 35 | 36 | -- CreateTable 37 | CREATE TABLE `Like` ( 38 | `id` INTEGER NOT NULL AUTO_INCREMENT, 39 | `createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), 40 | `userId` VARCHAR(191) NOT NULL, 41 | `postId` INTEGER NOT NULL, 42 | 43 | PRIMARY KEY (`id`) 44 | ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; 45 | 46 | -- CreateTable 47 | CREATE TABLE `SavedPosts` ( 48 | `id` INTEGER NOT NULL AUTO_INCREMENT, 49 | `createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), 50 | `userId` VARCHAR(191) NOT NULL, 51 | `postId` INTEGER NOT NULL, 52 | 53 | PRIMARY KEY (`id`) 54 | ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; 55 | 56 | -- CreateTable 57 | CREATE TABLE `Follow` ( 58 | `id` INTEGER NOT NULL AUTO_INCREMENT, 59 | `createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), 60 | `followerId` VARCHAR(191) NOT NULL, 61 | `followingId` VARCHAR(191) NOT NULL, 62 | 63 | PRIMARY KEY (`id`) 64 | ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; 65 | 66 | -- AddForeignKey 67 | ALTER TABLE `Post` ADD CONSTRAINT `Post_userId_fkey` FOREIGN KEY (`userId`) REFERENCES `User`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; 68 | 69 | -- AddForeignKey 70 | ALTER TABLE `Post` ADD CONSTRAINT `Post_rePostId_fkey` FOREIGN KEY (`rePostId`) REFERENCES `Post`(`id`) ON DELETE SET NULL ON UPDATE CASCADE; 71 | 72 | -- AddForeignKey 73 | ALTER TABLE `Post` ADD CONSTRAINT `Post_parentPostId_fkey` FOREIGN KEY (`parentPostId`) REFERENCES `Post`(`id`) ON DELETE SET NULL ON UPDATE CASCADE; 74 | 75 | -- AddForeignKey 76 | ALTER TABLE `Like` ADD CONSTRAINT `Like_userId_fkey` FOREIGN KEY (`userId`) REFERENCES `User`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; 77 | 78 | -- AddForeignKey 79 | ALTER TABLE `Like` ADD CONSTRAINT `Like_postId_fkey` FOREIGN KEY (`postId`) REFERENCES `Post`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; 80 | 81 | -- AddForeignKey 82 | ALTER TABLE `SavedPosts` ADD CONSTRAINT `SavedPosts_userId_fkey` FOREIGN KEY (`userId`) REFERENCES `User`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; 83 | 84 | -- AddForeignKey 85 | ALTER TABLE `SavedPosts` ADD CONSTRAINT `SavedPosts_postId_fkey` FOREIGN KEY (`postId`) REFERENCES `Post`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; 86 | 87 | -- AddForeignKey 88 | ALTER TABLE `Follow` ADD CONSTRAINT `Follow_followerId_fkey` FOREIGN KEY (`followerId`) REFERENCES `User`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; 89 | 90 | -- AddForeignKey 91 | ALTER TABLE `Follow` ADD CONSTRAINT `Follow_followingId_fkey` FOREIGN KEY (`followingId`) REFERENCES `User`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; 92 | -------------------------------------------------------------------------------- /prisma/migrations/20250117131400_delete_user_name/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - You are about to drop the column `name` on the `User` table. All the data in the column will be lost. 5 | 6 | */ 7 | -- AlterTable 8 | ALTER TABLE `User` DROP COLUMN `name`; 9 | -------------------------------------------------------------------------------- /prisma/migrations/20250120154832_add_user_dates/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - Added the required column `updatedAt` to the `User` table without a default value. This is not possible if the table is not empty. 5 | 6 | */ 7 | -- AlterTable 8 | ALTER TABLE `User` ADD COLUMN `createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), 9 | ADD COLUMN `updatedAt` DATETIME(3) NOT NULL; 10 | -------------------------------------------------------------------------------- /prisma/migrations/20250121133759_add_img_height/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE `Post` ADD COLUMN `imgHeight` INTEGER NULL; 3 | -------------------------------------------------------------------------------- /prisma/migrations/migration_lock.toml: -------------------------------------------------------------------------------- 1 | # Please do not edit this file manually 2 | # It should be added in your version-control system (e.g., Git) 3 | provider = "mysql" -------------------------------------------------------------------------------- /prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | generator client { 2 | provider = "prisma-client-js" 3 | } 4 | 5 | datasource db { 6 | provider = "mysql" 7 | url = env("DATABASE_URL") 8 | } 9 | 10 | model User { 11 | id String @id 12 | email String @unique 13 | username String @unique 14 | displayName String? 15 | bio String? 16 | location String? 17 | job String? 18 | website String? 19 | img String? 20 | cover String? 21 | createdAt DateTime @default(now()) 22 | updatedAt DateTime @updatedAt 23 | 24 | // RELATIONS 25 | posts Post[] 26 | 27 | likes Like[] 28 | 29 | saves SavedPosts[] 30 | 31 | followers Follow[] @relation("UserFollowers") 32 | followings Follow[] @relation("UserFollowings") 33 | } 34 | 35 | model Post { 36 | id Int @id @default(autoincrement()) 37 | createdAt DateTime @default(now()) 38 | updatedAt DateTime @updatedAt 39 | desc String? @db.VarChar(255) 40 | img String? 41 | imgHeight Int? 42 | video String? 43 | isSensitive Boolean @default(false) 44 | 45 | // RELATIONS 46 | user User @relation(fields: [userId], references: [id]) 47 | userId String 48 | 49 | rePostId Int? 50 | rePost Post? @relation("RePosts", fields: [rePostId], references: [id]) 51 | rePosts Post[] @relation("RePosts") 52 | 53 | parentPostId Int? 54 | parentPost Post? @relation("PostComments", fields: [parentPostId], references: [id]) 55 | comments Post[] @relation("PostComments") 56 | 57 | likes Like[] 58 | 59 | saves SavedPosts[] 60 | } 61 | 62 | model Like { 63 | id Int @id @default(autoincrement()) 64 | createdAt DateTime @default(now()) 65 | 66 | // RELATIONS 67 | userId String 68 | postId Int 69 | 70 | user User @relation(fields: [userId], references: [id]) 71 | post Post @relation(fields: [postId], references: [id]) 72 | } 73 | 74 | model SavedPosts { 75 | id Int @id @default(autoincrement()) 76 | createdAt DateTime @default(now()) 77 | 78 | // RELATIONS 79 | userId String 80 | postId Int 81 | 82 | user User @relation(fields: [userId], references: [id]) 83 | post Post @relation(fields: [postId], references: [id]) 84 | } 85 | 86 | model Follow { 87 | id Int @id @default(autoincrement()) 88 | createdAt DateTime @default(now()) 89 | 90 | // RELATIONS 91 | followerId String 92 | followingId String 93 | 94 | follower User @relation("UserFollowers", fields: [followerId], references: [id]) 95 | following User @relation("UserFollowings", fields: [followingId], references: [id]) 96 | } 97 | -------------------------------------------------------------------------------- /prisma/seed.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from '@prisma/client'; 2 | 3 | const prisma = new PrismaClient(); 4 | 5 | async function main() { 6 | // Create 5 users with unique details 7 | const users = []; 8 | for (let i = 1; i <= 5; i++) { 9 | const user = await prisma.user.create({ 10 | data: { 11 | id: `user${i}`, 12 | email: `user${i}@example.com`, 13 | username: `user${i}`, 14 | displayName: `User ${i}`, 15 | bio: `Hi I'm user${i}. Welcome to my profile!`, 16 | location: `USA`, 17 | job: `Developer`, 18 | website: `google.com`, 19 | }, 20 | }); 21 | users.push(user); 22 | } 23 | console.log(`${users.length} users created.`); 24 | 25 | // Create 5 posts for each user 26 | const posts = []; 27 | for (let i = 0; i < users.length; i++) { 28 | for (let j = 1; j <= 5; j++) { 29 | const post = await prisma.post.create({ 30 | data: { 31 | desc: `Post ${j} by ${users[i].username}`, 32 | userId: users[i].id, 33 | }, 34 | }); 35 | posts.push(post); 36 | } 37 | } 38 | console.log('Posts created.'); 39 | 40 | // Create some follows 41 | await prisma.follow.createMany({ 42 | data: [ 43 | { followerId: users[0].id, followingId: users[1].id }, 44 | { followerId: users[0].id, followingId: users[2].id }, 45 | { followerId: users[1].id, followingId: users[3].id }, 46 | { followerId: users[2].id, followingId: users[4].id }, 47 | { followerId: users[3].id, followingId: users[0].id }, 48 | ], 49 | }); 50 | console.log('Follows created.'); 51 | 52 | // Create some likes 53 | await prisma.like.createMany({ 54 | data: [ 55 | { userId: users[0].id, postId: posts[0].id }, 56 | { userId: users[1].id, postId: posts[1].id }, 57 | { userId: users[2].id, postId: posts[2].id }, 58 | { userId: users[3].id, postId: posts[3].id }, 59 | { userId: users[4].id, postId: posts[4].id }, 60 | ], 61 | }); 62 | console.log('Likes created.'); 63 | 64 | // Create some comments (each comment is a post linked to a parent post) 65 | const comments = []; 66 | for (let i = 0; i < posts.length; i++) { 67 | const comment = await prisma.post.create({ 68 | data: { 69 | desc: `Comment on Post ${posts[i].id} by ${users[(i + 1) % 5].username}`, 70 | userId: users[(i + 1) % 5].id, 71 | parentPostId: posts[i].id, // Linking the comment to the post 72 | }, 73 | }); 74 | comments.push(comment); 75 | } 76 | console.log('Comments created.'); 77 | 78 | // Create reposts using the Post model's rePostId 79 | const reposts = []; 80 | for (let i = 0; i < posts.length; i++) { 81 | const repost = await prisma.post.create({ 82 | data: { 83 | desc: `Repost of Post ${posts[i].id} by ${users[(i + 2) % 5].username}`, 84 | userId: users[(i + 2) % 5].id, // The user who is reposting 85 | rePostId: posts[i].id, // Linking to the original post being reposted 86 | }, 87 | }); 88 | reposts.push(repost); 89 | } 90 | console.log('Reposts created.'); 91 | 92 | // Create saved posts (users save posts they like) 93 | await prisma.savedPosts.createMany({ 94 | data: [ 95 | { userId: users[0].id, postId: posts[1].id }, 96 | { userId: users[1].id, postId: posts[2].id }, 97 | { userId: users[2].id, postId: posts[3].id }, 98 | { userId: users[3].id, postId: posts[4].id }, 99 | { userId: users[4].id, postId: posts[0].id }, 100 | ], 101 | }); 102 | console.log('Saved posts created.'); 103 | } 104 | 105 | main() 106 | .then(async () => { 107 | await prisma.$disconnect(); 108 | }) 109 | .catch(async (e) => { 110 | console.error(e); 111 | await prisma.$disconnect(); 112 | process.exit(1); 113 | }); -------------------------------------------------------------------------------- /public/general/avatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/safak/x-clone/e4f743b6b2b74ab79d36cd04264a5488c733a61f/public/general/avatar.png -------------------------------------------------------------------------------- /public/general/cover.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/safak/x-clone/e4f743b6b2b74ab79d36cd04264a5488c733a61f/public/general/cover.jpg -------------------------------------------------------------------------------- /public/general/post.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/safak/x-clone/e4f743b6b2b74ab79d36cd04264a5488c733a61f/public/general/post.jpeg -------------------------------------------------------------------------------- /public/icons/back.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | -------------------------------------------------------------------------------- /public/icons/bookmark.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | -------------------------------------------------------------------------------- /public/icons/community.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | -------------------------------------------------------------------------------- /public/icons/date.svg: -------------------------------------------------------------------------------- 1 | 2 | 6 | -------------------------------------------------------------------------------- /public/icons/emoji.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | -------------------------------------------------------------------------------- /public/icons/explore.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | -------------------------------------------------------------------------------- /public/icons/gif.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | -------------------------------------------------------------------------------- /public/icons/home.svg: -------------------------------------------------------------------------------- 1 | 2 | 6 | -------------------------------------------------------------------------------- /public/icons/image.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | -------------------------------------------------------------------------------- /public/icons/infoMore.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | -------------------------------------------------------------------------------- /public/icons/job.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | -------------------------------------------------------------------------------- /public/icons/location.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | -------------------------------------------------------------------------------- /public/icons/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 6 | -------------------------------------------------------------------------------- /public/icons/message.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | -------------------------------------------------------------------------------- /public/icons/more.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | -------------------------------------------------------------------------------- /public/icons/notification.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | -------------------------------------------------------------------------------- /public/icons/poll.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | -------------------------------------------------------------------------------- /public/icons/post.svg: -------------------------------------------------------------------------------- 1 | 2 | 6 | -------------------------------------------------------------------------------- /public/icons/profile.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | -------------------------------------------------------------------------------- /public/icons/schedule.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | -------------------------------------------------------------------------------- /public/icons/userLocation.svg: -------------------------------------------------------------------------------- 1 | 2 | 6 | -------------------------------------------------------------------------------- /public/svg/arrow.svg: -------------------------------------------------------------------------------- 1 | 4 | 8 | -------------------------------------------------------------------------------- /public/svg/comment.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | -------------------------------------------------------------------------------- /public/svg/like.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | -------------------------------------------------------------------------------- /public/svg/original.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | -------------------------------------------------------------------------------- /public/svg/repost.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | -------------------------------------------------------------------------------- /public/svg/save.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | -------------------------------------------------------------------------------- /public/svg/share.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | -------------------------------------------------------------------------------- /public/svg/square.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | -------------------------------------------------------------------------------- /public/svg/wide.svg: -------------------------------------------------------------------------------- 1 | 3 | 6 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | import { createServer } from "node:http"; 2 | import next from "next"; 3 | import { Server } from "socket.io"; 4 | import { v4 as uuidv4 } from "uuid"; 5 | 6 | const dev = process.env.NODE_ENV !== "production"; 7 | const hostname = "localhost"; 8 | const port = 3000; 9 | // when using middleware `hostname` and `port` must be provided below 10 | const app = next({ dev, hostname, port }); 11 | const handler = app.getRequestHandler(); 12 | 13 | let onlineUsers = []; 14 | 15 | const addUser = (username, socketId) => { 16 | const isExist = onlineUsers.find((user) => user.socketId === socketId); 17 | 18 | if (!isExist) { 19 | onlineUsers.push({ username, socketId }); 20 | console.log(username + "added!"); 21 | } 22 | }; 23 | 24 | const removeUser = (socketId) => { 25 | onlineUsers = onlineUsers.filter((user) => user.socketId !== socketId); 26 | console.log("user removed!"); 27 | }; 28 | 29 | const getUser = (username) => { 30 | return onlineUsers.find((user) => user.username === username); 31 | }; 32 | 33 | app.prepare().then(() => { 34 | const httpServer = createServer(handler); 35 | 36 | const io = new Server(httpServer); 37 | 38 | io.on("connection", (socket) => { 39 | socket.on("newUser", (username) => { 40 | addUser(username, socket.id); 41 | }); 42 | 43 | socket.on("sendNotification", ({ receiverUsername, data }) => { 44 | const receiver = getUser(receiverUsername); 45 | 46 | io.to(receiver.socketId).emit("getNotification", { 47 | id: uuidv4(), 48 | ...data, 49 | }); 50 | }); 51 | 52 | socket.on("disconnect", () => { 53 | removeUser(socket.id); 54 | }); 55 | }); 56 | 57 | httpServer 58 | .once("error", (err) => { 59 | console.error(err); 60 | process.exit(1); 61 | }) 62 | .listen(port, () => { 63 | console.log(`> Ready on http://${hostname}:${port}`); 64 | }); 65 | }); 66 | -------------------------------------------------------------------------------- /src/action.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import { auth } from "@clerk/nextjs/server"; 4 | import { prisma } from "./prisma"; 5 | import { z } from "zod"; 6 | import { revalidatePath } from "next/cache"; 7 | import { UploadResponse } from "imagekit/dist/libs/interfaces"; 8 | import { imagekit } from "./utils"; 9 | 10 | export const followUser = async (targetUserId: string) => { 11 | const { userId } = await auth(); 12 | 13 | if (!userId) return; 14 | 15 | const existingFollow = await prisma.follow.findFirst({ 16 | where: { 17 | followerId: userId, 18 | followingId: targetUserId, 19 | }, 20 | }); 21 | 22 | if (existingFollow) { 23 | await prisma.follow.delete({ 24 | where: { id: existingFollow.id }, 25 | }); 26 | } else { 27 | await prisma.follow.create({ 28 | data: { followerId: userId, followingId: targetUserId }, 29 | }); 30 | } 31 | }; 32 | export const likePost = async (postId: number) => { 33 | const { userId } = await auth(); 34 | 35 | if (!userId) return; 36 | 37 | const existingLike = await prisma.like.findFirst({ 38 | where: { 39 | userId: userId, 40 | postId: postId, 41 | }, 42 | }); 43 | 44 | if (existingLike) { 45 | await prisma.like.delete({ 46 | where: { id: existingLike.id }, 47 | }); 48 | } else { 49 | await prisma.like.create({ 50 | data: { userId, postId }, 51 | }); 52 | } 53 | }; 54 | export const rePost = async (postId: number) => { 55 | const { userId } = await auth(); 56 | 57 | if (!userId) return; 58 | 59 | const existingRePost = await prisma.post.findFirst({ 60 | where: { 61 | userId: userId, 62 | rePostId: postId, 63 | }, 64 | }); 65 | 66 | if (existingRePost) { 67 | await prisma.post.delete({ 68 | where: { id: existingRePost.id }, 69 | }); 70 | } else { 71 | await prisma.post.create({ 72 | data: { userId, rePostId: postId }, 73 | }); 74 | } 75 | }; 76 | 77 | export const savePost = async (postId: number) => { 78 | const { userId } = await auth(); 79 | 80 | if (!userId) return; 81 | 82 | const existingSavedPost = await prisma.savedPosts.findFirst({ 83 | where: { 84 | userId: userId, 85 | postId: postId, 86 | }, 87 | }); 88 | 89 | if (existingSavedPost) { 90 | await prisma.savedPosts.delete({ 91 | where: { id: existingSavedPost.id }, 92 | }); 93 | } else { 94 | await prisma.savedPosts.create({ 95 | data: { userId, postId }, 96 | }); 97 | } 98 | }; 99 | 100 | export const addComment = async ( 101 | prevState: { success: boolean; error: boolean }, 102 | formData: FormData 103 | ) => { 104 | const { userId } = await auth(); 105 | 106 | if (!userId) return { success: false, error: true }; 107 | 108 | const postId = formData.get("postId"); 109 | const username = formData.get("username"); 110 | const desc = formData.get("desc"); 111 | 112 | const Comment = z.object({ 113 | parentPostId: z.number(), 114 | desc: z.string().max(140), 115 | }); 116 | 117 | const validatedFields = Comment.safeParse({ 118 | parentPostId: Number(postId), 119 | desc, 120 | }); 121 | 122 | if (!validatedFields.success) { 123 | console.log(validatedFields.error.flatten().fieldErrors); 124 | return { success: false, error: true }; 125 | } 126 | 127 | try { 128 | await prisma.post.create({ 129 | data: { 130 | ...validatedFields.data, 131 | userId, 132 | }, 133 | }); 134 | revalidatePath(`/${username}/status/${postId}`); 135 | return { success: true, error: false }; 136 | } catch (err) { 137 | console.log(err); 138 | return { success: false, error: true }; 139 | } 140 | }; 141 | 142 | export const addPost = async ( 143 | prevState: { success: boolean; error: boolean }, 144 | formData: FormData 145 | ) => { 146 | const { userId } = await auth(); 147 | 148 | if (!userId) return { success: false, error: true }; 149 | 150 | const desc = formData.get("desc"); 151 | const file = formData.get("file") as File; 152 | const isSensitive = formData.get("isSensitive") as string; 153 | const imgType = formData.get("imgType"); 154 | 155 | const uploadFile = async (file: File): Promise => { 156 | const bytes = await file.arrayBuffer(); 157 | const buffer = Buffer.from(bytes); 158 | 159 | const transformation = `w-600,${ 160 | imgType === "square" ? "ar-1-1" : imgType === "wide" ? "ar-16-9" : "" 161 | }`; 162 | 163 | return new Promise((resolve, reject) => { 164 | imagekit.upload( 165 | { 166 | file: buffer, 167 | fileName: file.name, 168 | folder: "/posts", 169 | ...(file.type.includes("image") && { 170 | transformation: { 171 | pre: transformation, 172 | }, 173 | }), 174 | }, 175 | function (error, result) { 176 | if (error) reject(error); 177 | else resolve(result as UploadResponse); 178 | } 179 | ); 180 | }); 181 | }; 182 | 183 | const Post = z.object({ 184 | desc: z.string().max(140), 185 | isSensitive: z.boolean().optional(), 186 | }); 187 | 188 | const validatedFields = Post.safeParse({ 189 | desc, 190 | isSensitive: JSON.parse(isSensitive), 191 | }); 192 | 193 | if (!validatedFields.success) { 194 | console.log(validatedFields.error.flatten().fieldErrors); 195 | return { success: false, error: true }; 196 | } 197 | 198 | let img = ""; 199 | let imgHeight = 0; 200 | let video = ""; 201 | 202 | if (file.size) { 203 | const result: UploadResponse = await uploadFile(file); 204 | 205 | if (result.fileType === "image") { 206 | img = result.filePath; 207 | imgHeight = result.height; 208 | } else { 209 | video = result.filePath; 210 | } 211 | } 212 | 213 | console.log({ 214 | ...validatedFields.data, 215 | userId, 216 | img, 217 | imgHeight, 218 | video, 219 | }); 220 | 221 | try { 222 | await prisma.post.create({ 223 | data: { 224 | ...validatedFields.data, 225 | userId, 226 | img, 227 | imgHeight, 228 | video, 229 | }, 230 | }); 231 | revalidatePath(`/`); 232 | return { success: true, error: false }; 233 | } catch (err) { 234 | console.log(err); 235 | return { success: false, error: true }; 236 | } 237 | return { success: false, error: true }; 238 | }; 239 | -------------------------------------------------------------------------------- /src/app/(board)/@modal/compose/post/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import Image from "@/components/Image"; 4 | import { useRouter } from "next/navigation"; 5 | 6 | const PostModal = () => { 7 | const router = useRouter(); 8 | 9 | const closeModal = () => { 10 | router.back(); 11 | }; 12 | 13 | return ( 14 |
15 |
16 | {/* TOP */} 17 |
18 |
19 | X 20 |
21 |
Drafts
22 |
23 | {/* CENTER */} 24 |
25 |
26 | Lama Dev 33 |
34 | 39 |
40 | {/* BOTTOM */} 41 |
42 |
43 | 50 | 57 | 64 | 71 | 78 | 85 |
86 | 87 |
88 |
89 |
90 | ); 91 | }; 92 | 93 | export default PostModal; 94 | -------------------------------------------------------------------------------- /src/app/(board)/@modal/default.tsx: -------------------------------------------------------------------------------- 1 | const Default = () => { 2 | return null; 3 | }; 4 | 5 | export default Default; 6 | -------------------------------------------------------------------------------- /src/app/(board)/[username]/page.tsx: -------------------------------------------------------------------------------- 1 | import Feed from "@/components/Feed"; 2 | import FollowButton from "@/components/FollowButton"; 3 | import Image from "@/components/Image"; 4 | import { prisma } from "@/prisma"; 5 | import { auth } from "@clerk/nextjs/server"; 6 | import Link from "next/link"; 7 | import { notFound } from "next/navigation"; 8 | 9 | const UserPage = async ({ 10 | params, 11 | }: { 12 | params: Promise<{ username: string }>; 13 | }) => { 14 | const { userId } = await auth(); 15 | 16 | const username = (await params).username; 17 | 18 | const user = await prisma.user.findUnique({ 19 | where: { username: username }, 20 | include: { 21 | _count: { select: { followers: true, followings: true } }, 22 | followings: userId ? { where: { followerId: userId } } : undefined, 23 | }, 24 | }); 25 | 26 | console.log(userId); 27 | if (!user) return notFound(); 28 | 29 | return ( 30 |
31 | {/* PROFILE TITLE */} 32 |
33 | 34 | back 35 | 36 |

{user.displayName}

37 |
38 | {/* INFO */} 39 |
40 | {/* COVER & AVATAR CONTAINER */} 41 |
42 | {/* COVER */} 43 |
44 | 51 |
52 | {/* AVATAR */} 53 |
54 | 61 |
62 |
63 |
64 |
65 | more 66 |
67 |
68 | more 69 |
70 |
71 | more 72 |
73 | {userId && ( 74 | 79 | )} 80 |
81 | {/* USER DETAILS */} 82 |
83 | {/* USERNAME & HANDLE */} 84 |
85 |

{user.displayName}

86 | @{user.username} 87 |
88 | {user.bio &&

{user.bio}

} 89 | {/* JOB & LOCATION & DATE */} 90 |
91 | {user.location && ( 92 |
93 | location 99 | {user.location} 100 |
101 | )} 102 |
103 | date 104 | 105 | Joined{" "} 106 | {new Date(user.createdAt.toString()).toLocaleDateString( 107 | "en-US", 108 | { month: "long", year: "numeric" } 109 | )} 110 | 111 |
112 |
113 | {/* FOLLOWINGS & FOLLOWERS */} 114 |
115 |
116 | {user._count.followers} 117 | Followers 118 |
119 |
120 | {user._count.followings} 121 | Followings 122 |
123 |
124 |
125 |
126 | {/* FEED */} 127 | 128 |
129 | ); 130 | }; 131 | 132 | export default UserPage; 133 | -------------------------------------------------------------------------------- /src/app/(board)/[username]/status/[postId]/page.tsx: -------------------------------------------------------------------------------- 1 | import Comments from "@/components/Comments"; 2 | import Image from "@/components/Image"; 3 | import Post from "@/components/Post"; 4 | import { prisma } from "@/prisma"; 5 | import { auth } from "@clerk/nextjs/server"; 6 | import Link from "next/link"; 7 | import { notFound } from "next/navigation"; 8 | 9 | const StatusPage = async ({ 10 | params, 11 | }: { 12 | params: Promise<{ username: string; postId: string }>; 13 | }) => { 14 | const { userId } = await auth(); 15 | const postId = (await params).postId; 16 | 17 | if (!userId) return; 18 | 19 | const post = await prisma.post.findFirst({ 20 | 21 | where: { id: Number(postId) }, 22 | include: { 23 | user: { select: { displayName: true, username: true, img: true } }, 24 | _count: { select: { likes: true, rePosts: true, comments: true } }, 25 | likes: { where: { userId: userId }, select: { id: true } }, 26 | rePosts: { where: { userId: userId }, select: { id: true } }, 27 | saves: { where: { userId: userId }, select: { id: true } }, 28 | comments: { 29 | orderBy:{createdAt:"desc"}, 30 | include: { 31 | user: { select: { displayName: true, username: true, img: true } }, 32 | _count: { select: { likes: true, rePosts: true, comments: true } }, 33 | likes: { where: { userId: userId }, select: { id: true } }, 34 | rePosts: { where: { userId: userId }, select: { id: true } }, 35 | saves: { where: { userId: userId }, select: { id: true } }, 36 | }, 37 | }, 38 | }, 39 | }); 40 | 41 | if (!post) return notFound(); 42 | 43 | return ( 44 |
45 |
46 | 47 | back 48 | 49 |

Post

50 |
51 | 52 | 57 |
58 | ); 59 | }; 60 | 61 | export default StatusPage; 62 | -------------------------------------------------------------------------------- /src/app/(board)/layout.tsx: -------------------------------------------------------------------------------- 1 | import LeftBar from "@/components/LeftBar"; 2 | import RightBar from "@/components/RightBar"; 3 | 4 | export default function BoardLayout({ 5 | children, 6 | modal, 7 | }: Readonly<{ 8 | children: React.ReactNode; 9 | modal: React.ReactNode; 10 | }>) { 11 | return ( 12 |
13 |
14 | 15 |
16 |
17 | {children} 18 | {modal} 19 |
20 |
21 | 22 |
23 |
24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /src/app/(board)/page.tsx: -------------------------------------------------------------------------------- 1 | import Feed from "@/components/Feed"; 2 | import Share from "@/components/Share"; 3 | import Link from "next/link"; 4 | 5 | const Homepage = () => { 6 | 7 | return
8 |
9 | For you 10 | Following 11 | React.js 12 | Javascript 13 | CSS 14 |
15 | 16 | 17 |
; 18 | }; 19 | 20 | export default Homepage; 21 | -------------------------------------------------------------------------------- /src/app/api/posts/route.ts: -------------------------------------------------------------------------------- 1 | import { prisma } from "@/prisma"; 2 | import { auth } from "@clerk/nextjs/server"; 3 | import { NextRequest } from "next/server"; 4 | 5 | export async function GET(request: NextRequest) { 6 | const searchParams = request.nextUrl.searchParams; 7 | 8 | const userProfileId = searchParams.get("user"); 9 | const page = searchParams.get("cursor"); 10 | const LIMIT = 3; 11 | 12 | const { userId } = await auth(); 13 | 14 | if (!userId) return; 15 | 16 | const whereCondition = 17 | userProfileId !== "undefined" 18 | ? { parentPostId: null, userId: userProfileId as string } 19 | : { 20 | parentPostId: null, 21 | userId: { 22 | in: [ 23 | userId, 24 | ...( 25 | await prisma.follow.findMany({ 26 | where: { followerId: userId }, 27 | select: { followingId: true }, 28 | }) 29 | ).map((follow) => follow.followingId), 30 | ], 31 | }, 32 | }; 33 | 34 | const postIncludeQuery = { 35 | user: { select: { displayName: true, username: true, img: true } }, 36 | _count: { select: { likes: true, rePosts: true, comments: true } }, 37 | likes: { where: { userId: userId }, select: { id: true } }, 38 | rePosts: { where: { userId: userId }, select: { id: true } }, 39 | saves: { where: { userId: userId }, select: { id: true } }, 40 | }; 41 | 42 | const posts = await prisma.post.findMany({ 43 | where: whereCondition, 44 | include: { 45 | rePost: { 46 | include: postIncludeQuery, 47 | }, 48 | ...postIncludeQuery, 49 | }, 50 | take: LIMIT, 51 | skip: (Number(page) - 1) * LIMIT, 52 | orderBy: { createdAt: "desc" } 53 | }); 54 | 55 | const totalPosts = await prisma.post.count({ where: whereCondition }); 56 | 57 | const hasMore = Number(page) * LIMIT < totalPosts; 58 | 59 | // await new Promise((resolve) => setTimeout(resolve, 3000)); 60 | 61 | return Response.json({ posts, hasMore }); 62 | } 63 | -------------------------------------------------------------------------------- /src/app/api/webhooks/clerk/route.ts: -------------------------------------------------------------------------------- 1 | import { Webhook } from "svix"; 2 | import { headers } from "next/headers"; 3 | import { WebhookEvent } from "@clerk/nextjs/server"; 4 | import { prisma } from "@/prisma"; 5 | 6 | export async function POST(req: Request) { 7 | const SIGNING_SECRET = process.env.SIGNING_SECRET; 8 | 9 | if (!SIGNING_SECRET) { 10 | throw new Error( 11 | "Error: Please add SIGNING_SECRET from Clerk Dashboard to .env or .env.local" 12 | ); 13 | } 14 | 15 | // Create new Svix instance with secret 16 | const wh = new Webhook(SIGNING_SECRET); 17 | 18 | // Get headers 19 | const headerPayload = await headers(); 20 | const svix_id = headerPayload.get("svix-id"); 21 | const svix_timestamp = headerPayload.get("svix-timestamp"); 22 | const svix_signature = headerPayload.get("svix-signature"); 23 | 24 | // If there are no headers, error out 25 | if (!svix_id || !svix_timestamp || !svix_signature) { 26 | return new Response("Error: Missing Svix headers", { 27 | status: 400, 28 | }); 29 | } 30 | 31 | // Get body 32 | const payload = await req.json(); 33 | const body = JSON.stringify(payload); 34 | 35 | let evt: WebhookEvent; 36 | 37 | // Verify payload with headers 38 | try { 39 | evt = wh.verify(body, { 40 | "svix-id": svix_id, 41 | "svix-timestamp": svix_timestamp, 42 | "svix-signature": svix_signature, 43 | }) as WebhookEvent; 44 | } catch (err) { 45 | console.error("Error: Could not verify webhook:", err); 46 | return new Response("Error: Verification error", { 47 | status: 400, 48 | }); 49 | } 50 | 51 | // Do something with payload 52 | // For this guide, log payload to console 53 | const { id } = evt.data; 54 | const eventType = evt.type; 55 | console.log(`Received webhook with ID ${id} and event type of ${eventType}`); 56 | console.log("Webhook payload:", body); 57 | 58 | if (eventType === "user.created") { 59 | try { 60 | await prisma.user.create({ 61 | data: { 62 | id: evt.data.id, 63 | username: JSON.parse(body).data.username, 64 | email: JSON.parse(body).data.email_addresses[0].email_address, 65 | img: JSON.parse(body).image_url || "" 66 | }, 67 | }); 68 | return new Response("User created", { status: 200 }); 69 | } catch (err) { 70 | console.log(err); 71 | return new Response("Error: Failed to create a user!", { 72 | status: 500, 73 | }); 74 | } 75 | } 76 | 77 | if (eventType === "user.deleted") { 78 | try { 79 | await prisma.user.delete({ where: { id: evt.data.id } }); 80 | return new Response("User deleted", { status: 200 }); 81 | } catch (err) { 82 | console.log(err); 83 | return new Response("Error: Failed to create a user!", { 84 | status: 500, 85 | }); 86 | } 87 | } 88 | 89 | return new Response("Webhook received", { status: 200 }); 90 | } 91 | -------------------------------------------------------------------------------- /src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/safak/x-clone/e4f743b6b2b74ab79d36cd04264a5488c733a61f/src/app/favicon.ico -------------------------------------------------------------------------------- /src/app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | body { 6 | color: #ededed; 7 | background: black; 8 | font-family: Arial, Helvetica, sans-serif; 9 | } 10 | -------------------------------------------------------------------------------- /src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import "./globals.css"; 2 | 3 | import type { Metadata } from "next"; 4 | import { ClerkProvider } from "@clerk/nextjs"; 5 | import QueryProvider from "@/providers/QueryProvider"; 6 | 7 | export const metadata: Metadata = { 8 | title: "Lama Dev X Clone", 9 | description: "Next.js social media application project", 10 | }; 11 | 12 | export default function AppLayout({ 13 | children, 14 | }: Readonly<{ 15 | children: React.ReactNode; 16 | }>) { 17 | return ( 18 | 19 | 20 | 21 | {children} 22 | 23 | 24 | 25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /src/app/sign-in/[[...sign-in]]/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as Clerk from "@clerk/elements/common"; 4 | import * as SignIn from "@clerk/elements/sign-in"; 5 | import Link from "next/link"; 6 | 7 | const SignInPage = () => { 8 | return ( 9 |
10 |
11 | 17 | 21 | 22 |
23 |
24 |

25 | Happening now 26 |

27 |

Join today.

28 | 29 | 33 | 34 | 38 | 42 | 46 | 50 | 51 | Sign in with Google 52 | 53 | 57 | 58 | 59 | 60 | Sign in with Apple 61 | 62 | {/* LOGIN WITH CREDENTIALS */} 63 | 64 | 65 | 69 | 70 | 71 | 75 | Continue 76 | 77 | 78 | 79 | 80 | 81 | 85 | 86 | 87 |
88 | 92 | Continue 93 | 94 | 98 | Forgot Password? 99 | 100 |
101 |
102 | 103 |

104 | We sent a code to . 105 |

106 | 107 | 108 | 112 | 113 | 114 | 115 | 119 | Continue 120 | 121 |
122 |
123 | 127 | 128 | Reset password 129 | 130 | 131 | 132 | Go back 133 | 134 | 135 | 136 |

Reset your password

137 | 138 | 139 | New password 140 | 141 | 142 | 143 | 144 | 145 | Confirm password 146 | 147 | 148 | 149 | 150 | Reset password 151 |
152 | {/* OR SIGN UP */} 153 |
154 |
155 | or 156 |
157 |
158 | 162 | Create Account 163 | 164 |

165 | By signing up, you agree to the Terms of Service and Privacy Policy, 166 | including Cookie Use. 167 |

168 |
169 |
170 |
171 | ); 172 | }; 173 | 174 | export default SignInPage; 175 | -------------------------------------------------------------------------------- /src/app/sign-in/layout.tsx: -------------------------------------------------------------------------------- 1 | export const metadata = { 2 | title: 'Next.js', 3 | description: 'Generated by Next.js', 4 | } 5 | 6 | export default function RootLayout({ 7 | children, 8 | }: { 9 | children: React.ReactNode 10 | }) { 11 | return ( 12 | 13 | {children} 14 | 15 | ) 16 | } 17 | -------------------------------------------------------------------------------- /src/app/sign-up/[[...sign-up]]/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as Clerk from "@clerk/elements/common"; 4 | import * as SignUp from "@clerk/elements/sign-up"; 5 | import Link from "next/link"; 6 | 7 | const SignUpPage = () => { 8 | return ( 9 |
10 |
11 | 17 | 21 | 22 |
23 |
24 |

25 | Happening now 26 |

27 |

Join today.

28 | 29 | 30 | 34 | 35 | 39 | 43 | 47 | 51 | 52 | Sign up with Google 53 | 54 | 58 | 59 | 60 | 61 | Sign up with Apple 62 | 63 |
64 | Sign up with Credentials 65 | 66 | 70 | 71 | 72 | 73 | 77 | 78 | 79 | 80 | 84 | 85 | 86 | 87 | 91 | Sign up 92 | 93 |
94 |
95 | 96 | 97 | 98 | 99 | 100 | 101 | Continue 102 | 103 | 104 | 105 |

Check your e-mail

106 | 107 | 111 | 112 | 113 | 117 | Verify 118 | 119 |
120 |
121 | {/* OR SIGN UP */} 122 |
123 |
124 | or 125 |
126 |
127 | 131 | Already have an account? 132 | 133 |

134 | By signing up, you agree to the{" "} 135 | Terms of Service and{" "} 136 | Privacy Policy, including{" "} 137 | Cookie Use. 138 |

139 |
140 |
141 |
142 | ); 143 | }; 144 | 145 | export default SignUpPage; 146 | -------------------------------------------------------------------------------- /src/components/Comments.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useUser } from "@clerk/nextjs"; 4 | import Image from "./Image"; 5 | import Post from "./Post"; 6 | import { Post as PostType } from "@prisma/client"; 7 | import { useActionState, useEffect } from "react"; 8 | import { addComment } from "@/action"; 9 | import { socket } from "@/socket"; 10 | 11 | type CommentWithDetails = PostType & { 12 | user: { displayName: string | null; username: string; img: string | null }; 13 | _count: { likes: number; rePosts: number; comments: number }; 14 | likes: { id: number }[]; 15 | rePosts: { id: number }[]; 16 | saves: { id: number }[]; 17 | }; 18 | 19 | const Comments = ({ 20 | comments, 21 | postId, 22 | username, 23 | }: { 24 | comments: CommentWithDetails[]; 25 | postId: number; 26 | username: string; 27 | }) => { 28 | const { isLoaded, isSignedIn, user } = useUser(); 29 | 30 | const [state, formAction, isPending] = useActionState(addComment, { 31 | success: false, 32 | error: false, 33 | }); 34 | 35 | useEffect(() => { 36 | if (state.success) { 37 | socket.emit("sendNotification", { 38 | receiverUsername: username, 39 | data: { 40 | senderUsername: user?.username, 41 | type: "comment", 42 | link: `/${username}/status/${postId}`, 43 | }, 44 | }); 45 | } 46 | }, [state.success, username, user?.username, postId]); 47 | 48 | return ( 49 |
50 | {user && ( 51 |
55 |
56 | Lama Dev 63 |
64 | 65 | 72 | 78 | 84 |
85 | )} 86 | {state.error && ( 87 | Something went wrong! 88 | )} 89 | {comments.map((comment) => ( 90 |
91 | 92 |
93 | ))} 94 |
95 | ); 96 | }; 97 | 98 | export default Comments; 99 | -------------------------------------------------------------------------------- /src/components/Feed.tsx: -------------------------------------------------------------------------------- 1 | import { prisma } from "@/prisma"; 2 | import Post from "./Post"; 3 | import { auth } from "@clerk/nextjs/server"; 4 | import InfiniteFeed from "./InfiniteFeed"; 5 | 6 | const Feed = async ({ userProfileId }: { userProfileId?: string }) => { 7 | const { userId } = await auth(); 8 | 9 | if (!userId) return; 10 | 11 | const whereCondition = userProfileId 12 | ? { parentPostId: null, userId: userProfileId } 13 | : { 14 | parentPostId: null, 15 | userId: { 16 | in: [ 17 | userId, 18 | ...( 19 | await prisma.follow.findMany({ 20 | where: { followerId: userId }, 21 | select: { followingId: true }, 22 | }) 23 | ).map((follow) => follow.followingId), 24 | ], 25 | }, 26 | }; 27 | 28 | const postIncludeQuery = { 29 | user: { select: { displayName: true, username: true, img: true } }, 30 | _count: { select: { likes: true, rePosts: true, comments: true } }, 31 | likes: { where: { userId: userId }, select: { id: true } }, 32 | rePosts: { where: { userId: userId }, select: { id: true } }, 33 | saves: { where: { userId: userId }, select: { id: true } }, 34 | }; 35 | 36 | const posts = await prisma.post.findMany({ 37 | where: whereCondition, 38 | include: { 39 | rePost: { 40 | include: postIncludeQuery, 41 | }, 42 | ...postIncludeQuery, 43 | }, 44 | take: 3, 45 | skip: 0, 46 | orderBy: { createdAt: "desc" }, 47 | }); 48 | 49 | return ( 50 |
51 | {posts.map((post) => ( 52 |
53 | 54 |
55 | ))} 56 | 57 |
58 | ); 59 | }; 60 | 61 | export default Feed; 62 | -------------------------------------------------------------------------------- /src/components/FollowButton.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { followUser } from "@/action"; 4 | import { socket } from "@/socket"; 5 | import { useUser } from "@clerk/nextjs"; 6 | import { useOptimistic, useState } from "react"; 7 | 8 | const FollowButton = ({ 9 | userId, 10 | isFollowed, 11 | username, 12 | }: { 13 | userId: string; 14 | isFollowed: boolean; 15 | username: string; 16 | }) => { 17 | const [state, setState] = useState(isFollowed); 18 | 19 | const { user } = useUser(); 20 | 21 | const [optimisticFollow, switchOptimisticFollow] = useOptimistic( 22 | state, 23 | (prev) => !prev 24 | ); 25 | 26 | if (!user) return; 27 | 28 | const followAction = async () => { 29 | switchOptimisticFollow(""); 30 | await followUser(userId); 31 | setState((prev) => !prev); 32 | // SEND NOTIFICATION 33 | socket.emit("sendNotification", { 34 | receiverUsername: username, 35 | data: { 36 | senderUsername: user.username, 37 | type: "follow", 38 | link: `/${user.username}`, 39 | }, 40 | }); 41 | }; 42 | 43 | return ( 44 |
45 | 48 |
49 | ); 50 | }; 51 | 52 | export default FollowButton; 53 | -------------------------------------------------------------------------------- /src/components/Image.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { IKImage } from "imagekitio-next"; 4 | 5 | type ImageType = { 6 | path?: string; 7 | src?: string; 8 | w?: number; 9 | h?: number; 10 | alt: string; 11 | className?: string; 12 | tr?: boolean; 13 | }; 14 | 15 | const urlEndpoint = process.env.NEXT_PUBLIC_URL_ENDPOINT; 16 | 17 | if (!urlEndpoint) { 18 | throw new Error('Error: Please add urlEndpoint to .env or .env.local') 19 | } 20 | 21 | const Image = ({ path, src, w, h, alt, className, tr }: ImageType) => { 22 | return ( 23 | 34 | ); 35 | }; 36 | 37 | export default Image; 38 | -------------------------------------------------------------------------------- /src/components/ImageEditor.tsx: -------------------------------------------------------------------------------- 1 | import Image from "next/image"; 2 | import React from "react"; 3 | 4 | const ImageEditor = ({ 5 | onClose, 6 | previewURL, 7 | settings, 8 | setSettings, 9 | }: { 10 | onClose: () => void; 11 | previewURL: string; 12 | settings: { 13 | type: "original" | "wide" | "square"; 14 | sensitive: boolean; 15 | }; 16 | setSettings: React.Dispatch< 17 | React.SetStateAction<{ 18 | type: "original" | "wide" | "square"; 19 | sensitive: boolean; 20 | }> 21 | >; 22 | }) => { 23 | const handleChangeSensitive = (sensitive: boolean) => { 24 | setSettings((prev) => ({ ...prev, sensitive })); 25 | }; 26 | const handleChangeType = (type: "original" | "wide" | "square") => { 27 | setSettings((prev) => ({ ...prev, type })); 28 | }; 29 | return ( 30 |
31 |
32 | {/* TOP */} 33 |
34 |
35 | 41 | 45 | 46 |

Media Settings

47 |
48 | 51 |
52 | {/* IMAGE CONTAINER */} 53 |
54 | 67 |
68 | {/* SETTINGS */} 69 |
70 |
71 |
handleChangeType("original")} 74 | > 75 | 76 | 84 | 85 | Original 86 |
87 |
handleChangeType("wide")} 90 | > 91 | 92 | 100 | 101 | Wide 102 |
103 |
handleChangeType("square")} 106 | > 107 | 108 | 116 | 117 | Square 118 |
119 |
120 |
handleChangeSensitive(!settings.sensitive)} 125 | > 126 | Sensitive 127 |
128 |
129 |
130 |
131 | ); 132 | }; 133 | 134 | export default ImageEditor; 135 | -------------------------------------------------------------------------------- /src/components/InfiniteFeed.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useInfiniteQuery } from "@tanstack/react-query"; 4 | import InfiniteScroll from "react-infinite-scroll-component"; 5 | import Post from "./Post"; 6 | 7 | const fetchPosts = async (pageParam: number, userProfileId?: string) => { 8 | const res = await fetch( 9 | "http://localhost:3000/api/posts?cursor=" + 10 | pageParam + 11 | "&user=" + 12 | userProfileId 13 | ); 14 | return res.json(); 15 | }; 16 | 17 | const InfiniteFeed = ({ userProfileId }: { userProfileId?: string }) => { 18 | const { data, error, status, hasNextPage, fetchNextPage } = useInfiniteQuery({ 19 | queryKey: ["posts"], 20 | queryFn: ({ pageParam = 2 }) => fetchPosts(pageParam, userProfileId), 21 | initialPageParam: 2, 22 | getNextPageParam: (lastPage, pages) => 23 | lastPage.hasMore ? pages.length + 2 : undefined, 24 | }); 25 | 26 | if (error) return "Something went wrong!"; 27 | if (status === "pending") return "Loading..."; 28 | 29 | console.log(data); 30 | 31 | const allPosts = data?.pages?.flatMap((page) => page.posts) || []; 32 | 33 | return ( 34 | Posts are loading...} 39 | endMessage={

All posts loaded!

} 40 | > 41 | {allPosts.map((post) => ( 42 | 43 | ))} 44 |
45 | ); 46 | }; 47 | 48 | export default InfiniteFeed; 49 | -------------------------------------------------------------------------------- /src/components/LeftBar.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | import Image from "./Image"; 3 | import Socket from "./Socket"; 4 | import Notification from "./Notification"; 5 | import { currentUser } from "@clerk/nextjs/server"; 6 | import Logout from "./Logout"; 7 | 8 | const menuList = [ 9 | { 10 | id: 1, 11 | name: "Homepage", 12 | link: "/", 13 | icon: "home.svg", 14 | }, 15 | { 16 | id: 2, 17 | name: "Explore", 18 | link: "/", 19 | icon: "explore.svg", 20 | }, 21 | // { 22 | // id: 3, 23 | // name: "Notification", 24 | // link: "/", 25 | // icon: "notification.svg", 26 | // }, 27 | { 28 | id: 4, 29 | name: "Messages", 30 | link: "/", 31 | icon: "message.svg", 32 | }, 33 | { 34 | id: 5, 35 | name: "Bookmarks", 36 | link: "/", 37 | icon: "bookmark.svg", 38 | }, 39 | { 40 | id: 6, 41 | name: "Jobs", 42 | link: "/", 43 | icon: "job.svg", 44 | }, 45 | { 46 | id: 7, 47 | name: "Communities", 48 | link: "/", 49 | icon: "community.svg", 50 | }, 51 | { 52 | id: 8, 53 | name: "Premium", 54 | link: "/", 55 | icon: "logo.svg", 56 | }, 57 | { 58 | id: 9, 59 | name: "Profile", 60 | link: "/", 61 | icon: "profile.svg", 62 | }, 63 | { 64 | id: 10, 65 | name: "More", 66 | link: "/", 67 | icon: "more.svg", 68 | }, 69 | ]; 70 | 71 | const LeftBar = async () => { 72 | const user = await currentUser(); 73 | 74 | return ( 75 |
76 | {/* LOGO MENU BUTTON */} 77 |
78 | {/* LOGO */} 79 | 80 | logo 81 | 82 | {/* MENU LIST */} 83 |
84 | {menuList.map((item, i) => ( 85 |
86 | {i === 2 && user && ( 87 |
88 | 89 |
90 | )} 91 | 95 | {item.name} 101 | {item.name} 102 | 103 |
104 | ))} 105 |
106 | {/* BUTTON */} 107 | 111 | new post 112 | 113 | 117 | Post 118 | 119 |
120 | {user && ( 121 | <> 122 | 123 | {/* USER */} 124 |
125 |
126 |
127 | 134 |
135 |
136 | {user?.username} 137 | @{user?.username} 138 |
139 |
140 | {/*
...
*/} 141 | {/* ADD LOGOUT */} 142 | 143 |
144 | 145 | )} 146 |
147 | ); 148 | }; 149 | 150 | export default LeftBar; 151 | -------------------------------------------------------------------------------- /src/components/Logout.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useClerk } from "@clerk/nextjs"; 4 | import Link from "next/link"; 5 | import { useState } from "react"; 6 | import Image from "./Image"; 7 | 8 | const Logout = () => { 9 | const [open, setOpen] = useState(false); 10 | 11 | const { signOut } = useClerk(); 12 | 13 | return ( 14 |
15 |
setOpen((prev) => !prev)} 18 | > 19 | ... 20 |
21 | {open && ( 22 |
23 | setOpen(false)} 27 | > 28 | User Profile 29 | 30 | setOpen(false)} 34 | > 35 | Saved Posts 36 | 37 | setOpen(false)} 41 | > 42 | Settings 43 | 44 |
45 | 51 |
52 | )} 53 |
54 | ); 55 | }; 56 | 57 | export default Logout; -------------------------------------------------------------------------------- /src/components/Notification.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useEffect, useState } from "react"; 4 | import Image from "./Image"; 5 | import { socket } from "@/socket"; 6 | import { useRouter } from "next/navigation"; 7 | 8 | type NotificationType = { 9 | id: string; 10 | senderUsername: string; 11 | type: "like" | "comment" | "rePost" | "follow"; 12 | link: string; 13 | }; 14 | 15 | const Notification = () => { 16 | const [notifications, setNotifications] = useState([]); 17 | const [open, setOpen] = useState(false); 18 | 19 | useEffect(() => { 20 | socket.on("getNotification", (data: NotificationType) => { 21 | setNotifications((prev) => [...prev, data]); 22 | }); 23 | }, []); 24 | 25 | const router = useRouter(); 26 | 27 | const reset = () => { 28 | setNotifications([]); 29 | setOpen(false); 30 | }; 31 | 32 | const handleClick = (notification: NotificationType) => { 33 | const filteredList = notifications.filter((n) => n.id !== notification.id); 34 | setNotifications(filteredList); 35 | setOpen(false); 36 | router.push(notification.link); 37 | }; 38 | return ( 39 |
40 |
setOpen((prev) => !prev)} 43 | > 44 |
45 | 46 | {notifications.length > 0 && ( 47 |
48 | {notifications.length} 49 |
50 | )} 51 |
52 | Notifications 53 |
54 | {open && ( 55 |
56 |

Notifications

57 | {notifications.map((n) => ( 58 |
handleClick(n)} 62 | > 63 | {n.senderUsername}{" "} 64 | {n.type === "like" 65 | ? "liked your post" 66 | : n.type === "rePost" 67 | ? "re-posted your post" 68 | : n.type === "comment" 69 | ? "replied your post" 70 | : "followed you"} 71 |
72 | ))} 73 | 79 |
80 | )} 81 |
82 | ); 83 | }; 84 | 85 | export default Notification; 86 | -------------------------------------------------------------------------------- /src/components/PopularTags.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | import Image from "./Image"; 3 | 4 | const PopularTags = () => { 5 | return ( 6 |
7 |

8 | {"What's"} Happening 9 |

10 | {/* TREND EVENT */} 11 |
12 |
13 | event 20 |
21 |
22 |

23 | Nadal v Federer Grand Slam 24 |

25 | Last Night 26 |
27 |
28 | {/* TOPICS */} 29 |
30 |
31 | Technology • Trending 32 | info 33 |
34 |

OpenAI

35 | 20K posts 36 |
37 | {/* TOPICS */} 38 |
39 |
40 | Technology • Trending 41 | info 42 |
43 |

OpenAI

44 | 20K posts 45 |
46 | {/* TOPICS */} 47 |
48 |
49 | Technology • Trending 50 | info 51 |
52 |

OpenAI

53 | 20K posts 54 |
55 | {/* TOPICS */} 56 |
57 |
58 | Technology • Trending 59 | info 60 |
61 |

OpenAI

62 | 20K posts 63 |
64 | 65 | Show More 66 | 67 |
68 | ); 69 | }; 70 | 71 | export default PopularTags; 72 | -------------------------------------------------------------------------------- /src/components/Post.tsx: -------------------------------------------------------------------------------- 1 | import { imagekit } from "@/utils"; 2 | import Image from "./Image"; 3 | import PostInfo from "./PostInfo"; 4 | import PostInteractions from "./PostInteractions"; 5 | import Video from "./Video"; 6 | import Link from "next/link"; 7 | import { Post as PostType } from "@prisma/client"; 8 | import { format } from "timeago.js"; 9 | 10 | type UserSummary = { 11 | displayName: string | null; 12 | username: string; 13 | img: string | null; 14 | }; 15 | 16 | type Engagement = { 17 | _count: { likes: number; rePosts: number; comments: number }; 18 | likes: { id: number }[]; 19 | rePosts: { id: number }[]; 20 | saves: { id: number }[]; 21 | }; 22 | 23 | type PostWithDetails = PostType & 24 | Engagement & { 25 | user: UserSummary; 26 | rePost?: (PostType & Engagement & { user: UserSummary }) | null; 27 | }; 28 | 29 | const Post = ({ 30 | type, 31 | post, 32 | }: { 33 | type?: "status" | "comment"; 34 | post: PostWithDetails; 35 | }) => { 36 | const originalPost = post.rePost || post; 37 | 38 | return ( 39 |
40 | {/* POST TYPE */} 41 | {post.rePostId && ( 42 |
43 | 49 | 53 | 54 | {post.user.displayName} reposted 55 |
56 | )} 57 | {/* POST CONTENT */} 58 |
59 | {/* AVATAR */} 60 | 61 |
66 | 73 |
74 | 75 | {/* CONTENT */} 76 |
77 | {/* TOP */} 78 |
79 | 83 |
88 | 95 |
96 |
101 |

102 | {originalPost.user.displayName} 103 |

104 | 107 | @{originalPost.user.username} 108 | 109 | {type !== "status" && ( 110 | 111 | {format(originalPost.createdAt)} 112 | 113 | )} 114 |
115 | 116 | 117 |
118 | {/* TEXT & MEDIA */} 119 | 122 |

123 | {originalPost.desc} 124 |

125 | 126 | {originalPost.img && ( 127 |
128 | 135 |
136 | )} 137 | {originalPost.video && ( 138 |
139 |
144 | )} 145 | {type === "status" && ( 146 | 8:41 PM · Dec 5, 2024 147 | )} 148 | 156 |
157 |
158 |
159 | ); 160 | }; 161 | 162 | export default Post; 163 | -------------------------------------------------------------------------------- /src/components/PostInfo.tsx: -------------------------------------------------------------------------------- 1 | import Image from "./Image"; 2 | 3 | const PostInfo = () => { 4 | return ( 5 |
6 | 7 |
8 | ); 9 | }; 10 | 11 | export default PostInfo; 12 | -------------------------------------------------------------------------------- /src/components/PostInteractions.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { likePost, rePost, savePost } from "@/action"; 4 | import { socket } from "@/socket"; 5 | import { useUser } from "@clerk/nextjs"; 6 | import Link from "next/link"; 7 | import { useOptimistic, useState } from "react"; 8 | 9 | const PostInteractions = ({ 10 | username, 11 | postId, 12 | count, 13 | isLiked, 14 | isRePosted, 15 | isSaved, 16 | }: { 17 | username: string; 18 | postId: number; 19 | count: { likes: number; rePosts: number; comments: number }; 20 | isLiked: boolean; 21 | isRePosted: boolean; 22 | isSaved: boolean; 23 | }) => { 24 | const [state, setState] = useState({ 25 | likes: count.likes, 26 | isLiked: isLiked, 27 | rePosts: count.rePosts, 28 | isRePosted, 29 | isSaved, 30 | }); 31 | 32 | const { user } = useUser(); 33 | 34 | const likeAction = async () => { 35 | if (!user) return; 36 | 37 | if (!optimisticCount.isLiked) { 38 | socket.emit("sendNotification", { 39 | receiverUsername: username, 40 | data: { 41 | senderUsername: user.username, 42 | type: "like", 43 | link: `/${username}/status/${postId}`, 44 | }, 45 | }); 46 | } 47 | 48 | addOptimisticCount("like"); 49 | await likePost(postId); 50 | setState((prev) => { 51 | return { 52 | ...prev, 53 | likes: prev.isLiked ? prev.likes - 1 : prev.likes + 1, 54 | isLiked: !prev.isLiked, 55 | }; 56 | }); 57 | }; 58 | 59 | const rePostAction = async () => { 60 | if (!user) return; 61 | 62 | if (!optimisticCount.isRePosted) { 63 | socket.emit("sendNotification", { 64 | receiverUsername: username, 65 | data: { 66 | senderUsername: user.username, 67 | type: "rePost", 68 | link: `/${username}/status/${postId}`, 69 | }, 70 | }); 71 | } 72 | 73 | addOptimisticCount("rePost"); 74 | await rePost(postId); 75 | setState((prev) => { 76 | return { 77 | ...prev, 78 | rePosts: prev.isRePosted ? prev.rePosts - 1 : prev.rePosts + 1, 79 | isRePosted: !prev.isRePosted, 80 | }; 81 | }); 82 | }; 83 | const saveAction = async () => { 84 | addOptimisticCount("save"); 85 | await savePost(postId); 86 | setState((prev) => { 87 | return { 88 | ...prev, 89 | isSaved: !prev.isSaved, 90 | }; 91 | }); 92 | }; 93 | 94 | const [optimisticCount, addOptimisticCount] = useOptimistic( 95 | state, 96 | (prev, type: "like" | "rePost" | "save") => { 97 | if (type === "like") { 98 | return { 99 | ...prev, 100 | likes: prev.isLiked ? prev.likes - 1 : prev.likes + 1, 101 | isLiked: !prev.isLiked, 102 | }; 103 | } 104 | if (type === "rePost") { 105 | return { 106 | ...prev, 107 | rePosts: prev.isRePosted ? prev.rePosts - 1 : prev.rePosts + 1, 108 | isRePosted: !prev.isRePosted, 109 | }; 110 | } 111 | if (type === "save") { 112 | return { 113 | ...prev, 114 | isSaved: !prev.isSaved, 115 | }; 116 | } 117 | return prev; 118 | } 119 | ); 120 | return ( 121 |
122 |
123 | {/* COMMENTS */} 124 | 125 | 131 | 135 | 136 | 137 | {count.comments} 138 | 139 | 140 | {/* REPOST */} 141 |
142 | 166 |
167 | {/* LIKE */} 168 |
169 | 191 |
192 |
193 |
194 | 209 |
210 | 216 | 220 | 221 |
222 |
223 |
224 | ); 225 | }; 226 | 227 | export default PostInteractions; 228 | -------------------------------------------------------------------------------- /src/components/Recommendations.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | import Image from "./Image"; 3 | import { prisma } from "@/prisma"; 4 | import { auth } from "@clerk/nextjs/server"; 5 | 6 | const Recommendations = async () => { 7 | const { userId } = await auth(); 8 | 9 | if (!userId) return; 10 | 11 | const followingIds = await prisma.follow.findMany({ 12 | where: { followerId: userId }, 13 | select: { followingId: true }, 14 | }); 15 | 16 | const followedUserIds = followingIds.map((f) => f.followingId); 17 | 18 | const friendRecommendations = await prisma.user.findMany({ 19 | where: { 20 | id: { not: userId, notIn: followedUserIds }, 21 | followings: { some: { followerId: { in: followedUserIds } } }, 22 | }, 23 | take: 3, 24 | select: { id: true, displayName: true, username: true, img: true }, 25 | }); 26 | 27 | return ( 28 |
29 | {friendRecommendations.map((person) => ( 30 |
31 | {/* IMAGE AND USER INFO */} 32 |
33 |
34 | {person.username} 41 |
42 |
43 |

{person.displayName || person.username}

44 | @{person.username} 45 |
46 |
47 | {/* BUTTON */} 48 | 51 |
52 | ))} 53 | 54 | 55 | Show More 56 | 57 |
58 | ); 59 | }; 60 | 61 | export default Recommendations; 62 | -------------------------------------------------------------------------------- /src/components/RightBar.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | import PopularTags from "./PopularTags"; 3 | import Recommendations from "./Recommendations"; 4 | import Search from "./Search"; 5 | 6 | const RightBar = () => { 7 | return ( 8 |
9 | 10 | 11 | 12 |
13 | Terms of Service 14 | Privacy Policy 15 | Cookie Policy 16 | Accessibility 17 | Ads Info 18 | © 2025 L Corp. 19 |
20 |
21 | ); 22 | }; 23 | 24 | export default RightBar; 25 | -------------------------------------------------------------------------------- /src/components/Search.tsx: -------------------------------------------------------------------------------- 1 | import Image from "./Image" 2 | 3 | const Search = () => { 4 | return ( 5 |
6 | search 7 | 8 |
9 | ) 10 | } 11 | 12 | export default Search -------------------------------------------------------------------------------- /src/components/Share.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React, { useActionState, useEffect, useRef, useState } from "react"; 4 | import Image from "./Image"; 5 | import NextImage from "next/image"; 6 | import ImageEditor from "./ImageEditor"; 7 | import { useUser } from "@clerk/nextjs"; 8 | import { addPost } from "@/action"; 9 | 10 | const Share = () => { 11 | const [media, setMedia] = useState(null); 12 | const [isEditorOpen, setIsEditorOpen] = useState(false); 13 | const [settings, setSettings] = useState<{ 14 | type: "original" | "wide" | "square"; 15 | sensitive: boolean; 16 | }>({ 17 | type: "original", 18 | sensitive: false, 19 | }); 20 | 21 | const handleMediaChange = (e: React.ChangeEvent) => { 22 | if (e.target.files && e.target.files[0]) { 23 | setMedia(e.target.files[0]); 24 | } 25 | }; 26 | 27 | const previewURL = media ? URL.createObjectURL(media) : null; 28 | 29 | const { user } = useUser(); 30 | 31 | const [state, formAction, isPending] = useActionState(addPost, { 32 | success: false, 33 | error: false, 34 | }); 35 | 36 | const formRef = useRef(null); 37 | 38 | useEffect(() => { 39 | if (state.success) { 40 | formRef.current?.reset(); 41 | setMedia(null); 42 | setSettings({ type: "original", sensitive: false }); 43 | } 44 | }, [state]); 45 | 46 | return ( 47 |
shareAction(formData, settings)} 51 | action={formAction} 52 | > 53 | {/* AVATAR */} 54 |
55 | 56 |
57 | {/* OTHERS */} 58 |
59 | 66 | 73 | 79 | {/* PREVIEW IMAGE */} 80 | {media?.type.includes("image") && previewURL && ( 81 |
82 | 95 |
setIsEditorOpen(true)} 98 | > 99 | Edit 100 |
101 |
setMedia(null)} 104 | > 105 | X 106 |
107 |
108 | )} 109 | {media?.type.includes("video") && previewURL && ( 110 |
111 |
119 | )} 120 | {isEditorOpen && previewURL && ( 121 | setIsEditorOpen(false)} 123 | previewURL={previewURL} 124 | settings={settings} 125 | setSettings={setSettings} 126 | /> 127 | )} 128 |
129 |
130 | 138 | 147 | 154 | 161 | 168 | 175 | 182 |
183 | 189 | {state.error && ( 190 | Something went wrong! 191 | )} 192 |
193 |
194 |
195 | ); 196 | }; 197 | 198 | export default Share; 199 | -------------------------------------------------------------------------------- /src/components/Socket.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useEffect, useState } from "react"; 4 | import { socket } from "../socket"; 5 | import { useUser } from "@clerk/nextjs"; 6 | 7 | export default function Socket() { 8 | const [isConnected, setIsConnected] = useState(false); 9 | const [transport, setTransport] = useState("N/A"); 10 | 11 | const { user } = useUser(); 12 | 13 | useEffect(() => { 14 | if (socket.connected) { 15 | onConnect(); 16 | } 17 | 18 | function onConnect() { 19 | setIsConnected(true); 20 | setTransport(socket.io.engine.transport.name); 21 | 22 | socket.io.engine.on("upgrade", (transport) => { 23 | setTransport(transport.name); 24 | }); 25 | if (user) { 26 | socket.emit("newUser", user.username); 27 | } 28 | } 29 | 30 | function onDisconnect() { 31 | setIsConnected(false); 32 | setTransport("N/A"); 33 | } 34 | 35 | socket.on("connect", onConnect); 36 | socket.on("disconnect", onDisconnect); 37 | 38 | return () => { 39 | socket.off("connect", onConnect); 40 | socket.off("disconnect", onDisconnect); 41 | }; 42 | }, [user]); 43 | 44 | return ( 45 | 46 | ); 47 | } 48 | -------------------------------------------------------------------------------- /src/components/Video.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { IKVideo } from "imagekitio-next"; 3 | 4 | const urlEndpoint = process.env.NEXT_PUBLIC_URL_ENDPOINT; 5 | 6 | type VideoTypes = { 7 | path: string; 8 | className?: string; 9 | }; 10 | 11 | const Video = ({ path, className }: VideoTypes) => { 12 | return ( 13 | 23 | ); 24 | }; 25 | 26 | export default Video; 27 | -------------------------------------------------------------------------------- /src/middleware.ts: -------------------------------------------------------------------------------- 1 | import { clerkMiddleware, createRouteMatcher } from "@clerk/nextjs/server"; 2 | 3 | const isProtectedRoute = createRouteMatcher("/"); 4 | 5 | export default clerkMiddleware( 6 | async (auth, req) => { 7 | if (isProtectedRoute(req)) await auth.protect(); 8 | }, 9 | { 10 | signInUrl: "/sign-in", 11 | signUpUrl: "/sign-up", 12 | } 13 | ); 14 | 15 | export const config = { 16 | matcher: [ 17 | // Skip Next.js internals and all static files, unless found in search params 18 | "/((?!_next|[^?]*\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)", 19 | // Always run for API routes 20 | "/(api|trpc)(.*)", 21 | ], 22 | }; 23 | -------------------------------------------------------------------------------- /src/prisma.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from '@prisma/client' 2 | 3 | const globalForPrisma = globalThis as unknown as { prisma: PrismaClient } 4 | 5 | export const prisma = 6 | globalForPrisma.prisma || new PrismaClient() 7 | 8 | if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma -------------------------------------------------------------------------------- /src/providers/QueryProvider.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; 4 | 5 | export default function QueryProvider({ 6 | children, 7 | }: Readonly<{ 8 | children: React.ReactNode; 9 | }>) { 10 | const queryClient = new QueryClient(); 11 | 12 | return ( 13 | {children} 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /src/socket.ts: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { io } from "socket.io-client"; 4 | 5 | export const socket = io(); -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import ImageKit from "imagekit" 2 | 3 | export const imagekit = new ImageKit({ 4 | publicKey: process.env.NEXT_PUBLIC_PUBLIC_KEY!, 5 | privateKey: process.env.PRIVATE_KEY!, 6 | urlEndpoint: process.env.NEXT_PUBLIC_URL_ENDPOINT!, 7 | }); -------------------------------------------------------------------------------- /tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "tailwindcss"; 2 | 3 | export default { 4 | content: [ 5 | "./src/pages/**/*.{js,ts,jsx,tsx,mdx}", 6 | "./src/components/**/*.{js,ts,jsx,tsx,mdx}", 7 | "./src/app/**/*.{js,ts,jsx,tsx,mdx}", 8 | ], 9 | theme: { 10 | extend: { 11 | screens: { 12 | xsm: "500px", 13 | sm: "600px", 14 | md: "690px", 15 | lg: "988px", 16 | xl: "1078px", 17 | xxl: "1265px", 18 | }, 19 | colors: { 20 | textGray: "#71767b", 21 | textGrayLight: "#e7e9ea", 22 | borderGray: "#2f3336", 23 | inputGray: "#202327", 24 | iconBlue: "#1d9bf0", 25 | iconGreen: "#00ba7c", 26 | iconPink: "#f91880", 27 | }, 28 | }, 29 | }, 30 | plugins: [], 31 | } satisfies Config; 32 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2017", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "noEmit": true, 9 | "esModuleInterop": true, 10 | "module": "esnext", 11 | "moduleResolution": "bundler", 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "jsx": "preserve", 15 | "incremental": true, 16 | "plugins": [ 17 | { 18 | "name": "next" 19 | } 20 | ], 21 | "paths": { 22 | "@/*": ["./src/*"] 23 | } 24 | }, 25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 26 | "exclude": ["node_modules"] 27 | } 28 | --------------------------------------------------------------------------------