├── .env.example ├── .gitignore ├── LICENSE ├── README.md ├── next.config.js ├── package-lock.json ├── package.json ├── postcss.config.cjs ├── prettier.config.cjs ├── prisma └── schema.prisma ├── public ├── bird.png ├── default.png ├── favicon.ico └── twitter-logo.svg ├── server └── src │ ├── prisma │ └── prisma.ts │ ├── router │ ├── root.ts │ └── routes │ │ ├── tweetRouter │ │ ├── getTweets.ts │ │ ├── index.ts │ │ ├── newTweet.ts │ │ ├── tweetActions.ts │ │ └── tweetRouter.ts │ │ └── userRouter │ │ ├── followUser.ts │ │ ├── getUserFollowers.ts │ │ ├── index.ts │ │ ├── updateBadge.ts │ │ ├── updateImg.ts │ │ └── userRouter.ts │ ├── trpc │ └── trpc.ts │ └── utils │ ├── TO_REMOVE.ts │ ├── removeProperties.ts │ └── uploadImg.ts ├── src ├── @types │ └── index.ts ├── Auth │ └── Auth.tsx ├── components │ ├── Avatar.tsx │ ├── DirectMessage │ │ ├── DirectMessage.tsx │ │ ├── MessageInput.tsx │ │ ├── Messagehead.tsx │ │ └── index.tsx │ ├── MainButton.tsx │ ├── MainTweet │ │ ├── Body.tsx │ │ ├── Counter.tsx │ │ ├── MainTweet.tsx │ │ ├── TweetActions.tsx │ │ ├── TweetActionsReply.tsx │ │ ├── TweetMetadata.tsx │ │ └── index.ts │ ├── NewTweets.tsx │ ├── NextLink.tsx │ ├── PageHead.tsx │ ├── PickVerificationIcon.tsx │ ├── SEO.tsx │ ├── SidebarLeft │ │ ├── Logo.tsx │ │ ├── NavItem.tsx │ │ ├── SidebarLeft.tsx │ │ ├── index.ts │ │ └── navItems.tsx │ ├── SidebarRight │ │ ├── SidebarRight.tsx │ │ └── index.ts │ ├── Spinner.tsx │ ├── TweetBody.tsx │ ├── TweetDetails │ │ ├── Avatar.tsx │ │ ├── Body.tsx │ │ ├── TweetActions.tsx │ │ ├── TweetDetails.tsx │ │ ├── TweetDetailsMetaData.tsx │ │ ├── TweetDetailsReply │ │ │ ├── Counter.tsx │ │ │ ├── TweetDetailsReply.tsx │ │ │ ├── TweetMetadata.tsx │ │ │ └── index.ts │ │ ├── TweetMetadata.tsx │ │ ├── TweetMetrics.tsx │ │ └── index.ts │ ├── TweetOptions │ │ ├── TweetOptions.tsx │ │ └── index.ts │ ├── TweetReply │ │ ├── Avatar.tsx │ │ ├── Body.tsx │ │ ├── TweetActions.tsx │ │ ├── TweetMetadata.tsx │ │ ├── TweetReply.tsx │ │ └── index.ts │ ├── UserMetadata │ │ └── UserMetadata.tsx │ ├── inputs │ │ ├── ReplyInput.tsx │ │ └── TweetInput │ │ │ ├── FilePreview.tsx │ │ │ ├── OtherIcons.tsx │ │ │ ├── TweetInput.tsx │ │ │ └── index.tsx │ ├── modals │ │ ├── EditProfileModal.tsx │ │ ├── ReplyModal.tsx │ │ ├── SigninModal.tsx │ │ ├── TweetModal.tsx │ │ ├── VerifiedDropdown.tsx │ │ └── VerifiedModal.tsx │ └── pageComponents │ │ ├── bookmarks │ │ └── BookmarksContent.tsx │ │ ├── followers │ │ └── FollowersContent.tsx │ │ ├── home │ │ └── HomeContent.tsx │ │ ├── messages │ │ └── MessagesContent.tsx │ │ ├── profile │ │ ├── EditProfileBtn.tsx │ │ ├── FollowStats.tsx │ │ ├── Joined.tsx │ │ ├── ProfileContent.tsx │ │ └── Url.tsx │ │ ├── settings │ │ └── SettingsContent.tsx │ │ └── tweet │ │ └── TweetContent.tsx ├── hooks │ ├── getUserSession.tsx │ ├── registerListener.tsx │ └── useFormattedDate.tsx ├── icons │ ├── CameraPlusIcon.tsx │ ├── CloseIcon.tsx │ ├── social │ │ ├── discord.tsx │ │ ├── github.tsx │ │ ├── google.tsx │ │ └── twitter.tsx │ ├── tweet │ │ ├── Arrow.tsx │ │ ├── LikeIcon.tsx │ │ ├── ReplyIcon.tsx │ │ ├── RetweetIcon.tsx │ │ ├── ShareIcon.tsx │ │ └── VerifiedIcon.tsx │ └── verified │ │ ├── Blue.tsx │ │ ├── Gold.tsx │ │ ├── Gray.tsx │ │ ├── Red.tsx │ │ └── index.tsx ├── pages │ ├── [username] │ │ ├── followers.tsx │ │ ├── following.tsx │ │ └── index.tsx │ ├── _app.tsx │ ├── api │ │ ├── auth │ │ │ └── [...nextauth].ts │ │ └── trpc │ │ │ └── [trpc].ts │ ├── bookmarks.tsx │ ├── home.tsx │ ├── index.tsx │ ├── messages.tsx │ ├── settings.tsx │ └── tweet │ │ └── [tweetId].tsx ├── styles │ └── globals.css └── utils │ ├── comporessImage.ts │ ├── date.ts │ ├── limitText.ts │ ├── trpc.ts │ └── updateSession.ts ├── tailwind.config.cjs └── tsconfig.json /.env.example: -------------------------------------------------------------------------------- 1 | 2 | # Required 3 | SUPABASE_URL=https://.supabase.co 4 | SUPABASE_KEY= 5 | SUPABASE_BUCKET= 6 | IMAGE_SERVER=https://.supabase.co/storage/v1/object/public/bucket-name 7 | 8 | # https://www.prisma.io/docs/guides/database/supabase#specific-considerations 9 | DATABASE_URL="postgres://postgres:@db..supabase.co:5432/postgres" 10 | 11 | 12 | NEXTAUTH_URL=http://localhost:3000 13 | NEXTAUTH_SECRET= 14 | SERVER_SECRET= 15 | 16 | # Optional 17 | GITHUB_ID= 18 | GITHUB_SECRET= 19 | 20 | GOOGLE_CLIENT_ID= 21 | GOOGLE_CLIENT_SECRET= 22 | 23 | DISCORD_CLIENT_ID= 24 | DISCORD_CLIENT_SECRET= 25 | -------------------------------------------------------------------------------- /.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 | # database 12 | /prisma/db.sqlite 13 | /prisma/db.sqlite-journal 14 | 15 | # next.js 16 | /.next/ 17 | /out/ 18 | next-env.d.ts 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 | # local env files 34 | # do not commit any .env files to git, except for the .env.example file. https://create.t3.gg/en/usage/env-variables#using-environment-variables 35 | .env 36 | .env*.local 37 | 38 | # vercel 39 | .vercel 40 | 41 | # typescript 42 | *.tsbuildinfo 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Aland Sleman 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 | This is an open source Twitter clone, Built with T3 Stack + NextAuth + Postgres(Supabase) + Prisma, It's a simple clone and may not be secure, So it's not recommended for production use. 2 | 3 | **Requirements** 4 | 5 | * Postgres Database: I recommend Supabase, Register a supabase account > Create a new project > Create a new database. 6 | 7 | * Supabase Storage: Your project > Create a new Bucket > Add this policy to be able upload images via our backend: 8 | `CREATE POLICY "" ON storage.objects FOR INSERT TO public WITH CHECK (bucket_id = '');` 9 | 10 | 11 | **How to Run Locally** 12 | 13 | To run the project locally, follow these steps: 14 | 15 | * Clone the project `git clone https://github.com/AlandSleman/t3-twitter-clone` 16 | * Copy the contents of the .env.example file into a new file named .env, then replace the values with your own. 17 | * Install the project dependencies by running `npm install` 18 | * Push the Prisma schema to the database `npx prisma db push` 19 | * Build the project `npm run build` 20 | * Start the project `npm start` 21 | 22 | If you want to use Express.js for the backend instead of Next.js, Check the other branch `with-express` 23 | 24 | 25 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | typescript: { 3 | ignoreBuildErrors: true, 4 | }, 5 | } 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "t3-twitter-clone", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "build": "next build", 7 | "dev": "next dev", 8 | "lint": "next lint", 9 | "start": "next start" 10 | }, 11 | "dependencies": { 12 | "@headlessui/react": "^1.7.10", 13 | "@heroicons/react": "^2.0.16", 14 | "@prisma/client": "^4.10.1", 15 | "@radix-ui/react-icons": "^1.2.0", 16 | "@radix-ui/react-menubar": "^1.0.0", 17 | "@supabase/supabase-js": "^2.10.0", 18 | "@tanstack/react-query": "^4.20.0", 19 | "@trpc/client": "^10.9.0", 20 | "@trpc/next": "^10.9.0", 21 | "@trpc/react-query": "^10.9.0", 22 | "@trpc/server": "^10.9.0", 23 | "@vercel/analytics": "^0.1.11", 24 | "bcrypt": "^5.1.0", 25 | "clsx": "^1.2.1", 26 | "cors": "^2.8.5", 27 | "dotenv": "^16.0.3", 28 | "framer-motion": "^9.0.3", 29 | "image-conversion": "^2.1.1", 30 | "js-cookie": "^3.0.1", 31 | "jsonwebtoken": "^9.0.0", 32 | "moment": "^2.29.4", 33 | "next": "13.1.6", 34 | "next-auth": "^4.20.1", 35 | "next-themes": "^0.2.1", 36 | "react": "18.2.0", 37 | "react-dom": "18.2.0", 38 | "react-hook-form": "^7.43.1", 39 | "react-hot-toast": "^2.4.0", 40 | "react-intersection-observer": "^9.4.3", 41 | "react-textarea-autosize": "^8.4.0", 42 | "superjson": "1.9.1", 43 | "uuid": "^9.0.0", 44 | "zod": "^3.20.2" 45 | }, 46 | "devDependencies": { 47 | "@types/bcrypt": "^5.0.0", 48 | "@types/js-cookie": "^3.0.2", 49 | "@types/jsonwebtoken": "^9.0.1", 50 | "@types/lodash": "^4.14.191", 51 | "@types/node": "^18.11.18", 52 | "@types/prettier": "^2.7.2", 53 | "@types/react": "^18.0.26", 54 | "@types/react-dom": "^18.0.10", 55 | "autoprefixer": "^10.4.7", 56 | "ignore-loader": "^0.1.2", 57 | "postcss": "^8.4.14", 58 | "prettier": "^2.8.3", 59 | "prettier-plugin-tailwindcss": "^0.2.2", 60 | "prisma": "^4.10.1", 61 | "sass": "^1.58.3", 62 | "tailwindcss": "^3.2.0", 63 | "typescript": "^4.9.4" 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /postcss.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /prettier.config.cjs: -------------------------------------------------------------------------------- 1 | /** @type {import("prettier").Config} */ 2 | module.exports = { 3 | plugins: [require.resolve("prettier-plugin-tailwindcss")], 4 | }; 5 | -------------------------------------------------------------------------------- /prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | generator client { 2 | provider = "prisma-client-js" 3 | } 4 | 5 | datasource db { 6 | provider = "postgres" 7 | url = env("DATABASE_URL") 8 | } 9 | 10 | model User { 11 | id String @id @default(uuid()) 12 | username String @unique 13 | name String? 14 | bio String? @db.Text 15 | website String? 16 | email String? @unique 17 | provider String 18 | password String? 19 | badge String? 20 | bgImage String? 21 | profileImage String? 22 | createdAt DateTime @default(now()) 23 | followersCount Int @default(0) 24 | followingCount Int @default(0) 25 | likeCount Int @default(0) 26 | tweets Tweet[] 27 | likes Like[] 28 | retweets Retweet[] 29 | replies Reply[] 30 | bookmarks Bookmark[] 31 | messagesSent Message[] @relation("sender") 32 | messagesReceived Message[] @relation("recipient") 33 | followers UserFollow[] @relation("follower") 34 | following UserFollow[] @relation("following") 35 | } 36 | 37 | model UserFollow { 38 | id String @id @default(uuid()) 39 | follower User @relation("follower", fields: [followerId], references: [id]) 40 | followerId String 41 | following User @relation("following", fields: [followingId], references: [id]) 42 | followingId String 43 | createdAt DateTime @default(now()) 44 | } 45 | 46 | model Message { 47 | id String @id @default(uuid()) 48 | sender User @relation("sender", fields: [senderId], references: [id]) 49 | senderId String 50 | recipient User @relation("recipient", fields: [recipientId], references: [id]) 51 | recipientId String 52 | body String @db.Text 53 | image String? 54 | createdAt DateTime @default(now()) 55 | } 56 | 57 | model Tweet { 58 | id String @id @default(uuid()) 59 | userId String 60 | body String @db.Text 61 | images String[] 62 | likeCount Int @default(0) 63 | retweetCount Int @default(0) 64 | replyCount Int @default(0) 65 | createdAt DateTime @default(now()) 66 | user User @relation(fields: [userId], references: [id]) 67 | likes Like[] 68 | retweets Retweet[] 69 | replies Reply[] 70 | Bookmark Bookmark[] 71 | } 72 | 73 | model Retweet { 74 | id String @id @default(uuid()) 75 | tweetId String 76 | userId String 77 | retweetDate DateTime @default(now()) 78 | tweet Tweet @relation(fields: [tweetId], references: [id]) 79 | user User @relation(fields: [userId], references: [id]) 80 | } 81 | 82 | model Like { 83 | id String @id @default(uuid()) 84 | user User @relation(fields: [userId], references: [id]) 85 | userId String 86 | tweet Tweet @relation(fields: [tweetId], references: [id]) 87 | tweetId String 88 | createdAt DateTime @default(now()) 89 | } 90 | 91 | model Reply { 92 | id String @id @default(uuid()) 93 | user User @relation(fields: [userId], references: [id]) 94 | userId String 95 | tweet Tweet @relation(fields: [tweetId], references: [id]) 96 | tweetId String 97 | body String @db.Text 98 | images String[] 99 | createdAt DateTime @default(now()) 100 | } 101 | 102 | model Bookmark { 103 | id String @id @default(uuid()) 104 | user User @relation(fields: [userId], references: [id]) 105 | userId String 106 | tweet Tweet @relation(fields: [tweetId], references: [id]) 107 | tweetId String 108 | createdAt DateTime @default(now()) 109 | } 110 | -------------------------------------------------------------------------------- /public/bird.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KryptXBSA/t3-twitter-clone/82fb33fb885ffbd0edd5319b6c76319ce6ccf74f/public/bird.png -------------------------------------------------------------------------------- /public/default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KryptXBSA/t3-twitter-clone/82fb33fb885ffbd0edd5319b6c76319ce6ccf74f/public/default.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KryptXBSA/t3-twitter-clone/82fb33fb885ffbd0edd5319b6c76319ce6ccf74f/public/favicon.ico -------------------------------------------------------------------------------- /public/twitter-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /server/src/prisma/prisma.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from "@prisma/client"; 2 | const globalForPrisma = globalThis as unknown as { prisma: PrismaClient }; 3 | 4 | export const prisma = 5 | globalForPrisma.prisma || 6 | new PrismaClient({ 7 | // log: ["query", "error", "warn"], 8 | }); 9 | 10 | // if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = prisma; 11 | -------------------------------------------------------------------------------- /server/src/router/root.ts: -------------------------------------------------------------------------------- 1 | import { createTRPCRouter } from "../trpc/trpc"; 2 | import { tweetRouter } from "./routes/tweetRouter"; 3 | import { userRouter } from "./routes/userRouter"; 4 | 5 | export const appRouter = createTRPCRouter({ 6 | user: userRouter, 7 | tweet: tweetRouter, 8 | }); 9 | 10 | export type AppRouter = typeof appRouter; 11 | -------------------------------------------------------------------------------- /server/src/router/routes/tweetRouter/getTweets.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | import { protectedProcedure, publicProcedure } from "../../../trpc/trpc"; 3 | import _ from "lodash"; 4 | import { TO_REMOVE } from "../../../utils/TO_REMOVE"; 5 | import { removeProperties } from "../../../utils/removeProperties"; 6 | 7 | export const getAllTweets = protectedProcedure 8 | .input(z.object({ skip: z.number().nullish() })) 9 | .mutation(async ({ ctx, input }) => { 10 | const pageSize = 10; 11 | let tweets = await ctx.prisma.tweet.findMany({ 12 | orderBy: { createdAt: "desc" }, 13 | include: { 14 | user: true, 15 | likes: true, 16 | replies: true, 17 | retweets: true, 18 | }, 19 | take: pageSize + 1, // fetch one more tweet than needed 20 | skip: input.skip || 0, 21 | }); 22 | 23 | const hasMore = tweets.length > pageSize; 24 | if (hasMore) { 25 | tweets.pop(); 26 | } 27 | 28 | return { success: true, tweets:removeProperties(tweets) , hasMore }; 29 | }); 30 | export const getTweet = publicProcedure 31 | .input(z.object({ id: z.string().uuid() })) 32 | .query(async ({ ctx, input }) => { 33 | let tweet = await ctx.prisma.tweet.findUnique({ 34 | where: { id: input.id }, 35 | include: { 36 | user: true, 37 | likes: true, 38 | replies: { include: { user: true } }, 39 | retweets: true, 40 | }, 41 | }); 42 | 43 | return { success: true, tweet:removeProperties(tweet) }; 44 | }); 45 | -------------------------------------------------------------------------------- /server/src/router/routes/tweetRouter/index.ts: -------------------------------------------------------------------------------- 1 | import {tweetRouter} from "./tweetRouter" 2 | 3 | export {tweetRouter} 4 | -------------------------------------------------------------------------------- /server/src/router/routes/tweetRouter/newTweet.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | import { protectedProcedure } from "../../../trpc/trpc"; 3 | import { uploadImg } from "../../../utils/uploadImg"; 4 | 5 | export const newTweet = protectedProcedure 6 | .input( 7 | z.object({ 8 | body: z.string().trim().min(3), 9 | image: z.string().nullish(), 10 | }) 11 | ) 12 | .mutation(async ({ ctx, input }) => { 13 | let imageUrl = ""; 14 | if (input.image) { 15 | imageUrl = await uploadImg(input.image); 16 | } 17 | let newTweet = await ctx.prisma.tweet.create({ 18 | data: { 19 | body: limitTextLines(input.body), 20 | images: [imageUrl], 21 | userId: ctx.session.id, 22 | }, 23 | include: { 24 | user: true, 25 | likes: true, 26 | replies: true, 27 | retweets: true, 28 | }, 29 | }); 30 | return { success: true, tweet: newTweet }; 31 | }); 32 | 33 | function limitTextLines(text: string) { 34 | const newText = text.replace(/(\n{3,})/g, "\n\n"); 35 | return newText; 36 | } 37 | -------------------------------------------------------------------------------- /server/src/router/routes/tweetRouter/tweetActions.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | import { protectedProcedure } from "../../../trpc/trpc"; 3 | 4 | export const likeTweet = protectedProcedure 5 | .input(z.object({ id: z.string().uuid() })) 6 | .mutation(async ({ ctx, input }) => { 7 | const existingLike = await ctx.prisma.like.findFirst({ 8 | where: { 9 | userId: ctx.session.id, 10 | tweetId: input.id, 11 | }, 12 | }); 13 | let updatedTweet; 14 | 15 | if (!existingLike) { 16 | updatedTweet = await ctx.prisma.tweet.update({ 17 | where: { id: input.id }, 18 | data: { 19 | likeCount: { increment: 1 }, 20 | likes: { 21 | create: { 22 | user: { connect: { id: ctx.session.id } }, 23 | }, 24 | }, 25 | }, 26 | include: { 27 | likes: true, 28 | }, 29 | }); 30 | } else { 31 | updatedTweet = await ctx.prisma.tweet.update({ 32 | where: { id: input.id }, 33 | data: { 34 | likeCount: { decrement: 1 }, 35 | likes: { 36 | delete: { 37 | id: existingLike.id, 38 | }, 39 | }, 40 | }, 41 | include: { 42 | likes: true, 43 | }, 44 | }); 45 | } 46 | return { success: true }; 47 | }); 48 | 49 | export const replyTweet = protectedProcedure 50 | .input(z.object({ id: z.string().uuid(), body: z.string().min(1) })) 51 | .mutation(async ({ ctx, input }) => { 52 | const [reply, tweet] = await ctx.prisma.$transaction([ 53 | ctx.prisma.reply.create({ 54 | data: { 55 | body: input.body, 56 | user: { connect: { id: ctx.session.id } }, 57 | tweet: { connect: { id: input.id } }, 58 | }, 59 | include: { user: true }, 60 | }), 61 | ctx.prisma.tweet.update({ 62 | where: { id: input.id }, 63 | data: { replyCount: { increment: 1 } }, 64 | }), 65 | ]); 66 | return { success: true, reply }; 67 | }); 68 | 69 | export const reTweet = protectedProcedure 70 | .input(z.object({ id: z.string().uuid() })) 71 | .mutation(async ({ ctx, input }) => { 72 | const existingRetweet = await ctx.prisma.retweet.findFirst({ 73 | where: { 74 | userId: ctx.session.id, 75 | tweetId: input.id, 76 | }, 77 | }); 78 | let updatedTweet; 79 | 80 | if (!existingRetweet) { 81 | updatedTweet = await ctx.prisma.tweet.update({ 82 | where: { id: input.id }, 83 | data: { 84 | retweetCount: { increment: 1 }, 85 | retweets: { 86 | create: { 87 | user: { connect: { id: ctx.session.id } }, 88 | }, 89 | }, 90 | }, 91 | include: { 92 | retweets: true, 93 | }, 94 | }); 95 | } else { 96 | updatedTweet = await ctx.prisma.tweet.update({ 97 | where: { id: input.id }, 98 | data: { 99 | retweetCount: { decrement: 1 }, 100 | retweets: { 101 | delete: { 102 | id: existingRetweet.id, 103 | }, 104 | }, 105 | }, 106 | include: { 107 | retweets: true, 108 | }, 109 | }); 110 | } 111 | 112 | return { success: true, data: JSON.stringify(updatedTweet) }; 113 | }); 114 | -------------------------------------------------------------------------------- /server/src/router/routes/tweetRouter/tweetRouter.ts: -------------------------------------------------------------------------------- 1 | import { t } from "../../../trpc/trpc"; 2 | import { getAllTweets, getTweet } from "./getTweets"; 3 | import { likeTweet, replyTweet, reTweet } from "./tweetActions"; 4 | import { newTweet } from "./newTweet"; 5 | 6 | export const tweetRouter = t.router({ 7 | getTweet, 8 | getAllTweets, 9 | likeTweet, 10 | replyTweet, 11 | reTweet, 12 | newTweet, 13 | }); 14 | -------------------------------------------------------------------------------- /server/src/router/routes/userRouter/followUser.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | import { protectedProcedure } from "../../../trpc/trpc"; 3 | 4 | export const followUser = protectedProcedure 5 | .input(z.object({ id: z.string().uuid() })) 6 | .mutation(async ({ ctx, input }) => { 7 | const existingFollow = await ctx.prisma.userFollow.findFirst({ 8 | where: { 9 | AND: [ 10 | { followingId: { equals: ctx.session.id } }, 11 | { followerId: { equals: input.id } }, 12 | ], 13 | }, 14 | }); 15 | 16 | let updatedUser; 17 | if (!existingFollow) { 18 | // User is not following, add new row in UserFollow and increment following count of the user and increment the followers count of the other user 19 | let createRecord = await ctx.prisma.userFollow.create({ 20 | data: { 21 | followingId: ctx.session.id, 22 | followerId: input.id, 23 | }, 24 | }); 25 | 26 | updatedUser = await ctx.prisma.user.update({ 27 | where: { id: ctx.session.id }, 28 | data: { 29 | followingCount: { 30 | increment: 1, 31 | }, 32 | }, 33 | include: { 34 | "followers": true, 35 | }, 36 | }); 37 | 38 | await ctx.prisma.user.update({ 39 | where: { id: input.id }, 40 | data: { 41 | followersCount: { 42 | increment: 1, 43 | }, 44 | }, 45 | }); 46 | } else { 47 | // User is already following, remove the row and decrement both 48 | let deleteRecord = await ctx.prisma.userFollow.delete({ 49 | where: { id: existingFollow.id }, 50 | }); 51 | 52 | updatedUser = await ctx.prisma.user.update({ 53 | where: { id: ctx.session.id }, 54 | data: { 55 | followingCount: { 56 | decrement: 1, 57 | }, 58 | }, 59 | }); 60 | 61 | await ctx.prisma.user.update({ 62 | where: { id: input.id }, 63 | data: { 64 | followersCount: { 65 | decrement: 1, 66 | }, 67 | }, 68 | }); 69 | } 70 | 71 | return { success: true }; 72 | }); 73 | -------------------------------------------------------------------------------- /server/src/router/routes/userRouter/getUserFollowers.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | import { protectedProcedure } from "../../../trpc/trpc"; 3 | import { removeProperties } from "../../../utils/removeProperties"; 4 | 5 | export const getUserFollowers = protectedProcedure 6 | .input(z.object({ username: z.string() })) 7 | .query(async ({ ctx, input }) => { 8 | let userFollowers = await ctx.prisma.user.findUnique({ 9 | where: { username: input.username }, 10 | include: { 11 | following: { 12 | include: { 13 | follower: { 14 | select: { 15 | bio: true, 16 | username: true, 17 | name: true, 18 | profileImage: true, 19 | badge: true, 20 | }, 21 | }, 22 | }, 23 | }, 24 | followers: { 25 | include: { 26 | following: { 27 | select: { 28 | bio: true, 29 | username: true, 30 | name: true, 31 | profileImage: true, 32 | badge: true, 33 | }, 34 | }, 35 | }, 36 | }, 37 | }, 38 | }); 39 | 40 | return { success: true, userFollowers:removeProperties(userFollowers) }; 41 | }); 42 | -------------------------------------------------------------------------------- /server/src/router/routes/userRouter/index.ts: -------------------------------------------------------------------------------- 1 | import {userRouter} from "./userRouter" 2 | 3 | export {userRouter} 4 | -------------------------------------------------------------------------------- /server/src/router/routes/userRouter/updateBadge.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | import { protectedProcedure } from "../../../trpc/trpc"; 3 | 4 | export const updateBadge = protectedProcedure 5 | .input(z.object({ badge: z.string() })) 6 | .mutation(async ({ ctx, input }) => { 7 | 8 | let user = await ctx.prisma.user.update({ 9 | where: { id: ctx.session.id }, 10 | data: { 11 | badge: input.badge, 12 | }, 13 | }); 14 | return { success: true }; 15 | }); 16 | -------------------------------------------------------------------------------- /server/src/router/routes/userRouter/updateImg.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | import { protectedProcedure } from "../../../trpc/trpc"; 3 | import { uploadImg } from "../../../utils/uploadImg"; 4 | 5 | export const updateImg = protectedProcedure 6 | .input(z.object({ bgImg: z.string().nullish(), profileImg: z.string().nullish() })) 7 | .mutation(async ({ ctx, input }) => { 8 | let currentUser = await ctx.prisma.user.findUnique({ 9 | where: { id: ctx.session.id }, 10 | }); 11 | 12 | let bgImage = ""; 13 | if (input.bgImg) { 14 | bgImage = await uploadImg(input.bgImg); 15 | } else { 16 | bgImage = currentUser?.bgImage!; 17 | } 18 | 19 | let profileImage = ""; 20 | if (input.profileImg) { 21 | profileImage = await uploadImg(input.profileImg); 22 | } else { 23 | profileImage = currentUser?.profileImage!; 24 | } 25 | 26 | await ctx.prisma.user.update({ 27 | where: { id: ctx.session.id }, 28 | data: { 29 | bgImage, 30 | profileImage, 31 | }, 32 | }); 33 | 34 | return { success: true }; 35 | }); 36 | -------------------------------------------------------------------------------- /server/src/router/routes/userRouter/userRouter.ts: -------------------------------------------------------------------------------- 1 | import { protectedProcedure, publicProcedure, t } from "../../../trpc/trpc"; 2 | import bcrypt from "bcrypt"; 3 | import { z } from "zod"; 4 | import { followUser } from "./followUser"; 5 | import { getUserFollowers } from "./getUserFollowers"; 6 | import { updateImg } from "./updateImg"; 7 | import { updateBadge } from "./updateBadge"; 8 | import _ from "lodash"; 9 | import { removeProperties } from "../../../utils/removeProperties"; 10 | 11 | const Provider = z.enum(["credentials", "google", "github", "discord"]); 12 | 13 | export const userRouter = t.router({ 14 | updateUser: protectedProcedure 15 | .input( 16 | z 17 | .object({ 18 | // username: z.string(), 19 | name: z.string().trim().min(3).nullish(), 20 | bio: z.string().nullish(), 21 | website: z.string().nullish(), 22 | bgImage: z.string().nullish(), 23 | profileImage: z.string().nullish(), 24 | }) 25 | .refine((obj) => { 26 | if ( 27 | Object.values(obj).some((val) => val === null && val === undefined) 28 | ) { 29 | throw new Error("At least one value has to be defined"); 30 | } 31 | return true; 32 | }) 33 | ) 34 | .mutation(async ({ input, ctx }) => { 35 | let user = await ctx.prisma.user.update({ 36 | where: { id: ctx.session.id }, 37 | data: { 38 | ...input, 39 | }, 40 | }); 41 | return { success: true, user }; 42 | }), 43 | getUser: protectedProcedure 44 | .input( 45 | z 46 | .object({ id: z.string().nullish(), username: z.string().nullish() }) 47 | .refine((obj) => { 48 | if (!obj.id && !obj.username) { 49 | throw new Error("At least one of id or username must be defined"); 50 | } 51 | return true; 52 | }) 53 | ) 54 | .query(async ({ input, ctx }) => { 55 | let user = await ctx.prisma.user.findFirst({ 56 | where: { 57 | OR: [{ id: input.id! }, { username: input.username! }], 58 | }, 59 | include: { 60 | followers: true, 61 | following: true, 62 | tweets: { 63 | orderBy: { createdAt: "desc" }, 64 | include: { 65 | user: true, 66 | likes: true, 67 | replies: true, 68 | retweets: true, 69 | }, 70 | }, 71 | }, 72 | }); 73 | 74 | return { success: true, user: removeProperties(user) }; 75 | }), 76 | createUser: publicProcedure 77 | .input( 78 | z 79 | .object({ 80 | name: z.string().trim().min(3).nullish(), 81 | username: z.string().trim().min(3), 82 | password: z.string().nullish(), 83 | provider: Provider, 84 | email: z.string().nullish(), 85 | }) 86 | .refine((data) => { 87 | // error if not credentials and there is no email 88 | if (data.provider !== "credentials" && !data.email) { 89 | throw new z.ZodError([ 90 | { 91 | path: ["email"], 92 | message: 'Email should be provided if !=="credentials"', 93 | code: "custom", 94 | }, 95 | ]); 96 | } 97 | // Throw an error if email is provided and provider is "credentials" 98 | if (data.provider === "credentials" && data.email) { 99 | throw new z.ZodError([ 100 | { 101 | path: ["email"], 102 | message: 'Email should be null for provider "credentials"', 103 | code: "custom", 104 | }, 105 | ]); 106 | } 107 | if (data.provider === "credentials" && !data.password) { 108 | throw new z.ZodError([ 109 | { 110 | path: ["password"], 111 | message: "Password should be provided", 112 | code: "custom", 113 | }, 114 | ]); 115 | } 116 | return true; 117 | }) 118 | ) 119 | .mutation(async ({ ctx, input }) => { 120 | // credentials are only username and password 121 | // if (input.provider === "credentials" && input.email) 122 | // throw new TRPCError({ 123 | // data: "Invalid request", 124 | // code: "BAD_REQUEST", 125 | // }); 126 | let existingUser = await ctx.prisma.user.findFirst({ 127 | where: { 128 | OR: [{ email: input.email }, { username: input.username }], 129 | }, 130 | }); 131 | 132 | // if no user and it's not credentials just sign up via provider 133 | if (input.provider !== "credentials" && !existingUser) { 134 | let user = await ctx.prisma.user.create({ 135 | // @ts-ignore 136 | data: { 137 | ...input, 138 | }, 139 | }); 140 | return { 141 | success: true, 142 | data: user, 143 | }; 144 | } 145 | 146 | // if no user and provider is credentials just sign up 147 | if (!existingUser && input.provider === "credentials") { 148 | // Create a new user 149 | let user = await ctx.prisma.user.create({ 150 | // @ts-ignore the input is already validated above no worries 151 | data: { 152 | ...input, 153 | password: bcrypt.hashSync(input.password!, 10), // hash the password 154 | }, 155 | }); 156 | 157 | // Create a new session for the user 158 | // let session = await createSession(user, ctx); 159 | 160 | return { 161 | success: true, 162 | data: user, 163 | }; 164 | } 165 | if (existingUser) { 166 | // if it's via email just login 167 | if (input.provider !== "credentials") { 168 | return { 169 | success: true, 170 | data: existingUser, 171 | }; 172 | } 173 | if ( 174 | existingUser.password && 175 | input.password && 176 | bcrypt.compareSync(input.password, existingUser.password) 177 | ) { 178 | // Create a new session for the user 179 | // let session = await createSession(existingUser, ctx); 180 | // return { success: true, data: JSON.stringify(session) }; 181 | return { 182 | success: true, 183 | data: existingUser, 184 | }; 185 | } else { 186 | return { success: false, data: "Invalid password" }; 187 | } 188 | } 189 | 190 | return { success: true, data: "no" }; 191 | }), 192 | followUser, 193 | getUserFollowers, 194 | updateImg, 195 | updateBadge, 196 | topUsers: protectedProcedure.query(async ({ ctx }) => { 197 | let users = await ctx.prisma.user.findMany({ 198 | orderBy: { followersCount: "desc" }, 199 | take: 3, 200 | }); 201 | return { success: true, users }; 202 | }), 203 | }); 204 | -------------------------------------------------------------------------------- /server/src/trpc/trpc.ts: -------------------------------------------------------------------------------- 1 | import { initTRPC, TRPCError } from "@trpc/server"; 2 | import superjson from "superjson"; 3 | import { prisma } from "../prisma/prisma"; 4 | import { User } from "@prisma/client"; 5 | import { type CreateNextContextOptions } from "@trpc/server/adapters/next"; 6 | import { getServerAuthSession } from "pages/api/auth/[...nextauth]"; 7 | 8 | type CreateContextOptions = { 9 | session: User; 10 | isServer: boolean; 11 | }; 12 | 13 | const createInnerTRPCContext = (opts: CreateContextOptions) => { 14 | return { 15 | session: opts.session, 16 | isServer: opts.isServer, 17 | prisma, 18 | }; 19 | }; 20 | 21 | export const createContext = async (opts: CreateNextContextOptions) => { 22 | const { req, res } = opts; 23 | 24 | const session = await getServerAuthSession({ req, res }); 25 | return createInnerTRPCContext({ 26 | session: session?.userData!, 27 | isServer: req?.headers?.pass === process.env.SERVER_SECRET, 28 | }); 29 | }; 30 | 31 | export const t = initTRPC.context().create({ 32 | transformer: superjson, 33 | errorFormatter({ shape }) { 34 | return shape; 35 | }, 36 | }); 37 | 38 | export const createTRPCRouter = t.router; 39 | 40 | export const publicProcedure = t.procedure; 41 | 42 | /** 43 | * Reusable middleware that enforces users are logged in before running the 44 | * procedure. 45 | */ 46 | const enforceUserIsAuthed = t.middleware(({ ctx, next }) => { 47 | if (ctx.isServer) { 48 | return next({ 49 | ctx: { 50 | // infers the `session` as non-nullable 51 | session: { ...ctx.session }, 52 | }, 53 | }); 54 | } 55 | if (!ctx.session || !ctx.session?.id) { 56 | throw new TRPCError({ code: "UNAUTHORIZED" }); 57 | } 58 | return next({ 59 | ctx: { 60 | // infers the `session` as non-nullable 61 | session: { ...ctx.session }, 62 | }, 63 | }); 64 | }); 65 | 66 | /** 67 | * Protected (authenticated) procedure 68 | * 69 | * If you want a query or mutation to ONLY be accessible to logged in users, use 70 | * this. It verifies the session is valid and guarantees `ctx.session.user` is 71 | * not null. 72 | * 73 | * @see https://trpc.io/docs/procedures 74 | */ 75 | export const protectedProcedure = t.procedure.use(enforceUserIsAuthed); 76 | -------------------------------------------------------------------------------- /server/src/utils/TO_REMOVE.ts: -------------------------------------------------------------------------------- 1 | export const TO_REMOVE=["password"] 2 | -------------------------------------------------------------------------------- /server/src/utils/removeProperties.ts: -------------------------------------------------------------------------------- 1 | import { TO_REMOVE } from "./TO_REMOVE"; 2 | 3 | export function removeProperties(obj: T, toRemove?: string[]): T { 4 | if (!toRemove) toRemove = TO_REMOVE; 5 | if (obj instanceof Date || typeof obj !== "object" || obj === null) { 6 | return obj as T; 7 | } 8 | 9 | const newObj: any = Array.isArray(obj) ? [] : {}; 10 | 11 | for (const [key, value] of Object.entries(obj)) { 12 | if (toRemove.includes(key)) { 13 | continue; 14 | } 15 | 16 | newObj[key] = removeProperties(value, toRemove); 17 | } 18 | 19 | return newObj as T; 20 | } 21 | -------------------------------------------------------------------------------- /server/src/utils/uploadImg.ts: -------------------------------------------------------------------------------- 1 | import { v4 as uuidv4 } from "uuid"; 2 | 3 | import { createClient } from "@supabase/supabase-js"; 4 | 5 | const supabase = createClient( 6 | process.env.SUPABASE_URL!, 7 | process.env.SUPABASE_KEY! 8 | ); 9 | 10 | export async function uploadImg(imageURI: string) { 11 | let imageUrl = ""; 12 | const image = imageURI; 13 | const imageName = uuidv4(); 14 | const imageData = image.replace(/^data:image\/\w+;base64,/, ""); 15 | const imageBuffer = Buffer.from(imageData, "base64"); 16 | imageUrl = process.env.IMAGE_SERVER!.endsWith("/") 17 | ? process.env.IMAGE_SERVER + imageName 18 | : process.env.IMAGE_SERVER + "/" + imageName; 19 | 20 | const { data, error } = await supabase.storage 21 | .from(process.env.SUPABASE_BUCKET!) 22 | .upload(imageName, imageBuffer, { 23 | cacheControl: "999999999", 24 | }); 25 | console.info("data:", data, "err", error); 26 | return imageUrl; 27 | } 28 | -------------------------------------------------------------------------------- /src/@types/index.ts: -------------------------------------------------------------------------------- 1 | 2 | import type { Tweet, User, Like, Retweet, Reply } from "@prisma/client"; 3 | export type UserData = User 4 | 5 | export type TweetProps = Tweet & { 6 | user: User; 7 | likes: Like[]; 8 | retweets: Retweet[]; 9 | replies: Reply[]; 10 | }; 11 | -------------------------------------------------------------------------------- /src/Auth/Auth.tsx: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | import { useSession, signIn, signOut } from "next-auth/react"; 3 | import { useTheme } from "next-themes"; 4 | import Head from "next/head"; 5 | import { ReactNode, useEffect, useRef, useState } from "react"; 6 | import SigninModal from "@components/modals/SigninModal"; 7 | import TwitterIcon from "@icons/social/twitter"; 8 | 9 | const Auth = ({ children }: { children: ReactNode }) => { 10 | const { data: session, status } = useSession(); 11 | 12 | if (status === "loading") { 13 | return ( 14 | <> 15 | 16 | Loading 17 | 18 | 19 | 20 | 21 | ); 22 | } 23 | 24 | if (status === "unauthenticated") { 25 | return ( 26 | <> 27 | 28 | Sign In 29 | 30 | 31 | 32 |
35 |
36 | 37 |
38 |
39 | 40 |
41 |
42 | 43 | ); 44 | } 45 | 46 | return <>{children}; 47 | }; 48 | 49 | export default Auth; 50 | 51 | function SignIn() { 52 | let [isOpen, setIsOpen] = useState(false); 53 | function closeModal() { 54 | setIsOpen(false); 55 | } 56 | 57 | async function signup(e: any) { 58 | e.preventDefault(); 59 | setIsOpen(!isOpen); 60 | } 61 | return ( 62 | <> 63 | 64 |
68 |
69 | 70 |

71 | See what's happening in the world right now 72 |

73 |

Join Twitter today.

74 | 77 |
78 |
79 | 80 | ); 81 | } 82 | -------------------------------------------------------------------------------- /src/components/Avatar.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import cn from"clsx" 3 | 4 | export default function Avatar({ 5 | size = 56, 6 | avatarImage, 7 | className 8 | }: { 9 | size?: number; 10 | avatarImage?: string; 11 | className?: string; 12 | }) { 13 | return ( 14 |
15 | avatar 23 |
24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /src/components/DirectMessage/DirectMessage.tsx: -------------------------------------------------------------------------------- 1 | import Avatar from "@components/Avatar"; 2 | import { MessageHead } from "./Messagehead"; 3 | import { MessageInput } from "./MessageInput"; 4 | 5 | export default function DirectMessage() { 6 | return ( 7 |
8 |
9 |
10 | 11 |
12 | 13 | 14 | 15 |
16 |
17 | 18 |
19 |
20 | ); 21 | } 22 | 23 | let body = ` 24 | adssdsdsadjhasd dashasdgdash sdahdshagd asdhhg asdddddddddhj 25 | `; 26 | function MessageBody() { 27 | return ( 28 |
29 |
50 ? " rounded-xl" : "rounded-full" 32 | } flex items-center justify-center bg-[#2B9BF0] p-2 px-3`} 33 | > 34 | {body} 35 |
36 | 41 |
42 | ); 43 | } 44 | function MessageBody2() { 45 | return ( 46 |
47 |
50 ? " rounded-xl" : "rounded-full" 50 | } flex items-center justify-center bg-[#2E3236] p-2 px-3`} 51 | > 52 | {body} 53 |
54 | 59 |
60 | ); 61 | } 62 | -------------------------------------------------------------------------------- /src/components/DirectMessage/MessageInput.tsx: -------------------------------------------------------------------------------- 1 | export function MessageInput() { 2 | return ( 3 |
4 |
8 | 15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 27 |
28 | 37 |
38 |
39 | ); 40 | } 41 | 42 | -------------------------------------------------------------------------------- /src/components/DirectMessage/Messagehead.tsx: -------------------------------------------------------------------------------- 1 | import Avatar from "@components/Avatar"; 2 | import ArrowIcon from "@icons/tweet/Arrow"; 3 | import { useRouter } from "next/router"; 4 | import React from "react"; 5 | 6 | export function MessageHead({ 7 | name, 8 | username, 9 | }: { 10 | name?: string; 11 | username?: string; 12 | }) { 13 | return ( 14 |
15 |
16 | 17 |
18 |

19 | {name || username} 20 |

21 |
22 |
23 |
24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /src/components/DirectMessage/index.tsx: -------------------------------------------------------------------------------- 1 | import DirectMessage from "./DirectMessage"; 2 | export default DirectMessage 3 | -------------------------------------------------------------------------------- /src/components/MainButton.tsx: -------------------------------------------------------------------------------- 1 | import { ButtonHTMLAttributes } from "react"; 2 | import cn from "clsx"; 3 | 4 | export default function MainButton({ 5 | text, 6 | textClassname, 7 | ...props 8 | }: { text: string; textClassname?: string } & ButtonHTMLAttributes) { 9 | return ( 10 | 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /src/components/MainTweet/Body.tsx: -------------------------------------------------------------------------------- 1 | import TweetBody from "@components/TweetBody"; 2 | import { TweetProps } from "@types"; 3 | import React from "react"; 4 | 5 | export function Body(props: TweetProps) { 6 | return ( 7 | <> 8 |

9 | 10 |

11 | {props.images[0] && ( 12 |
13 | 18 |
19 | )} 20 | 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /src/components/MainTweet/Counter.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from "react"; 2 | import { useState } from "react"; 3 | 4 | type PropsType = { 5 | num: number; 6 | }; 7 | 8 | export const Counter: React.FC = ({ num }) => { 9 | const [curValue, setCurValue] = React.useState(num); 10 | const [inputStyle, setInputStyle] = React.useState({}); 11 | const [initial, setInitial] = useState(false); 12 | 13 | useEffect(() => { 14 | initial && handleValueChange(num); 15 | setInitial(true); 16 | }, [num]); 17 | 18 | const handleValueChange = (newValue: number) => { 19 | setInputStyle({ 20 | transform: newValue > curValue ? "translateY(-100%)" : "translateY(100%)", 21 | opacity: 0, 22 | }); 23 | 24 | setTimeout(() => { 25 | setInputStyle({ 26 | transitionDuration: "0s", 27 | transform: 28 | newValue > curValue ? "translateY(100%) " : "translateY(-100%)", 29 | opacity: 0, 30 | }); 31 | 32 | setCurValue(num); 33 | 34 | setTimeout(() => { 35 | setInputStyle({ 36 | transitionDuration: "0.3s", 37 | transform: "translateY(0)", 38 | opacity: 1, 39 | }); 40 | }, 20); 41 | }, 250); 42 | }; 43 | 44 | return ( 45 | { 51 | e.preventDefault(); 52 | // handleValueChange(parseInt(e.target.value, 10), true); 53 | }, 54 | type: "text", 55 | value: curValue, 56 | }} 57 | > 58 | {curValue} 59 | 60 | ); 61 | }; 62 | -------------------------------------------------------------------------------- /src/components/MainTweet/MainTweet.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import type { Variants } from "framer-motion"; 4 | import { TweetMetadata } from "./TweetMetadata"; 5 | import { Body } from "./Body"; 6 | import { TweetActions } from "./TweetActions"; 7 | import Link from "next/link"; 8 | import NextLink from "@components/NextLink"; 9 | import Avatar from "@components/Avatar"; 10 | import { TweetProps } from "@types"; 11 | import TweetOptions from "@components/TweetOptions"; 12 | 13 | export const variants: Variants = { 14 | initial: { opacity: 0 }, 15 | animate: { opacity: 1, transition: { duration: 0.8 } }, 16 | exit: { opacity: 0, transition: { duration: 0.2 } }, 17 | }; 18 | export function MainTweet({ 19 | reply, 20 | tweet, 21 | }: { 22 | reply?: boolean; 23 | tweet: TweetProps; 24 | }) { 25 | return ( 26 | 27 |
28 |
29 |
30 | 31 | {reply && ( 32 |
33 | )} 34 |
35 |
36 | 37 | 38 | 39 | 40 | 41 |
42 | 43 |
44 |
45 |
46 | ); 47 | } 48 | -------------------------------------------------------------------------------- /src/components/MainTweet/TweetActions.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactNode, useEffect, useState } from "react"; 2 | import cn from "clsx"; 3 | import { trpc } from "@utils/trpc"; 4 | import { useSession } from "next-auth/react"; 5 | import ReplyModal from "@components/modals/ReplyModal"; 6 | import { TweetProps } from "@types"; 7 | import ReplyIcon from "@icons/tweet/ReplyIcon"; 8 | import RetweetIcon from "@icons/tweet/RetweetIcon"; 9 | import LikeIcon from "@icons/tweet/LikeIcon"; 10 | import ShareIcon from "@icons/tweet/ShareIcon"; 11 | import NextLink from "@components/NextLink"; 12 | import { Counter } from "./Counter"; 13 | import { getUserSession } from "@hooks/getUserSession"; 14 | 15 | export function TweetActions({ 16 | allDisabled, 17 | ...props 18 | }: TweetProps & { allDisabled?: boolean }) { 19 | let [isOpen, setIsOpen] = useState(false); 20 | let userId = getUserSession().id!; 21 | const [buttons, setButtons] = useState([ 22 | { 23 | id: "reply", 24 | icon: , 25 | count: allDisabled ? 0 : props.replyCount, 26 | active: allDisabled ? false : interactionState(props, userId).replied, 27 | onClick: toggleModal, 28 | disabled: allDisabled || false, 29 | }, 30 | { 31 | id: "retweet", 32 | icon: , 33 | count: allDisabled ? 0 : props.retweetCount, 34 | className: "dark:hover:text-green-400", 35 | active: allDisabled ? false : interactionState(props, userId).retweeted, 36 | activeClassName: "text-green-400", 37 | onClick: reTweet, 38 | disabled: allDisabled || false, 39 | }, 40 | { 41 | id: "like", 42 | count: allDisabled ? 0 : props.likeCount, 43 | active: allDisabled ? false : interactionState(props, userId).liked, 44 | activeClassName: "text-red-600", 45 | className: "dark:hover:text-red-600", 46 | onClick: like, 47 | disabled: allDisabled || false, 48 | }, 49 | { id: "share", icon: , disabled: true }, 50 | ]); 51 | 52 | if (allDisabled) 53 | return ( 54 |
e.preventDefault()} 56 | className="my-1 flex w-full justify-around" 57 | > 58 | {buttons.map((p) => ( 59 | 60 | ))} 61 |
62 | ); 63 | 64 | function closeModal() { 65 | setIsOpen(false); 66 | } 67 | 68 | let { data } = useSession(); 69 | let likeTweet = trpc.tweet.likeTweet.useMutation(); 70 | let replyTweet = trpc.tweet.replyTweet.useMutation(); 71 | let reTweet0 = trpc.tweet.reTweet.useMutation(); 72 | function interact(id: ToInteract, inc: boolean) { 73 | let newBtns = buttons; 74 | newBtns.forEach((b) => { 75 | if (b.id === id) { 76 | (b.count || b.count === 0) && inc ? b.count++ : b.count!--; 77 | b.active = inc; 78 | } 79 | }); 80 | setButtons(newBtns); 81 | } 82 | function like() { 83 | let result = likeTweet.mutate({ id: props.id }); 84 | let toInteract: ToInteract = "like"; 85 | let inc = buttons.find((b) => b.id === "like"); 86 | interact(toInteract, !inc?.active); 87 | } 88 | function reply(body: string) { 89 | let toInteract: ToInteract = "reply"; 90 | interact(toInteract, true); 91 | toggleModal(); 92 | replyTweet.mutate({ id: props.id, body }); 93 | } 94 | function toggleModal() { 95 | setIsOpen(!isOpen); 96 | } 97 | function reTweet() { 98 | let result = reTweet0.mutate({ id: props.id }); 99 | 100 | let toInteract: ToInteract = "retweet"; 101 | let inc = buttons.find((b) => b.id === toInteract)?.active!; 102 | interact(toInteract, !inc); 103 | } 104 | 105 | function interactionState(props: TweetProps, userId: string) { 106 | const liked = props.likes.some((like) => like.userId === userId); 107 | const retweeted = props.retweets.some( 108 | (retweet) => retweet.userId === userId 109 | ); 110 | const replied = props.replies.some((reply) => reply.userId === userId); 111 | return { liked, retweeted, replied }; 112 | } 113 | useEffect(() => { 114 | // Update interactionState 115 | let state = interactionState(props, userId); 116 | setButtons((prevButtons) => 117 | prevButtons.map((button) => { 118 | if (button.id === "like") { 119 | return { 120 | ...button, 121 | count: props.likeCount, 122 | active: state.liked, 123 | }; 124 | } else if (button.id === "retweet") { 125 | return { 126 | ...button, 127 | count: props.retweetCount, 128 | active: state.retweeted, 129 | }; 130 | } else if (button.id === "reply") { 131 | return { 132 | ...button, 133 | count: props.replyCount, 134 | active: state.replied, 135 | }; 136 | } else { 137 | return button; 138 | } 139 | }) 140 | ); 141 | }, [props.likes, props.retweets, props.replies, userId]); 142 | if (buttons[2].active === null) return <>; 143 | return ( 144 |
e.preventDefault()}> 145 | 151 |
152 | {buttons.map((p) => ( 153 | 154 | ))} 155 |
156 |
157 | ); 158 | } 159 | function ActionButton({ 160 | icon, 161 | count, 162 | className, 163 | onClick, 164 | id, 165 | disabled, 166 | active, 167 | activeClassName, 168 | }: ActionButtonProps) { 169 | return ( 170 |
null : onClick} 172 | className={cn( 173 | "duration-350 flex grow items-center justify-center p-2 text-xs transition ease-in-out ", 174 | disabled 175 | ? "cursor-not-allowed" 176 | : "hover:text-blue-400 dark:hover:text-blue-400", 177 | className, 178 | active && activeClassName 179 | )} 180 | > 181 | {id === "like" ? ( 182 | active ? ( 183 | 184 | ) : ( 185 | 186 | ) 187 | ) : ( 188 | icon 189 | )} 190 | 191 | 192 |
193 | ); 194 | } 195 | type ToInteract = "like" | "retweet" | "reply" | "share"; 196 | type ActionButtonProps = { 197 | onClick?: () => any; 198 | id?: ToInteract; 199 | activeClassName?: string; 200 | count?: number; 201 | disabled?: boolean; 202 | active?: boolean; 203 | icon?: ReactNode; 204 | className?: string; 205 | }; 206 | -------------------------------------------------------------------------------- /src/components/MainTweet/TweetActionsReply.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import { trpc } from "@utils/trpc"; 3 | import { useSession } from "next-auth/react"; 4 | import ReplyModal from "@components/modals/ReplyModal"; 5 | import { TweetProps } from "@types"; 6 | import ReplyIcon from "@icons/tweet/ReplyIcon"; 7 | import RetweetIcon from "@icons/tweet/RetweetIcon"; 8 | import LikeIcon from "@icons/tweet/LikeIcon"; 9 | import ShareIcon from "@icons/tweet/ShareIcon"; 10 | 11 | export function TweetActions(props: TweetProps) { 12 | let [isOpen, setIsOpen] = useState(false); 13 | function closeModal() { 14 | setIsOpen(false); 15 | } 16 | 17 | let { data } = useSession(); 18 | let likeTweet = trpc.tweet.likeTweet.useMutation(); 19 | let replyTweet = trpc.tweet.replyTweet.useMutation(); 20 | let reTweet0 = trpc.tweet.reTweet.useMutation(); 21 | function like() { 22 | let result = likeTweet.mutate({ id: props.id }); 23 | } 24 | function reply() { 25 | setIsOpen(!isOpen); 26 | // let result = replyTweet.mutate({ id: props.id, body: "test" }); 27 | } 28 | function reTweet() { 29 | let result = reTweet0.mutate({ id: props.id }); 30 | } 31 | const [interactionState, setInteractionState] = useState({ 32 | liked: false, 33 | retweeted: false, 34 | replied: false, 35 | }); 36 | 37 | useEffect(() => { 38 | const isLiked = props.likes.some((l) => l.userId === data?.userData.id); 39 | const isRetweeted = props.retweets.some( 40 | (r) => r.userId === data?.userData.id 41 | ); 42 | const isReplied = props.replies.some((r) => r.userId === data?.userData.id); 43 | 44 | setInteractionState({ 45 | liked: isLiked, 46 | retweeted: isRetweeted, 47 | replied: isReplied, 48 | }); 49 | }, []); 50 | return ( 51 | <> 52 | 53 |
62 | 63 | {props.replyCount} 64 |
65 |
69 | 70 | {props.retweetCount} 71 |
72 |
76 | 77 | {props.likeCount} 78 |
79 |
80 | 81 |
82 | 83 | ); 84 | } 85 | -------------------------------------------------------------------------------- /src/components/MainTweet/TweetMetadata.tsx: -------------------------------------------------------------------------------- 1 | import { useFormattedDate } from "@hooks/useFormattedDate"; 2 | import React from "react"; 3 | import { TweetProps, UserData } from "@types"; 4 | import { 5 | ColorType, 6 | PickVerificationIcon, 7 | } from "@components/PickVerificationIcon"; 8 | 9 | export function TweetMetadata({ 10 | color, 11 | ...props 12 | }: { user: UserData } & ColorType & TweetProps) { 13 | const formattedDate = useFormattedDate(props.createdAt); 14 | 15 | return ( 16 | <> 17 |
18 |

19 | {props.user.name || props.user.username} 20 | 21 | 22 | @{props.user.username} · {formattedDate} 23 | 24 |

25 |
26 | 27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /src/components/MainTweet/index.ts: -------------------------------------------------------------------------------- 1 | import { MainTweet } from "./MainTweet"; 2 | export default MainTweet 3 | -------------------------------------------------------------------------------- /src/components/NewTweets.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export function NewTweets() { 4 | return ( 5 |
6 |
7 | Show 9 Tweets 8 |
9 |
10 | ); 11 | } 12 | 13 | -------------------------------------------------------------------------------- /src/components/NextLink.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | import React from "react"; 3 | 4 | export default function NextLink({ 5 | href, 6 | disabled, 7 | children, 8 | ...props 9 | }: { 10 | href?: string; 11 | disabled?: boolean; 12 | children: React.ReactNode; 13 | } & React.AnchorHTMLAttributes) { 14 | if (disabled) return {children}; 15 | 16 | return ( 17 | 18 | {children} 19 | 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /src/components/PageHead.tsx: -------------------------------------------------------------------------------- 1 | import ArrowIcon from "@icons/tweet/Arrow"; 2 | import { useRouter } from "next/router"; 3 | import React from "react"; 4 | 5 | export function PageHead({ 6 | name, 7 | username, 8 | backBtn, 9 | profile, 10 | }: { 11 | name: string; 12 | username?: string; 13 | backBtn?: boolean; 14 | profile?: boolean; 15 | }) { 16 | let router = useRouter(); 17 | return ( 18 |
19 |
20 | {backBtn && ( 21 |
router.back()} 23 | className="hover-main rounded-full p-2" 24 | > 25 | 26 |
27 | )} 28 |
29 |

30 | {name} 31 |

32 | {profile && ( 33 |

34 | @{username} 35 |

36 | )} 37 |
38 |
39 |
40 | ); 41 | } 42 | -------------------------------------------------------------------------------- /src/components/PickVerificationIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | export type ColorType = { color?: "red" | "blue" | "gold" | "gray"|string }; 3 | import { 4 | BlueVerified, 5 | GoldVerified, 6 | GrayVerified, 7 | RedVerified, 8 | } from "@icons/verified"; 9 | 10 | export function PickVerificationIcon({ color }: ColorType) { 11 | if (color === "gold") return ; 12 | if (color === "gray") return ; 13 | if (color === "red") return ; 14 | if (color === "blue") return ; 15 | return <>; 16 | } 17 | -------------------------------------------------------------------------------- /src/components/SEO.tsx: -------------------------------------------------------------------------------- 1 | 2 | import Head from 'next/head'; 3 | 4 | type MainLayoutProps = { 5 | title: string; 6 | description?: string; 7 | }; 8 | 9 | export function SEO({ 10 | title, 11 | description 12 | }: MainLayoutProps): JSX.Element { 13 | 14 | return ( 15 | 16 | {title} 17 | 18 | {description && } 19 | {description && } 20 | 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /src/components/SidebarLeft/Logo.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import NextLink from "@components/NextLink"; 3 | 4 | export function Logo() { 5 | return ( 6 | 7 |
8 | 13 | 14 | 15 | 16 | 17 |
18 |
19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /src/components/SidebarLeft/NavItem.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import NextLink from "@components/NextLink"; 3 | 4 | export type NavItemProps = { 5 | href?: string; 6 | text: string; 7 | diabled?: boolean; 8 | icon?: React.ReactNode; 9 | active?: number; 10 | index?: number; 11 | }; 12 | 13 | export function NavItem({ 14 | href, 15 | text, 16 | icon, 17 | active, 18 | index, 19 | diabled, 20 | }: NavItemProps) { 21 | if (diabled) 22 | return ( 23 |
28 | {icon} 29 | {text} 30 |
31 | ); 32 | 33 | return ( 34 | 40 | {icon} 41 | {text} 42 | 43 | ); 44 | } 45 | -------------------------------------------------------------------------------- /src/components/SidebarLeft/SidebarLeft.tsx: -------------------------------------------------------------------------------- 1 | import { getUserSession } from "@hooks/getUserSession"; 2 | import React, { useEffect, useState } from "react"; 3 | import Avatar from "@components/Avatar"; 4 | import { NavItem } from "./NavItem"; 5 | import { navItems } from "./navItems"; 6 | import { Logo } from "./Logo"; 7 | import TweetModal from "@components/modals/TweetModal"; 8 | import VerifiedModal from "@components/modals/VerifiedModal"; 9 | import { ArrowRightOnRectangleIcon } from "@heroicons/react/20/solid"; 10 | import { signOut } from "next-auth/react"; 11 | import NextLink from "@components/NextLink"; 12 | import { BlueVerified } from "@icons/verified"; 13 | import { PickVerificationIcon } from "@components/PickVerificationIcon"; 14 | import { trpc } from "@utils/trpc"; 15 | 16 | export default function SidebarLeft({ active }: { active?: number }) { 17 | const [isOpen, setIsOpen] = useState(false); 18 | function toggleModal() { 19 | setIsOpen(!isOpen); 20 | } 21 | 22 | const [isOpen2, setIsOpen2] = useState(false); 23 | function toggleModal2() { 24 | setIsOpen2(!isOpen2); 25 | } 26 | let session=getUserSession() 27 | return ( 28 | <> 29 | 30 | 31 |
32 |
33 | 34 | 56 | 57 |
58 |
59 | 60 | ); 61 | } 62 | 63 | function User() { 64 | let session = getUserSession(); 65 | const [user, setUser] = useState(session); 66 | function logout() { 67 | signOut(); 68 | } 69 | if (!user) return <>; 70 | return ( 71 |
75 | 76 |
77 | 78 |
79 |

80 | 81 | {" "} 82 | {user.name||user.username} 83 | 84 | 85 |

86 |

@{user.username}

87 |
88 |
89 |
90 |
91 |
95 | 96 |
97 |
98 |
99 | ); 100 | } 101 | 102 | function ProfileIcon() { 103 | return ( 104 | 105 | 111 | 112 | ) 113 | } 114 | -------------------------------------------------------------------------------- /src/components/SidebarLeft/index.ts: -------------------------------------------------------------------------------- 1 | import SidebarLeft from "./SidebarLeft"; 2 | export default SidebarLeft; 3 | -------------------------------------------------------------------------------- /src/components/SidebarLeft/navItems.tsx: -------------------------------------------------------------------------------- 1 | import { NavItemProps } from "./NavItem"; 2 | 3 | export let navItems: NavItemProps[] = [ 4 | { 5 | href: "/", 6 | text: "Home", 7 | icon: ( 8 | 9 | 15 | 16 | ), 17 | }, 18 | { 19 | href: "/explore", 20 | text: "Explore", 21 | diabled:true, 22 | icon: ( 23 | 24 | 30 | 31 | ), 32 | }, 33 | { 34 | diabled:true, 35 | href: "/notifications", 36 | text: "Notifications", 37 | icon: ( 38 | 39 | 45 | 46 | ), 47 | }, 48 | { 49 | href: "/messages", 50 | text: "Messages", 51 | diabled:true, 52 | icon: ( 53 | 54 | 60 | 61 | ), 62 | }, 63 | { 64 | href: "/bookmarks", 65 | text: "Bookmarks", 66 | diabled:true, 67 | icon: ( 68 | 69 | 75 | 76 | ), 77 | }, 78 | { 79 | href: "/settings", 80 | text: "Settings", 81 | diabled:true, 82 | icon: ( 83 | 91 | 96 | 101 | 102 | ), 103 | }, 104 | , 105 | ]; 106 | -------------------------------------------------------------------------------- /src/components/SidebarRight/SidebarRight.tsx: -------------------------------------------------------------------------------- 1 | import Avatar from "@components/Avatar"; 2 | import MainButton from "@components/MainButton"; 3 | import NextLink from "@components/NextLink"; 4 | import { PickVerificationIcon } from "@components/PickVerificationIcon"; 5 | import GithubIcon from "@icons/social/github"; 6 | import { trpc } from "@utils/trpc"; 7 | import React, { useEffect, useState } from "react"; 8 | 9 | export default function SidebarRight() { 10 | return ( 11 |
12 |
13 | 14 | 15 |
16 | Made by{" "} 17 | 23 | AlandSleman 24 | 25 |
26 |
27 |
28 | ); 29 | } 30 | function SearchBar() { 31 | return ( 32 |
33 |
34 | 46 | 47 | 48 |
49 | 54 |
55 | ); 56 | } 57 | 58 | const TrendingTopic = ({ 59 | hashtag, 60 | tweets, 61 | }: { 62 | hashtag: string; 63 | tweets: string; 64 | }) => { 65 | return ( 66 |
69 |

70 | {hashtag} 71 |

72 |

{tweets} Tweets

73 |
74 | ); 75 | }; 76 | 77 | const TwitterAccount = ({ 78 | name, 79 | username, 80 | profileImage, 81 | badge, 82 | }: { 83 | name: string; 84 | username: string; 85 | profileImage: string; 86 | badge: string; 87 | }) => { 88 | return ( 89 | 90 |
93 |
94 |
95 | 96 |
97 |

98 | {name || username} 99 |

100 |

@{username}

101 |
102 | 103 |
104 | 105 |
106 |
107 |
108 | ); 109 | }; 110 | 111 | const Loader = () => { 112 | return ( 113 |
116 |
117 |
118 |
119 |
120 |
121 | ); 122 | }; 123 | 124 | const TrendsForYou = () => { 125 | return ( 126 |
127 |

128 | Trends for you 129 |

130 | 131 | 132 | 133 |
134 | ); 135 | }; 136 | 137 | const WhoToFollow = () => { 138 | let getTopUsers = trpc.user.topUsers.useQuery(); 139 | const [topUsers, setTopUsers] = useState(getTopUsers.data?.users); 140 | useEffect(() => { 141 | setTopUsers(getTopUsers.data?.users); 142 | }, [getTopUsers.data]); 143 | return ( 144 |
145 |

146 | Who to follow 147 |

148 | {topUsers?.map((u, i) => ( 149 | 150 | ))} 151 |
152 | ); 153 | }; 154 | -------------------------------------------------------------------------------- /src/components/SidebarRight/index.ts: -------------------------------------------------------------------------------- 1 | import SidebarRight from "./SidebarRight"; 2 | export default SidebarRight 3 | -------------------------------------------------------------------------------- /src/components/Spinner.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export function Spinner() { 4 | return ( 5 |
6 | 7 | 14 | 25 | 26 |
27 | ); 28 | } 29 | 30 | -------------------------------------------------------------------------------- /src/components/TweetBody.tsx: -------------------------------------------------------------------------------- 1 | export default function TweetBody({ body }: { body: string }) { 2 | 3 | const formatText = (text:string) => { 4 | let formattedText = text; 5 | formattedText = formattedText.replace(/(\w)(@|#)/g, '$1 $2'); // Add space before @ and # 6 | formattedText = formattedText.replace(/(@|#)\s+(\w+)/g, '$1$2'); // Remove space after @ and # 7 | const words = formattedText.split(/(?<=\s)(?=\S)|(?<=\S)(?=\s)/); // Split on spaces that are followed by a non-newline character or on non-newline characters that are preceded by a space 8 | const formattedWords = words.map((word, index) => { 9 | if (word.includes('@') || word.includes('#')) { 10 | // Check if the previous character was a new line or the beginning of the string 11 | const isStartOfLine = index === 0 || /\n/.test(words[index-1]); 12 | const blueClass = isStartOfLine ? "text-blue-500" : ""; 13 | return {word} ; 14 | } else { 15 | return {word} ; 16 | } 17 | }); 18 | return formattedWords; 19 | }; 20 | 21 | return ( 22 |

{formatText(body)}

23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /src/components/TweetDetails/Avatar.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export function Avatar({size=56,avatarImage}:{size?:number,avatarImage:string}) { 4 | return ( 5 |
6 | avatar 10 |
11 | ); 12 | } 13 | 14 | -------------------------------------------------------------------------------- /src/components/TweetDetails/Body.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { TweetProps } from "./Tweet"; 3 | 4 | export function Body(props: TweetProps) { 5 | return ( 6 | <> 7 |

{props.body}

8 | {props.images[0] && ( 9 |
10 | 11 |
12 | )} 13 | 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /src/components/TweetDetails/TweetActions.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import { trpc } from "@utils/trpc"; 3 | import { useSession } from "next-auth/react"; 4 | import { TweetProps } from "./Tweet"; 5 | import ReplyModal from "@components/modals/ReplyModal"; 6 | 7 | export function TweetActions(props: TweetProps) { 8 | let [isOpen, setIsOpen] = useState(false); 9 | function closeModal() { 10 | setIsOpen(false); 11 | } 12 | 13 | let { data } = useSession(); 14 | let likeTweet = trpc.tweet.likeTweet.useMutation(); 15 | let replyTweet = trpc.tweet.replyTweet.useMutation(); 16 | let reTweet0 = trpc.tweet.reTweet.useMutation(); 17 | function like() { 18 | let result = likeTweet.mutate({ id: props.id }); 19 | console.log("like", result); 20 | } 21 | function reply() { 22 | setIsOpen(!isOpen); 23 | // let result = replyTweet.mutate({ id: props.id, body: "test" }); 24 | // console.log("reply", result); 25 | } 26 | function reTweet() { 27 | let result = reTweet0.mutate({ id: props.id }); 28 | console.log("reTweet", result); 29 | } 30 | const [interactionState, setInteractionState] = useState({ 31 | liked: false, 32 | retweeted: false, 33 | replied: false, 34 | }); 35 | 36 | useEffect(() => { 37 | const isLiked = props.likes.some((l) => l.userId === data?.userData.id); 38 | const isRetweeted = props.retweets.some( 39 | (r) => r.userId === data?.userData.id 40 | ); 41 | const isReplied = props.replies.some((r) => r.userId === data?.userData.id); 42 | console.log("isr", isReplied); 43 | 44 | setInteractionState({ 45 | liked: isLiked, 46 | retweeted: isRetweeted, 47 | replied: isReplied, 48 | }); 49 | }, []); 50 | return ( 51 |
52 | 53 |
61 | 62 | 63 | 64 | 65 | 66 | {props.replyCount} 67 |
68 |
72 | 73 | 74 | 75 | 76 | 77 | {props.retweetCount} 78 |
79 |
83 | 84 | 85 | 86 | 87 | 88 | {props.likeCount} 89 |
90 |
91 | 92 | 93 | 94 | 95 | 96 | 97 |
98 |
99 | ); 100 | } 101 | -------------------------------------------------------------------------------- /src/components/TweetDetails/TweetDetails.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { useForm, SubmitHandler } from "react-hook-form"; 3 | import type { Tweet, User, Like, Retweet, Reply } from "@prisma/client"; 4 | import type { Variants } from "framer-motion"; 5 | import { Avatar } from "./Avatar"; 6 | import ReactTextareaAutosize from "react-textarea-autosize"; 7 | import { TweetReply } from "@components/TweetReply"; 8 | import MainButton from "@components/MainButton"; 9 | import { Body } from "@components/MainTweet/Body"; 10 | import { TweetDetailsMetaData } from "./TweetDetailsMetaData"; 11 | import { TweetMetrics } from "./TweetMetrics"; 12 | import { TweetActions } from "@components/MainTweet/TweetActions"; 13 | import TweetDetailsReply from "./TweetDetailsReply"; 14 | import { trpc } from "@utils/trpc"; 15 | import { getUserSession } from "@hooks/getUserSession"; 16 | 17 | type Inputs = { 18 | body: string; 19 | }; 20 | export const variants: Variants = { 21 | initial: { opacity: 0 }, 22 | animate: { opacity: 1, transition: { duration: 0.8 } }, 23 | exit: { opacity: 0, transition: { duration: 0.2 } }, 24 | }; 25 | export type TweetProps = Tweet & { 26 | user: User; 27 | likes: Like[]; 28 | retweets: Retweet[]; 29 | replies: Reply[]; 30 | }; 31 | export function TweetDetails({ 32 | reply, 33 | tweet, 34 | }: { 35 | reply?: boolean; 36 | tweet: TweetProps; 37 | }) { 38 | const { register, handleSubmit, watch, reset } = useForm(); 39 | let replyTweet = trpc.tweet.replyTweet.useMutation(); 40 | const [tweetReplies, setTweetReplies] = useState(tweet.replies); 41 | let session = getUserSession(); 42 | 43 | const onSubmit: SubmitHandler = async (data) => { 44 | let res = await replyTweet.mutateAsync({ id: tweet.id, body: data.body }); 45 | setTweetReplies([res.reply, ...tweetReplies]); 46 | reset(); 47 | }; 48 | return ( 49 |
50 | 51 |
52 | 53 |
54 | 55 |
56 | 57 |
58 | 59 |
60 | 61 |
62 |
63 | 64 |
65 |
69 | 77 | 78 | 79 | 80 |
81 |
82 | {tweetReplies.map((t) => ( 83 | 84 | ))} 85 |
86 |
87 |
88 | ); 89 | } 90 | -------------------------------------------------------------------------------- /src/components/TweetDetails/TweetDetailsMetaData.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { TweetMetadata } from "./TweetMetadata"; 3 | import { Avatar } from "./Avatar"; 4 | import NextLink from "@components/NextLink"; 5 | import { TweetProps } from "./TweetDetails"; 6 | 7 | 8 | export function TweetDetailsMetaData({ 9 | tweet, reply, 10 | }: { 11 | tweet: TweetProps; 12 | reply?: boolean; 13 | }) { 14 | return ( 15 |
16 |
17 | 18 | {reply && ( 19 |
20 | )} 21 |
22 |
23 | 24 | 25 | 26 |
27 |
28 | ); 29 | } 30 | 31 | -------------------------------------------------------------------------------- /src/components/TweetDetails/TweetDetailsReply/Counter.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from "react"; 2 | import { useState } from "react"; 3 | 4 | type PropsType = { 5 | num: number; 6 | }; 7 | 8 | export const Counter: React.FC = ({ num }) => { 9 | const [curValue, setCurValue] = React.useState(num); 10 | const [inputStyle, setInputStyle] = React.useState({}); 11 | const [initial, setInitial] = useState(false); 12 | 13 | useEffect(() => { 14 | initial && handleValueChange(num); 15 | setInitial(true); 16 | }, [num]); 17 | 18 | const handleValueChange = (newValue: number) => { 19 | setInputStyle({ 20 | transform: newValue > curValue ? "translateY(-100%)" : "translateY(100%)", 21 | opacity: 0, 22 | }); 23 | 24 | setTimeout(() => { 25 | setInputStyle({ 26 | transitionDuration: "0s", 27 | transform: 28 | newValue > curValue ? "translateY(100%) " : "translateY(-100%)", 29 | opacity: 0, 30 | }); 31 | 32 | setCurValue(num); 33 | 34 | setTimeout(() => { 35 | setInputStyle({ 36 | transitionDuration: "0.3s", 37 | transform: "translateY(0)", 38 | opacity: 1, 39 | }); 40 | }, 20); 41 | }, 250); 42 | }; 43 | 44 | return ( 45 | { 51 | e.preventDefault(); 52 | // handleValueChange(parseInt(e.target.value, 10), true); 53 | }, 54 | type: "text", 55 | value: curValue, 56 | }} 57 | > 58 | {curValue} 59 | 60 | ); 61 | }; 62 | -------------------------------------------------------------------------------- /src/components/TweetDetails/TweetDetailsReply/TweetDetailsReply.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import type { Variants } from "framer-motion"; 4 | import { TweetMetadata } from "./TweetMetadata"; 5 | import Link from "next/link"; 6 | import NextLink from "@components/NextLink"; 7 | import Avatar from "@components/Avatar"; 8 | import { TweetProps } from "@types"; 9 | import TweetOptions from "@components/TweetOptions"; 10 | import { Body } from "@components/MainTweet/Body"; 11 | import { TweetActions } from "@components/MainTweet/TweetActions"; 12 | 13 | export const variants: Variants = { 14 | initial: { opacity: 0 }, 15 | animate: { opacity: 1, transition: { duration: 0.8 } }, 16 | exit: { opacity: 0, transition: { duration: 0.2 } }, 17 | }; 18 | export function TweetDetailsReply({ 19 | reply, 20 | tweet, 21 | }: { 22 | reply?: boolean; 23 | tweet: TweetProps; 24 | }) { 25 | return ( 26 | 27 |
28 |
29 |
30 | 31 | {reply && ( 32 |
33 | )} 34 |
35 |
36 | 37 | 38 | 39 | 40 | 41 |
42 | 43 |
44 |
45 |
46 | ); 47 | } 48 | -------------------------------------------------------------------------------- /src/components/TweetDetails/TweetDetailsReply/TweetMetadata.tsx: -------------------------------------------------------------------------------- 1 | import { useFormattedDate } from "@hooks/useFormattedDate"; 2 | import React from "react"; 3 | import { VerifiedIcon } from "@icons/tweet/VerifiedIcon"; 4 | import { TweetProps } from "@types"; 5 | import NextLink from "@components/NextLink"; 6 | import { ColorType, PickVerificationIcon } from "@components/PickVerificationIcon"; 7 | 8 | export function TweetMetadata({color,...props}: TweetProps&ColorType) { 9 | const formattedDate = useFormattedDate(props.createdAt); 10 | 11 | return ( 12 | <> 13 |
14 |
15 |

16 | {props.user.name || props.user.username} 17 | 18 | 19 | @{props.user.username} · {formattedDate} 20 | 21 |

22 | 23 | Replying to @{props.user.username} 24 | 25 |
26 |
27 | 28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /src/components/TweetDetails/TweetDetailsReply/index.ts: -------------------------------------------------------------------------------- 1 | import { TweetDetailsReply } from "./TweetDetailsReply"; 2 | export default TweetDetailsReply 3 | -------------------------------------------------------------------------------- /src/components/TweetDetails/TweetMetadata.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { UserData } from "@types"; 3 | import { 4 | ColorType, 5 | PickVerificationIcon, 6 | } from "@components/PickVerificationIcon"; 7 | 8 | export function TweetMetadata({ 9 | color, 10 | ...props 11 | }: { user: UserData } & ColorType) { 12 | return ( 13 | <> 14 |
15 |
16 |

17 | {props.user.name || props.user.username} 18 |

19 | 20 |
21 | 22 | @{props.user.username} 23 | 24 |
25 | 26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /src/components/TweetDetails/TweetMetrics.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import moment from "moment"; 3 | import { TweetProps } from "@types"; 4 | 5 | export function TweetMetrics({tweet}:{tweet:TweetProps}) { 6 | return ( 7 |
8 |
9 |

{moment(tweet.createdAt).format("h:mm A · MMM D, YYYY")}

{" "} 10 |
11 |
12 |
13 | {tweet.retweetCount} 14 | Retweets 15 |
16 |
17 | 18 | {tweet.likeCount} 19 | 20 | Likes 21 |
22 |
23 |
24 | ); 25 | } 26 | 27 | -------------------------------------------------------------------------------- /src/components/TweetDetails/index.ts: -------------------------------------------------------------------------------- 1 | import { TweetDetails } from "./TweetDetails"; 2 | export { TweetDetails }; 3 | -------------------------------------------------------------------------------- /src/components/TweetOptions/TweetOptions.tsx: -------------------------------------------------------------------------------- 1 | import NextLink from "@components/NextLink"; 2 | import { Menu, Transition } from "@headlessui/react"; 3 | import { Fragment } from "react"; 4 | 5 | let bookmarked = false; 6 | export default function TweetOptions({ id }: { id: string }) { 7 | function deleteTweet() { 8 | id; 9 | } 10 | function bookmarkTweet() { 11 | id; 12 | } 13 | return ( 14 | 15 |
16 | 17 | 21 | 29 | 34 | 35 | 36 | 45 | 46 |
47 | 48 | {({ active }) => ( 49 | 68 | )} 69 | 70 | 71 | {({ active }) => ( 72 | 84 | )} 85 | 86 |
87 |
88 |
89 |
90 |
91 |
92 | ); 93 | } 94 | 95 | function BookmarkActiveIcon(props: any) { 96 | return ( 97 | 104 | 109 | 110 | ); 111 | } 112 | 113 | function BookmarkIcon(props: any) { 114 | return ( 115 | 123 | 128 | 129 | ); 130 | } 131 | 132 | function DeleteIcon(props: any) { 133 | return ( 134 | 142 | 147 | 148 | ); 149 | } 150 | -------------------------------------------------------------------------------- /src/components/TweetOptions/index.ts: -------------------------------------------------------------------------------- 1 | import TweetOptions from "./TweetOptions"; 2 | 3 | export default TweetOptions 4 | -------------------------------------------------------------------------------- /src/components/TweetReply/Avatar.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export function Avatar({size=56,avatarImage}:{size?:number,avatarImage:string}) { 4 | return ( 5 |
6 | avatar 10 |
11 | ); 12 | } 13 | 14 | -------------------------------------------------------------------------------- /src/components/TweetReply/Body.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { TweetProps } from "./Tweet"; 3 | 4 | export function Body(props: TweetProps) { 5 | return ( 6 | <> 7 |

8 | {props.body} 9 |

10 | {props.images[0] && ( 11 |
12 | 13 |
14 | )} 15 | 16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /src/components/TweetReply/TweetActions.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import { trpc } from "@utils/trpc"; 3 | import { useSession } from "next-auth/react"; 4 | import { TweetProps } from "./Tweet"; 5 | import ReplyModal from "@components/modals/ReplyModal"; 6 | 7 | export function TweetActions(props: TweetProps) { 8 | let [isOpen, setIsOpen] = useState(false); 9 | function closeModal() { 10 | setIsOpen(false); 11 | } 12 | 13 | let { data } = useSession(); 14 | let likeTweet = trpc.tweet.likeTweet.useMutation(); 15 | let replyTweet = trpc.tweet.replyTweet.useMutation(); 16 | let reTweet0 = trpc.tweet.reTweet.useMutation(); 17 | function like() { 18 | let result = likeTweet.mutate({ id: props.id }); 19 | console.log("like", result); 20 | } 21 | function reply() { 22 | setIsOpen(!isOpen); 23 | // let result = replyTweet.mutate({ id: props.id, body: "test" }); 24 | // console.log("reply", result); 25 | } 26 | function reTweet() { 27 | let result = reTweet0.mutate({ id: props.id }); 28 | console.log("reTweet", result); 29 | } 30 | const [interactionState, setInteractionState] = useState({ 31 | liked: false, 32 | retweeted: false, 33 | replied: false, 34 | }); 35 | 36 | useEffect(() => { 37 | const isLiked = props.likes.some((l) => l.userId === data?.userData.id); 38 | const isRetweeted = props.retweets.some( 39 | (r) => r.userId === data?.userData.id 40 | ); 41 | const isReplied = props.replies.some((r) => r.userId === data?.userData.id); 42 | console.log("isr", isReplied); 43 | 44 | setInteractionState({ 45 | liked: isLiked, 46 | retweeted: isRetweeted, 47 | replied: isReplied, 48 | }); 49 | }, []); 50 | return ( 51 |
52 | 53 |
61 | 62 | 63 | 64 | 65 | 66 | {props.replyCount} 67 |
68 |
72 | 73 | 74 | 75 | 76 | 77 | {props.retweetCount} 78 |
79 |
83 | 84 | 85 | 86 | 87 | 88 | {props.likeCount} 89 |
90 |
91 | 92 | 93 | 94 | 95 | 96 | 97 |
98 |
99 | ); 100 | } 101 | -------------------------------------------------------------------------------- /src/components/TweetReply/TweetMetadata.tsx: -------------------------------------------------------------------------------- 1 | import { useFormattedDate } from "@hooks/useFormattedDate"; 2 | import React from "react"; 3 | import { VerifiedIcon } from "@icons/tweet/VerifiedIcon"; 4 | import { TweetProps } from "@types"; 5 | 6 | export function TweetMetadata(props: TweetProps) { 7 | const formattedDate = useFormattedDate(props.createdAt); 8 | 9 | return ( 10 | <> 11 |
12 |

13 | {props.user.name || props.user.username} 14 | 15 | 16 | @{props.user.username} · {formattedDate} 17 | 18 |

19 |
20 | 21 | ); 22 | } 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/components/TweetReply/TweetReply.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import cn from "clsx"; 3 | import type { Tweet, User, Like, Retweet, Reply } from "@prisma/client"; 4 | 5 | import { AnimatePresence, motion } from "framer-motion"; 6 | 7 | import type { Variants } from "framer-motion"; 8 | import { TweetMetadata } from "./TweetMetadata"; 9 | import { Body } from "./Body"; 10 | import { TweetActions } from "./TweetActions"; 11 | import Link from "next/link"; 12 | import { Avatar } from "./Avatar"; 13 | import { useSession } from "next-auth/react"; 14 | import NextLink from "@components/NextLink"; 15 | 16 | export const variants: Variants = { 17 | initial: { opacity: 0 }, 18 | animate: { opacity: 1, transition: { duration: 0.8 } }, 19 | exit: { opacity: 0, transition: { duration: 0.2 } }, 20 | }; 21 | export type TweetProps = Tweet & { 22 | user: User; 23 | likes: Like[]; 24 | retweets: Retweet[]; 25 | replies: Reply[]; 26 | }; 27 | export function TweetReply({ 28 | reply, 29 | tweet, 30 | }: { 31 | reply?: boolean; 32 | tweet: TweetProps; 33 | }) { 34 | return ( 35 |
36 |
37 | 38 | {reply && ( 39 |
40 | )} 41 |
42 |
43 | 44 | 45 | 46 | 47 | 48 | 49 |
50 | {reply ? ( 51 |

52 | Replying to 53 | 57 | @{tweet.user.username} 58 | 59 |

60 | ) : ( 61 |
62 | 63 |
64 | )} 65 |
66 |
67 |
68 | ); 69 | } 70 | -------------------------------------------------------------------------------- /src/components/TweetReply/index.ts: -------------------------------------------------------------------------------- 1 | import { TweetReply } from "./TweetReply"; 2 | export { TweetReply }; 3 | -------------------------------------------------------------------------------- /src/components/UserMetadata/UserMetadata.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import NextLink from "@components/NextLink"; 3 | import Avatar from "@components/Avatar"; 4 | import { PickVerificationIcon } from "@components/PickVerificationIcon"; 5 | 6 | export function UserMetadata({ 7 | user, 8 | }: { 9 | user: { 10 | bio: string; 11 | username: string; 12 | name: string; 13 | profileImage: string; 14 | badge: any; 15 | }; 16 | }) { 17 | return ( 18 |
19 |
20 | 21 |
22 |
23 | 24 |
25 |

26 | {user.name || user.username} 27 | 28 |

29 | 30 | @{user.username} 31 | 32 |
33 |
34 |
35 |
36 | ); 37 | } 38 | -------------------------------------------------------------------------------- /src/components/inputs/ReplyInput.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactTextareaAutosize from "react-textarea-autosize"; 3 | import { useForm, SubmitHandler } from "react-hook-form"; 4 | import { trpc } from "@utils/trpc"; 5 | import Avatar from "@components/Avatar"; 6 | import { getUserSession } from "@hooks/getUserSession"; 7 | 8 | type Inputs = { 9 | body: string; 10 | }; 11 | type InputProps = { 12 | onReply: any; 13 | hideAvatar?: boolean; 14 | minH?: number; 15 | }; 16 | let avatarSize = 56; 17 | export function ReplyInput({ onReply, hideAvatar, minH = 80 }: InputProps) { 18 | const { 19 | register, 20 | handleSubmit, 21 | watch, 22 | reset, 23 | formState: { errors }, 24 | } = useForm(); 25 | const onSubmit: SubmitHandler = (data) => { 26 | onReply(data.body); 27 | reset(); 28 | }; 29 | let session = getUserSession() 30 | return ( 31 |
32 |
33 | {hideAvatar ? ( 34 | <> 35 | ) : ( 36 |
37 | {" "} 38 | 39 |
40 | )} 41 |
42 | 48 |
49 |
50 |
51 | 68 | 74 |
75 |
76 | ); 77 | } 78 | 79 | function OtherIcons() { 80 | return ( 81 | <> 82 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | ); 131 | } 132 | -------------------------------------------------------------------------------- /src/components/inputs/TweetInput/FilePreview.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export function FilePreview({ 4 | selectedFile, clearInputs, 5 | }: { 6 | selectedFile: string; 7 | clearInputs: any; 8 | }) { 9 | return ( 10 | <> 11 | {selectedFile && ( 12 |
13 |
14 | 23 | 27 | 28 |
29 | Selected file preview 33 |
34 | )} 35 | 36 | ); 37 | } 38 | 39 | -------------------------------------------------------------------------------- /src/components/inputs/TweetInput/OtherIcons.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export function OtherIcons() { 4 | return ( 5 | <> 6 | 7 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | ); 56 | } 57 | 58 | -------------------------------------------------------------------------------- /src/components/inputs/TweetInput/TweetInput.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef, useState } from "react"; 2 | import ReactTextareaAutosize from "react-textarea-autosize"; 3 | import { useForm, SubmitHandler } from "react-hook-form"; 4 | import Avatar from "@components/Avatar"; 5 | import { useSession } from "next-auth/react"; 6 | import { trpc } from "@utils/trpc"; 7 | import { OtherIcons } from "./OtherIcons"; 8 | import { FilePreview } from "./FilePreview"; 9 | import { compressFile } from "@utils/comporessImage"; 10 | import { getUserSession } from "@hooks/getUserSession"; 11 | 12 | type Inputs = { 13 | body: string; 14 | }; 15 | export function TweetInput({ onPost }: { onPost?: any }) { 16 | const [isPosting, setIsPosting] = useState(false); 17 | let session = getUserSession(); 18 | const [user, setUser] = useState(session); 19 | const { 20 | register, 21 | handleSubmit, 22 | watch, 23 | reset, 24 | formState: { errors }, 25 | } = useForm(); 26 | let { data } = useSession(); 27 | let newTweet = trpc.tweet.newTweet.useMutation(); 28 | 29 | const [selectedFile, setSelectedFile] = useState(); 30 | const onSubmit: SubmitHandler = (data) => { 31 | setIsPosting(true); 32 | newTweet.mutate({ 33 | body: data.body, 34 | image: selectedFile || null, 35 | }); 36 | // if (!newTweet.isError) onPost(newTweet.data?.tweet); 37 | reset(); 38 | clearInputs(); 39 | }; 40 | 41 | const fileInputRef = useRef(null); 42 | 43 | function handleImageUploadClick() { 44 | if (fileInputRef.current) { 45 | fileInputRef.current.click(); 46 | } 47 | } 48 | async function handleFileSelection( 49 | event: React.ChangeEvent 50 | ) { 51 | if (event.target.files && event.target.files.length > 0) { 52 | const selectedFile = event.target.files[0]; 53 | //compress 54 | let compressedFile = await compressFile(selectedFile, 0.7); 55 | // Read the contents of the selected file 56 | console.log("imageee", compressedFile); 57 | const reader = new FileReader(); 58 | reader.readAsDataURL(compressedFile); 59 | 60 | // When the file contents are loaded, set the selected file state to the Data URI 61 | reader.onload = () => { 62 | if (typeof reader.result === "string") { 63 | setSelectedFile(reader.result); 64 | } 65 | }; 66 | } 67 | } 68 | function clearInputs() { 69 | setSelectedFile(null); 70 | if (fileInputRef.current) { 71 | fileInputRef.current.value = ""; 72 | } 73 | } 74 | 75 | useEffect(() => { 76 | if ( 77 | isPosting && 78 | !newTweet.isLoading && 79 | !newTweet.isError && 80 | newTweet.data?.tweet 81 | ) { 82 | onPost(newTweet.data.tweet); 83 | setIsPosting(false); 84 | } 85 | }, [isPosting, newTweet, onPost]); 86 | if (!user) return <>; 87 | return ( 88 |
92 |
93 |
94 | 95 |
96 |
97 | 103 | 104 |
105 |
106 |
107 |
111 | 119 | 120 | 121 | 122 | 123 | 124 | 125 |
126 | 127 | 133 |
134 |
135 | ); 136 | } 137 | -------------------------------------------------------------------------------- /src/components/inputs/TweetInput/index.tsx: -------------------------------------------------------------------------------- 1 | import {TweetInput} from "./TweetInput" 2 | export {TweetInput} 3 | -------------------------------------------------------------------------------- /src/components/modals/ReplyModal.tsx: -------------------------------------------------------------------------------- 1 | import { ReplyInput } from "@components/inputs/ReplyInput"; 2 | import { TweetInput } from "@components/inputs/TweetInput"; 3 | import Avatar from "@components/Avatar"; 4 | import { Dialog, Transition } from "@headlessui/react"; 5 | import { signIn } from "next-auth/react"; 6 | import { Fragment } from "react"; 7 | 8 | import { useForm, SubmitHandler } from "react-hook-form"; 9 | import { TweetProps } from "@types"; 10 | import MainTweet from "@components/MainTweet"; 11 | import { TweetReply } from "@components/TweetReply"; 12 | 13 | type Inputs = { 14 | username: string; 15 | password: string; 16 | }; 17 | export default function ReplyModal({ 18 | isOpen, 19 | closeModal, 20 | onReply, 21 | tweet, 22 | }: { 23 | isOpen: boolean; 24 | closeModal: any; 25 | onReply: any; 26 | tweet: TweetProps; 27 | }) { 28 | return ( 29 | <> 30 | 31 | 32 | 41 |
42 | 43 | 44 |
45 |
46 | 55 | 56 |
57 | 58 |
59 |
60 | 61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 | 71 | ); 72 | } 73 | -------------------------------------------------------------------------------- /src/components/modals/TweetModal.tsx: -------------------------------------------------------------------------------- 1 | import { Dialog, Transition } from "@headlessui/react"; 2 | import { signIn } from "next-auth/react"; 3 | import { Fragment } from "react"; 4 | import DiscordIcon from "@icons/social/discord"; 5 | import GithubIcon from "@icons/social/github"; 6 | import GoogleIcon from "@icons/social/google"; 7 | import TwitterIcon from "@icons/social/twitter"; 8 | 9 | import { useForm, SubmitHandler } from "react-hook-form"; 10 | import { TweetInput } from "@components/inputs/TweetInput"; 11 | 12 | type Inputs = { 13 | username: string; 14 | password: string; 15 | }; 16 | export default function TweetModal({ 17 | isOpen, 18 | closeModal, 19 | }: { 20 | isOpen: boolean; 21 | closeModal: any; 22 | }) { 23 | const { 24 | register, 25 | handleSubmit, 26 | watch, 27 | reset, 28 | formState: { errors }, 29 | } = useForm(); 30 | 31 | const onSubmit: SubmitHandler = (data) => { 32 | console.log("cred data", data); 33 | signIn("credentials", { ...data }); 34 | reset(); 35 | }; 36 | return ( 37 | <> 38 | 39 | 40 | 49 |
50 | 51 | 52 |
53 |
54 | 63 | 64 | closeModal()} /> 65 | 66 | 67 |
68 |
69 |
70 |
71 | 72 | ); 73 | } 74 | -------------------------------------------------------------------------------- /src/components/modals/VerifiedDropdown.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import * as Menubar from "@radix-ui/react-menubar"; 3 | import { 4 | CheckIcon, 5 | ChevronRightIcon, 6 | DotFilledIcon, 7 | } from "@radix-ui/react-icons"; 8 | import { TweetMetadata } from "@components/TweetDetails/TweetMetadata"; 9 | import { getUserSession } from "@hooks/getUserSession"; 10 | import Avatar from "@components/Avatar"; 11 | import MainButton from "@components/MainButton"; 12 | import { trpc } from "@utils/trpc"; 13 | import updateSession from "@utils/updateSession"; 14 | 15 | const COLORS: any[] = ["blue", "red", "gold", "gray"]; 16 | 17 | const VerifiedDropdown = ({ closeModal }: { closeModal: any }) => { 18 | let session = getUserSession(); 19 | const [radioSelection, setRadioSelection] = React.useState( 20 | session.badge 21 | ); 22 | let updateBadge = trpc.user.updateBadge.useMutation(); 23 | async function update() { 24 | updateBadge.mutateAsync({ badge: radioSelection }); 25 | updateSession(); 26 | closeModal(); 27 | } 28 | 29 | return ( 30 | <> 31 | 32 | 33 | 34 |
35 | 36 | 37 |
38 |
39 | 40 | 46 | 51 | {COLORS.map((item) => ( 52 | 57 |
58 |
59 | 60 |
61 | 62 |
63 |
64 | ))} 65 |
66 |
67 |
68 |
69 |
70 | 71 | 72 | ); 73 | }; 74 | 75 | export default VerifiedDropdown; 76 | -------------------------------------------------------------------------------- /src/components/modals/VerifiedModal.tsx: -------------------------------------------------------------------------------- 1 | import { Dialog, Transition } from "@headlessui/react"; 2 | import { signIn } from "next-auth/react"; 3 | import { Fragment } from "react"; 4 | 5 | import { useForm, SubmitHandler } from "react-hook-form"; 6 | import { TweetInput } from "@components/inputs/TweetInput"; 7 | import VerifiedDropdown from "./VerifiedDropdown"; 8 | import MainButton from "@components/MainButton"; 9 | 10 | type Inputs = { 11 | username: string; 12 | password: string; 13 | }; 14 | export default function VerifiedModal({ 15 | isOpen, 16 | closeModal, 17 | }: { 18 | isOpen: boolean; 19 | closeModal: any; 20 | }) { 21 | const { 22 | register, 23 | handleSubmit, 24 | watch, 25 | reset, 26 | formState: { errors }, 27 | } = useForm(); 28 | 29 | const onSubmit: SubmitHandler = (data) => { 30 | console.log("cred data", data); 31 | signIn("credentials", { ...data }); 32 | reset(); 33 | }; 34 | return ( 35 | <> 36 | 37 | 38 | 47 |
48 | 49 | 50 |
51 |
52 | 61 | 62 | 65 | Choose your badge 66 | 67 |
68 | 69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 | 77 | ); 78 | } 79 | -------------------------------------------------------------------------------- /src/components/pageComponents/bookmarks/BookmarksContent.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef, useState } from "react"; 2 | import { TweetInput } from "@components/inputs/TweetInput"; 3 | import { trpc } from "@utils/trpc"; 4 | import { Spinner } from "@components/Spinner"; 5 | import { NewTweets } from "@components/NewTweets"; 6 | import { PageHead } from "@components/PageHead"; 7 | import MainTweet from "@components/MainTweet"; 8 | 9 | export default function BookmarksContent() { 10 | let allTweets = trpc.tweet.getAllTweets.useQuery({ id: "anysddssdss" }); 11 | const [tweets, setTweets] = useState(allTweets.data?.tweets); 12 | console.log("tweetssss", tweets, allTweets.data); 13 | useEffect(() => { 14 | setTweets(allTweets.data?.tweets); 15 | }, [allTweets.data]); 16 | 17 | function onPost(body: string) { 18 | const newTweet = { 19 | username: "new", 20 | body, 21 | name: "Test test", 22 | id: Date.now(), 23 | }; 24 | // setTweets([newTweet, ...tweets]); 25 | } 26 | return ( 27 |
28 |
29 | 30 | {tweets?.map((t) => ( 31 | 32 | ))} 33 | 34 |
35 |
36 | ); 37 | } 38 | -------------------------------------------------------------------------------- /src/components/pageComponents/followers/FollowersContent.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef, useState } from "react"; 2 | import { Spinner } from "@components/Spinner"; 3 | import { trpc } from "@utils/trpc"; 4 | import { PageHead } from "@components/PageHead"; 5 | import MainButton from "@components/MainButton"; 6 | import { TweetDetailsMetaData } from "@components/TweetDetails/TweetDetailsMetaData"; 7 | import { UserMetadata } from "@components/UserMetadata/UserMetadata"; 8 | import NextLink from "@components/NextLink"; 9 | export default function FollowersContent({ 10 | username, 11 | showFollowers, 12 | }: { 13 | username: string; 14 | showFollowers: boolean; 15 | }) { 16 | let userFollowers = trpc.user.getUserFollowers.useQuery({ username }); 17 | const [followers, setFollowers] = useState( 18 | userFollowers.data?.userFollowers![ 19 | showFollowers ? "followers" : "following" 20 | ] 21 | ); 22 | useEffect(() => { 23 | setFollowers( 24 | userFollowers.data?.userFollowers![ 25 | showFollowers ? "followers" : "following" 26 | ] 27 | ); 28 | }, [userFollowers.data]); 29 | return ( 30 |
31 | 32 | 33 | {userFollowers.data || userFollowers.isLoading ? ( 34 | <> 35 |
36 | {followers?.map((f) => ( 37 |
38 | {/* @ts-ignore */} 39 | 40 |
41 | 45 | 46 |
47 |
48 |

49 | {/* @ts-ignore */} 50 | {f[showFollowers ? "following" : "follower"].bio} 51 |

52 |
53 | ))} 54 |
55 | 56 | ) : ( 57 | 58 | )} 59 |
60 | ); 61 | } 62 | -------------------------------------------------------------------------------- /src/components/pageComponents/home/HomeContent.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef, useState } from "react"; 2 | import { TweetInput } from "@components/inputs/TweetInput"; 3 | import { trpc } from "@utils/trpc"; 4 | import { Spinner } from "@components/Spinner"; 5 | import { NewTweets } from "@components/NewTweets"; 6 | import { PageHead } from "@components/PageHead"; 7 | import MainTweet from "@components/MainTweet"; 8 | import { useInView } from "react-intersection-observer"; 9 | import { newTweet } from "../../../../server/src/router/routes/tweetRouter/newTweet"; 10 | 11 | export default function HomeContent() { 12 | let getTweets = trpc.tweet.getAllTweets.useMutation(); 13 | const [tweets, setTweets] = useState(getTweets.data?.tweets); 14 | const [hasMore, setHasMore] = useState(false); 15 | 16 | const { ref, inView, entry } = useInView({ 17 | threshold: 0, 18 | }); 19 | async function fetchTweets() { 20 | const skip = tweets?.length || 0; 21 | const newTweets = await getTweets.mutateAsync({ skip }); 22 | const uniqueTweets = removeDuplicates([ 23 | ...(tweets || []), 24 | ...(newTweets?.tweets || []), 25 | ]); 26 | setTweets(uniqueTweets); 27 | setHasMore(newTweets.hasMore); 28 | } 29 | 30 | function removeDuplicates(tweets:any) { 31 | const tweetSet = new Set(); 32 | return tweets.filter((tweet) => { 33 | if (tweetSet.has(tweet.id)) { 34 | return false; 35 | } else { 36 | tweetSet.add(tweet.id); 37 | return true; 38 | } 39 | }); 40 | } 41 | 42 | useEffect(() => { 43 | fetchTweets(); 44 | }, []); 45 | 46 | // Refetch tweets when inView and not loading 47 | useEffect(() => { 48 | if (inView && !getTweets.isLoading && hasMore) { 49 | fetchTweets(); 50 | } 51 | }, [getTweets.isLoading, inView, tweets, hasMore]); 52 | 53 | function onPost(data: any) { 54 | //@ts-ignore 55 | data && setTweets([data, ...tweets]); 56 | } 57 | return ( 58 |
59 |
60 | 61 | 62 | 63 | {/* 64 | */} 65 | {tweets?.map((t, i) => ( 66 | 67 | ))} 68 |
69 | {hasMore && } 70 | {getTweets.isLoading && } 71 |
72 |
73 |
74 | ); 75 | } 76 | -------------------------------------------------------------------------------- /src/components/pageComponents/messages/MessagesContent.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef, useState } from "react"; 2 | import { TweetInput } from "@components/inputs/TweetInput"; 3 | import { trpc } from "@utils/trpc"; 4 | import { Spinner } from "@components/Spinner"; 5 | import { NewTweets } from "@components/NewTweets"; 6 | import { PageHead } from "@components/PageHead"; 7 | import MainTweet from "@components/MainTweet"; 8 | import Avatar from "@components/Avatar"; 9 | import { PickVerificationIcon } from "@components/PickVerificationIcon"; 10 | import NextLink from "@components/NextLink"; 11 | 12 | export default function MessagesContent() { 13 | let allTweets = trpc.tweet.getAllTweets.useQuery({ id: "anysddssdss" }); 14 | const [tweets, setTweets] = useState(allTweets.data?.tweets); 15 | console.log("tweetssss", tweets, allTweets.data); 16 | useEffect(() => { 17 | setTweets(allTweets.data?.tweets); 18 | }, [allTweets.data]); 19 | 20 | function onPost(body: string) { 21 | const newTweet = { 22 | username: "new", 23 | body, 24 | name: "Test test", 25 | id: Date.now(), 26 | }; 27 | // setTweets([newTweet, ...tweets]); 28 | } 29 | return ( 30 |
31 |
32 | 33 |
34 | 35 | 36 | 37 | 38 |
39 | 40 |
41 |
42 | ); 43 | } 44 | 45 | function Message() { 46 | return ( 47 |
48 | 49 |
50 |

51 | aland 52 | 53 | 54 | @{"aland"} · {"10 pm "} 55 | 56 |

57 | 58 | {"aland"} 59 | 60 |
61 |
62 | ); 63 | } 64 | -------------------------------------------------------------------------------- /src/components/pageComponents/profile/EditProfileBtn.tsx: -------------------------------------------------------------------------------- 1 | export function EditProfileBtn({ onClick }: { onClick: any }) { 2 | return ( 3 |
4 | 7 |
8 | ); 9 | } 10 | -------------------------------------------------------------------------------- /src/components/pageComponents/profile/FollowStats.tsx: -------------------------------------------------------------------------------- 1 | import NextLink from "@components/NextLink"; 2 | 3 | export function FollowStats({ username ,following, followers}: { username: string,followers:number,following:number }) { 4 | return ( 5 |
6 |
7 | 8 | {following} 9 | Following 10 | 11 |
12 |
13 | 14 | {followers} 15 | Followers 16 | 17 |
18 |
19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /src/components/pageComponents/profile/Joined.tsx: -------------------------------------------------------------------------------- 1 | import { formatDate } from "@utils/date"; 2 | 3 | export function Joined({ date }: { date: Date }) { 4 | let formattedDate = formatDate(date) 5 | return ( 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | {" "} 20 | Joined {formattedDate} 21 | 22 | ); 23 | } 24 | 25 | -------------------------------------------------------------------------------- /src/components/pageComponents/profile/ProfileContent.tsx: -------------------------------------------------------------------------------- 1 | import { type NextPage } from "next"; 2 | import { PageHead } from "@components/PageHead"; 3 | import { useRouter } from "next/router"; 4 | import { Url } from "./Url"; 5 | import { Joined } from "./Joined"; 6 | import { FollowStats } from "./FollowStats"; 7 | import Avatar from "@components/Avatar"; 8 | import MainTweet from "@components/MainTweet"; 9 | import { trpc } from "@utils/trpc"; 10 | import { useEffect, useState } from "react"; 11 | import { EditProfileBtn } from "./EditProfileBtn"; 12 | import EditProfileModal from "@components/modals/EditProfileModal"; 13 | import { Spinner } from "@components/Spinner"; 14 | import { SEO } from "@components/SEO"; 15 | import { getUserSession } from "@hooks/getUserSession"; 16 | import MainButton from "@components/MainButton"; 17 | import { User } from "@prisma/client"; 18 | import { PickVerificationIcon } from "@components/PickVerificationIcon"; 19 | 20 | export const ProfileContent: NextPage = () => { 21 | const router = useRouter(); 22 | const { username } = router.query as { username: string }; 23 | let getUser = trpc.user.getUser.useQuery({ username }); 24 | let followUser = trpc.user.followUser.useMutation(); 25 | const [user, setUser] = useState(getUser.data?.user); 26 | let session = getUserSession(); 27 | const [isFollowing, setIsFollowing] = useState( 28 | user?.followers.some((f) => f.followingId === session.id) 29 | ); 30 | useEffect(() => { 31 | setUser(getUser.data?.user); 32 | setIsFollowing(user?.followers.some((f) => f.followingId === session.id)); 33 | }, [getUser.data, user]); 34 | let [isOpen, setIsOpen] = useState(false); 35 | function toggleModal() { 36 | setIsOpen(!isOpen); 37 | } 38 | function editProfile(data: User) { 39 | // @ts-ignore 40 | setUser({ ...user, ...data }); 41 | } 42 | async function follow() { 43 | setIsFollowing(!isFollowing); 44 | let res = await followUser.mutateAsync({ id: user?.id! }); 45 | console.log("tressss", res); 46 | } 47 | return ( 48 | <> 49 |
50 | 56 | 57 | {user ? ( 58 | <> 59 |
60 | 61 |
62 |
63 |
64 | 65 |
66 | {user.id === session.id ? ( 67 | 68 | ) : ( 69 | 73 | )} 74 |
75 |
76 |
77 |
78 |

79 | {user?.name || username} 80 |

81 | {/* @ts-ignore */} 82 | 83 |
84 |

85 | @{username} 86 |

87 |
88 |
89 |

90 | {user?.bio} 91 |

92 |
93 | 94 | 95 |
96 |
97 | 102 |
103 |
104 |
105 |
106 |
    107 | {user?.tweets?.map((t) => ( 108 |
    109 | 110 |
    111 | ))} 112 |
113 | 119 | 120 | ) : ( 121 | <> 122 | 123 | 124 | )} 125 |
126 | 127 | ); 128 | }; 129 | 130 | function BgImg({ src }: { src: string }) { 131 | return ( 132 | 137 | ); 138 | } 139 | -------------------------------------------------------------------------------- /src/components/pageComponents/profile/Url.tsx: -------------------------------------------------------------------------------- 1 | export function Url({ website }: { website: string }) { 2 | if (!website || !website.trim()) return <>; 3 | return ( 4 | 5 | 6 | 7 | 8 | 9 | 10 | {" "} 11 | 16 | {website} 17 | 18 | 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /src/components/pageComponents/settings/SettingsContent.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef, useState } from "react"; 2 | import { useForm, SubmitHandler } from "react-hook-form"; 3 | import { TweetInput } from "@components/inputs/TweetInput"; 4 | import { trpc } from "@utils/trpc"; 5 | import { Spinner } from "@components/Spinner"; 6 | import { NewTweets } from "@components/NewTweets"; 7 | import { PageHead } from "@components/PageHead"; 8 | import MainTweet from "@components/MainTweet"; 9 | import Avatar from "@components/Avatar"; 10 | import { PickVerificationIcon } from "@components/PickVerificationIcon"; 11 | import NextLink from "@components/NextLink"; 12 | import MainButton from "@components/MainButton"; 13 | 14 | export default function SettingsContent() { 15 | const { 16 | register, 17 | handleSubmit, 18 | watch, 19 | reset, 20 | formState: { errors }, 21 | } = useForm(); 22 | let allTweets = trpc.tweet.getAllTweets.useQuery({ id: "anysddssdss" }); 23 | const [tweets, setTweets] = useState(allTweets.data?.tweets); 24 | console.log("tweetssss", tweets, allTweets.data); 25 | useEffect(() => { 26 | setTweets(allTweets.data?.tweets); 27 | }, [allTweets.data]); 28 | 29 | return ( 30 |
31 |
32 | 33 |
34 | 41 | 42 |
43 | 44 |
45 |
46 | ); 47 | } 48 | -------------------------------------------------------------------------------- /src/components/pageComponents/tweet/TweetContent.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef, useState } from "react"; 2 | import { Spinner } from "@components/Spinner"; 3 | import { trpc } from "@utils/trpc"; 4 | import { PageHead } from "@components/PageHead"; 5 | import { useEventContext } from "@context/EventContext"; 6 | import { TweetDetails } from "@components/TweetDetails"; 7 | 8 | export default function TweetContent({ tweetId }: { tweetId: string }) { 9 | let { data } = trpc.tweet.getTweet.useQuery({ id: tweetId }); 10 | 11 | console.log("tweeet", data); 12 | // let allTweets = trpc.tweet.getAllTweets.useQuery({ id: "anysddssdss" }); 13 | // const [tweets, setTweets] = useState(allTweets.data?.tweets); 14 | // console.log("tweetssss", tweets, allTweets.data); 15 | // useEffect(() => { 16 | // setTweets(allTweets.data?.tweets); 17 | // }, [allTweets.data]); 18 | // 19 | return ( 20 |
21 | 22 | {data?.tweet ? ( 23 | 24 | ) : ( 25 | 26 | )} 27 |
28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /src/hooks/getUserSession.tsx: -------------------------------------------------------------------------------- 1 | import { UserData } from "@types"; 2 | import { useSession } from "next-auth/react"; 3 | 4 | export function getUserSession(): UserData { 5 | let { data } = useSession(); 6 | return data?.userData!; 7 | } 8 | -------------------------------------------------------------------------------- /src/hooks/registerListener.tsx: -------------------------------------------------------------------------------- 1 | import { useEventContext } from "@context/EventContext"; 2 | import { useEffect, useState } from "react"; 3 | 4 | export function registerListener(id: string, callback: (data: any) => void) { 5 | const { registerListener, unregisterListener } = useEventContext(); 6 | const [registered, setRegistered] = useState(false); 7 | 8 | useEffect(() => { 9 | if (!registered) { 10 | registerListener(id, callback); 11 | setRegistered(true); 12 | } 13 | return () => { 14 | unregisterListener(id); 15 | }; 16 | }, []); 17 | } 18 | -------------------------------------------------------------------------------- /src/hooks/useFormattedDate.tsx: -------------------------------------------------------------------------------- 1 | import { formatDate } from "@utils/date"; 2 | import { useEffect, useState } from "react"; 3 | 4 | export function useFormattedDate(date:Date) { 5 | const [formattedDate, setFormattedDate] = useState(formatDate(date)); 6 | 7 | useEffect(() => { 8 | const interval = setInterval(() => { 9 | setFormattedDate(formatDate(date)); 10 | }, 1000); 11 | return () => clearInterval(interval); 12 | }, [date]); 13 | 14 | return formattedDate; 15 | } 16 | -------------------------------------------------------------------------------- /src/icons/CameraPlusIcon.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { SVGProps } from "react" 3 | 4 | const CameraPlusIcon = (props: SVGProps) => ( 5 | 13 | ) 14 | 15 | export default CameraPlusIcon 16 | -------------------------------------------------------------------------------- /src/icons/CloseIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export default function CloseIcon() { 4 | return ( 5 | 13 | 18 | 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /src/icons/social/discord.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { SVGProps } from "react"; 3 | 4 | const DiscordIcon = (props: SVGProps) => ( 5 | 10 | 14 | 15 | ); 16 | 17 | export default DiscordIcon; 18 | -------------------------------------------------------------------------------- /src/icons/social/github.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { SVGProps } from "react"; 3 | 4 | const GithubIcon = (props: SVGProps) => ( 5 | 10 | 13 | 14 | ); 15 | 16 | export default GithubIcon; 17 | -------------------------------------------------------------------------------- /src/icons/social/google.tsx: -------------------------------------------------------------------------------- 1 | 2 | import * as React from "react" 3 | import { SVGProps } from "react" 4 | 5 | const GoogleIcon = (props: SVGProps) => ( 6 | 11 | 12 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | ) 26 | 27 | export default GoogleIcon 28 | -------------------------------------------------------------------------------- /src/icons/social/twitter.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { SVGProps } from "react"; 3 | 4 | const TwitterIcon = (props: SVGProps) => ( 5 | 11 | 15 | 16 | ); 17 | 18 | export default TwitterIcon; 19 | -------------------------------------------------------------------------------- /src/icons/tweet/Arrow.tsx: -------------------------------------------------------------------------------- 1 | export default function ArrowIcon({ 2 | className, 3 | pathClass, 4 | }: { 5 | className?: string; 6 | pathClass?: string; 7 | }) { 8 | return ( 9 | 17 | 22 | 23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /src/icons/tweet/LikeIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export default function LikeIcon({ className }: { className?: string }) { 4 | return ( 5 | 12 | 17 | 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /src/icons/tweet/ReplyIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export default function ReplyIcon() { 4 | return ( 5 | 6 | 7 | 8 | 9 | 10 | ) 11 | } 12 | -------------------------------------------------------------------------------- /src/icons/tweet/RetweetIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export default function RetweetIcon() { 4 | return ( 5 | 6 | 7 | 8 | 9 | 10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /src/icons/tweet/ShareIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export default function ShareIcon() { 4 | return ( 5 | 6 | 7 | 8 | 9 | 10 | 11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /src/icons/tweet/VerifiedIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export function VerifiedIcon() { 4 | return ( 5 | 11 | 12 | 13 | 14 | 15 | ); 16 | } 17 | 18 | -------------------------------------------------------------------------------- /src/icons/verified/Blue.tsx: -------------------------------------------------------------------------------- 1 | import cn from "clsx"; 2 | export default function BlueVerified({ className }: { className?: string }) { 3 | return ( 4 | 10 | 11 | 12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /src/icons/verified/Gold.tsx: -------------------------------------------------------------------------------- 1 | 2 | export default function GoldVerified() { 3 | return ( 4 | 10 | 15 | 16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /src/icons/verified/Gray.tsx: -------------------------------------------------------------------------------- 1 | 2 | export default function GrayVerified() { 3 | return ( 4 | 10 | 15 | 16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /src/icons/verified/Red.tsx: -------------------------------------------------------------------------------- 1 | 2 | export default function RedVerified() { 3 | return ( 4 | 10 | 15 | 16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /src/icons/verified/index.tsx: -------------------------------------------------------------------------------- 1 | import BlueVerified from "./Blue"; 2 | import GoldVerified from "./Gold"; 3 | import GrayVerified from "./Gray"; 4 | import RedVerified from "./Red"; 5 | 6 | export{BlueVerified ,GrayVerified,GoldVerified,RedVerified} 7 | -------------------------------------------------------------------------------- /src/pages/[username]/followers.tsx: -------------------------------------------------------------------------------- 1 | import { type NextPage } from "next"; 2 | import { useRouter } from "next/router"; 3 | import SidebarLeft from "@components/SidebarLeft"; 4 | import SidebarRight from "@components/SidebarRight/SidebarRight"; 5 | import { SEO } from "@components/SEO"; 6 | import FollowersContent from "@components/pageComponents/followers/FollowersContent"; 7 | 8 | const Tweet: NextPage = () => { 9 | const router = useRouter(); 10 | const { username } = router.query as { username: string }; 11 | return ( 12 | <> 13 | 14 |
15 |
16 | 17 | 18 | 19 |
20 |
21 | 22 | ); 23 | }; 24 | 25 | export default Tweet; 26 | -------------------------------------------------------------------------------- /src/pages/[username]/following.tsx: -------------------------------------------------------------------------------- 1 | import { type NextPage } from "next"; 2 | import { useRouter } from "next/router"; 3 | import SidebarLeft from "@components/SidebarLeft"; 4 | import SidebarRight from "@components/SidebarRight/SidebarRight"; 5 | import { SEO } from "@components/SEO"; 6 | import FollowersContent from "@components/pageComponents/followers/FollowersContent"; 7 | 8 | const Tweet: NextPage = () => { 9 | const router = useRouter(); 10 | const { username } = router.query as { username: string }; 11 | 12 | return ( 13 | <> 14 | 15 |
16 |
17 | 18 | 19 | 20 |
21 |
22 | 23 | ); 24 | }; 25 | 26 | export default Tweet; 27 | -------------------------------------------------------------------------------- /src/pages/[username]/index.tsx: -------------------------------------------------------------------------------- 1 | import { SEO } from "@components/SEO"; 2 | import { type NextPage } from "next"; 3 | import SidebarLeft from "@components/SidebarLeft"; 4 | import SidebarRight from "@components/SidebarRight"; 5 | import { ProfileContent } from "@components/pageComponents/profile/ProfileContent"; 6 | import { getUserSession } from "@hooks/getUserSession"; 7 | import { useRouter } from "next/router"; 8 | 9 | const ProfilePage: NextPage = () => { 10 | let session = getUserSession(); 11 | const router = useRouter(); 12 | const { username } = router.query as { username: string }; 13 | return ( 14 | <> 15 | 16 |
17 |
18 | 19 | 20 | 21 |
22 |
23 | 24 | ); 25 | }; 26 | 27 | export default ProfilePage; 28 | -------------------------------------------------------------------------------- /src/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import { type AppType } from "next/app"; 2 | import { Analytics } from "@vercel/analytics/react"; 3 | import { SessionProvider } from "next-auth/react"; 4 | import Auth from "../Auth/Auth"; 5 | import { ThemeProvider } from "next-themes"; 6 | import { trpc } from "../utils/trpc"; 7 | import { Toaster } from "react-hot-toast"; 8 | import "../styles/globals.css"; 9 | 10 | const MyApp: AppType = ({ 11 | Component, 12 | pageProps: { session, ...pageProps }, 13 | }: any) => { 14 | return ( 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | ); 25 | }; 26 | 27 | export default trpc.withTRPC(MyApp); 28 | -------------------------------------------------------------------------------- /src/pages/api/auth/[...nextauth].ts: -------------------------------------------------------------------------------- 1 | import NextAuth, { getServerSession, NextAuthOptions, User } from "next-auth"; 2 | import DiscordProvider from "next-auth/providers/discord"; 3 | import GithubProvider from "next-auth/providers/github"; 4 | import GoogleProvider from "next-auth/providers/google"; 5 | import CredentialsProvider from "next-auth/providers/credentials"; 6 | import { 7 | GetServerSidePropsContext, 8 | NextApiRequest, 9 | NextApiResponse, 10 | } from "next"; 11 | import jwt from "jsonwebtoken"; 12 | import superjson from "superjson"; 13 | import { UserData } from "@types"; 14 | import { createTRPCProxyClient, httpBatchLink } from "@trpc/client"; 15 | import { AppRouter } from "../../../../server/src/router/root"; 16 | 17 | const client = createTRPCProxyClient({ 18 | transformer: superjson, 19 | links: [ 20 | httpBatchLink({ 21 | url: process.env.NEXTAUTH_URL + "/api/trpc", 22 | headers() { 23 | return { 24 | pass: process.env.SERVER_SECRET, 25 | }; 26 | }, 27 | }), 28 | ], 29 | }); 30 | 31 | declare module "next-auth" { 32 | interface Session { 33 | userData: UserData; 34 | } 35 | interface User { 36 | userData: UserData; 37 | } 38 | } 39 | 40 | declare module "next-auth/jwt" { 41 | /** Returned by the `jwt` callback and `getToken`, when using JWT sessions */ 42 | interface JWT { 43 | userData: UserData; 44 | } 45 | } 46 | 47 | export function authOptions(update?: boolean): NextAuthOptions { 48 | return { 49 | callbacks: { 50 | async signIn(p) { 51 | let success = false; 52 | let body = {}; 53 | if (!p.credentials) { 54 | let provider = p.account?.provider; 55 | let username = p.user.name?.replace(/\s/g, "") 56 | let email = p.user.email; 57 | body = { 58 | provider, 59 | username, 60 | name: username, 61 | email, 62 | }; 63 | } else { 64 | body = { 65 | provider: "credentials", 66 | username: p.credentials.username, 67 | password: p.credentials.password, 68 | }; 69 | } 70 | try { 71 | // @ts-ignore 72 | let createUser = await client.user.createUser.mutate({ ...body }); 73 | success = createUser.success; 74 | let userData = createUser.data; 75 | if (typeof createUser.data === "string") { 76 | throw new Error(createUser.data); 77 | } 78 | // @ts-ignore 79 | p.user.userData = userData; 80 | } catch (e: any) {} 81 | return success; 82 | }, 83 | 84 | async jwt(p) { 85 | if (update) { 86 | let { user } = await client.user.getUser.query({ 87 | id: p.token.userData.id, 88 | }); 89 | //@ts-ignore 90 | p.token.userData = user; 91 | } else { 92 | p.token.userData = p.user?.userData || p.token.userData; 93 | } 94 | // params.token.customData = params.user?.customData || params.token.customData; 95 | return p.token; 96 | }, 97 | async session(p) { 98 | if (update) { 99 | let { user } = await client.user.getUser.query({ 100 | id: p.token.userData.id, 101 | }); 102 | //@ts-ignore 103 | p.session.userData = user; 104 | } else { 105 | p.session.userData = p.token.userData; 106 | } 107 | return p.session; 108 | }, 109 | }, 110 | providers: getProviders(), 111 | jwt: { 112 | async encode(p) { 113 | let token = jwt.sign(p.token!, p.secret); 114 | return token; 115 | }, 116 | // @ts-ignore 117 | async decode(p) { 118 | // @ts-ignore 119 | let decoded = jwt.verify(p.token, p.secret); 120 | return decoded; 121 | }, 122 | }, 123 | }; 124 | } 125 | export const getServerAuthSession = (ctx: { 126 | req: GetServerSidePropsContext["req"]; 127 | res: GetServerSidePropsContext["res"]; 128 | }) => { 129 | return getServerSession(ctx.req, ctx.res, authOptions()); 130 | }; 131 | function getProviders() { 132 | return [ 133 | GithubProvider({ 134 | clientId: process.env.GITHUB_ID!, 135 | clientSecret: process.env.GITHUB_SECRET!, 136 | }), 137 | GoogleProvider({ 138 | clientId: process.env.GOOGLE_CLIENT_ID!, 139 | clientSecret: process.env.GOOGLE_CLIENT_SECRET!, 140 | }), 141 | DiscordProvider({ 142 | clientId: process.env.DISCORD_CLIENT_ID!, 143 | clientSecret: process.env.DISCORD_CLIENT_SECRET!, 144 | }), 145 | CredentialsProvider({ 146 | id: "credentials", 147 | name: "credentials", 148 | credentials: { 149 | username: { 150 | label: "Username", 151 | type: "text", 152 | placeholder: "Username", 153 | }, 154 | password: { label: "Password", type: "password" }, 155 | }, 156 | authorize: async (credentials) => { 157 | let body = {}; 158 | if (!credentials) { 159 | throw new Error("Invalid login"); 160 | } else { 161 | body = { 162 | provider: "credentials", 163 | username: credentials.username, 164 | password: credentials.password, 165 | }; 166 | } 167 | // @ts-ignore 168 | let createUser = await client.user.createUser.mutate({ ...body }); 169 | // @ts-ignore 170 | let userData: User = { userData: createUser.data }; 171 | if (typeof createUser.data === "string") { 172 | throw new Error(createUser.data); 173 | } 174 | return userData; 175 | }, 176 | }), 177 | ]; 178 | } 179 | 180 | const handler = async (req: NextApiRequest, res: NextApiResponse) => { 181 | // @ts-ignore 182 | return await NextAuth(req, res, authOptions(req?.query?.update)); 183 | }; 184 | export default handler; 185 | -------------------------------------------------------------------------------- /src/pages/api/trpc/[trpc].ts: -------------------------------------------------------------------------------- 1 | import { createNextApiHandler } from "@trpc/server/adapters/next"; 2 | import { appRouter } from "../../../../server/src/router/root"; 3 | import { createContext } from "../../../../server/src/trpc/trpc"; 4 | 5 | // export API handler 6 | export default createNextApiHandler({ 7 | router: appRouter, 8 | createContext: createContext, 9 | }); 10 | -------------------------------------------------------------------------------- /src/pages/bookmarks.tsx: -------------------------------------------------------------------------------- 1 | import { SEO } from "@components/SEO"; 2 | import { type NextPage } from "next"; 3 | import SidebarLeft from "@components/SidebarLeft"; 4 | import SidebarRight from "@components/SidebarRight/SidebarRight"; 5 | import BookmarksContent from "@components/pageComponents/bookmarks/BookmarksContent"; 6 | 7 | const Home: NextPage = () => { 8 | return ( 9 | <> 10 | 11 |
12 |
13 | 14 | 15 | 16 |
17 |
18 | 19 | ); 20 | }; 21 | 22 | export default Home; 23 | -------------------------------------------------------------------------------- /src/pages/home.tsx: -------------------------------------------------------------------------------- 1 | import { SEO } from "@components/SEO"; 2 | import { type NextPage } from "next"; 3 | import SidebarLeft from "@components/SidebarLeft"; 4 | import SidebarRight from "@components/SidebarRight/SidebarRight"; 5 | import HomeContent from "@components/pageComponents/home/HomeContent"; 6 | 7 | const Home: NextPage = () => { 8 | return ( 9 | <> 10 | 11 |
12 |
13 | 14 | 15 | 16 |
17 |
18 | 19 | ); 20 | }; 21 | 22 | export default Home; 23 | -------------------------------------------------------------------------------- /src/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import { SEO } from "@components/SEO"; 2 | import { type NextPage } from "next"; 3 | import SidebarLeft from "@components/SidebarLeft"; 4 | import SidebarRight from "@components/SidebarRight"; 5 | import HomeContent from "@components/pageComponents/home/HomeContent"; 6 | 7 | const Home: NextPage = () => { 8 | return ( 9 | <> 10 | 11 |
12 |
13 | 14 | 15 | 16 |
17 |
18 | 19 | ); 20 | }; 21 | 22 | export default Home 23 | -------------------------------------------------------------------------------- /src/pages/messages.tsx: -------------------------------------------------------------------------------- 1 | import { SEO } from "@components/SEO"; 2 | import { type NextPage } from "next"; 3 | import SidebarLeft from "@components/SidebarLeft"; 4 | import DirectMessage from "@components/DirectMessage"; 5 | import MessagesContent from "@components/pageComponents/messages/MessagesContent"; 6 | 7 | const Home: NextPage = () => { 8 | return ( 9 | <> 10 | 11 |
12 |
13 | 14 | 15 | 16 |
17 |
18 | 19 | ); 20 | }; 21 | 22 | export default Home; 23 | -------------------------------------------------------------------------------- /src/pages/settings.tsx: -------------------------------------------------------------------------------- 1 | import { SEO } from "@components/SEO"; 2 | import { type NextPage } from "next"; 3 | import SidebarLeft from "@components/SidebarLeft"; 4 | import DirectMessage from "@components/DirectMessage"; 5 | import MessagesContent from "@components/pageComponents/messages/MessagesContent"; 6 | import SettingsContent from "@components/pageComponents/settings/SettingsContent"; 7 | 8 | const Home: NextPage = () => { 9 | return ( 10 | <> 11 | 12 |
13 |
14 | 15 | 16 |
17 | 18 |
19 |
20 |
21 | 22 | ); 23 | }; 24 | 25 | export default Home; 26 | -------------------------------------------------------------------------------- /src/pages/tweet/[tweetId].tsx: -------------------------------------------------------------------------------- 1 | import { type NextPage } from "next"; 2 | import { useRouter } from "next/router"; 3 | import SidebarLeft from "@components/SidebarLeft"; 4 | import SidebarRight from "@components/SidebarRight"; 5 | import { SEO } from "@components/SEO"; 6 | import TweetContent from "@components/pageComponents/tweet/TweetContent"; 7 | 8 | const Tweet: NextPage = () => { 9 | const router = useRouter(); 10 | const { tweetId } = router.query as { tweetId: string }; 11 | 12 | return ( 13 | <> 14 | 15 |
16 |
17 | 18 | 19 | 20 |
21 |
22 | 23 | ); 24 | }; 25 | 26 | export default Tweet; 27 | -------------------------------------------------------------------------------- /src/styles/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | body { 5 | @apply bg-slate-200 text-black dark:bg-black dark:text-white; 6 | } 7 | button { 8 | @apply transition-colors duration-150; 9 | } 10 | h2,h1,h3,h4 { 11 | @apply truncate text-ellipsis 12 | } 13 | @keyframes fadeIn { 14 | 0% { 15 | opacity: 0; 16 | } 17 | 100% { 18 | opacity: 1; 19 | } 20 | } 21 | .fade-in { 22 | animation: fadeIn 0.5s ease-out; 23 | } 24 | 25 | .bg-main { 26 | @apply bg-slate-200 text-black dark:bg-black dark:text-white; 27 | } 28 | .bg-secondary { 29 | @apply bg-slate-400 text-black dark:bg-slate-800 dark:text-white; 30 | } 31 | .bg-secondary-hover { 32 | @apply hover:bg-slate-300 dark:hover:bg-slate-700; 33 | } 34 | .main-border { 35 | @apply border-[#262A2D]; 36 | } 37 | 38 | .tweet-hover { 39 | @apply transition-colors duration-150 hover:bg-slate-300 dark:hover:bg-slate-900; 40 | } 41 | .sidebar-hover { 42 | @apply transition-colors duration-150 hover:bg-slate-300 dark:hover:bg-[#1d1e23]; 43 | } 44 | .hover-main { 45 | @apply cursor-pointer transition-colors duration-150 hover:bg-slate-300 dark:hover:bg-slate-900; 46 | } 47 | .hover-options { 48 | @apply cursor-pointer transition-colors duration-150 hover:bg-slate-400 dark:hover:bg-slate-800; 49 | } 50 | 51 | .text-main-accent { 52 | @apply text-[#1d9bf0] dark:text-[#1d9bf0]; 53 | } 54 | .text-primary { 55 | @apply text-black dark:text-white; 56 | } 57 | .text-secondary { 58 | @apply text-[#536471] dark:text-[#71767B]; 59 | } 60 | .bg-line-reply { 61 | @apply bg-[#333639] dark:bg-[#333639]; 62 | } 63 | .text-tweet { 64 | @apply text-slate-900 dark:text-slate-300; 65 | } 66 | /* main-content-size */ 67 | .mcz { 68 | @apply mx-4 h-full sm:w-[600px]; 69 | } 70 | .input { 71 | text-align: center; 72 | color: #fff; 73 | transition-property: transform, opacity; 74 | text-transform: linear; 75 | transition-duration: 0.25s; 76 | } 77 | .sidebar-bg{ 78 | @apply bg-[#16181C] 79 | } 80 | -------------------------------------------------------------------------------- /src/utils/comporessImage.ts: -------------------------------------------------------------------------------- 1 | import { compress } from "image-conversion"; 2 | export async function compressFile(file: File, quality: number): Promise { 3 | let compressed = await compress(file, quality); 4 | return compressed; 5 | } 6 | -------------------------------------------------------------------------------- /src/utils/date.ts: -------------------------------------------------------------------------------- 1 | import moment from "moment"; 2 | 3 | export function formatDate(date: Date) { 4 | const now = moment(); 5 | const dateMoment = moment(date); 6 | if (dateMoment.isSame(now, "day")) { 7 | return dateMoment.fromNow(); 8 | } else { 9 | return dateMoment.format("D MMM"); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/utils/limitText.ts: -------------------------------------------------------------------------------- 1 | export function limitText(text: string) { 2 | if (text.length > 15) return text.slice(0, 15) + "..."; 3 | return text 4 | } 5 | -------------------------------------------------------------------------------- /src/utils/trpc.ts: -------------------------------------------------------------------------------- 1 | import { createTRPCNext } from "@trpc/next"; 2 | import { httpBatchLink, loggerLink } from "@trpc/client"; 3 | import superjson from "superjson"; 4 | import { AppRouter } from "../../server/src/router/root"; 5 | const getBaseUrl = () => { 6 | if (typeof window !== "undefined") return ""; // browser should use relative url 7 | if (process.env.VERCEL_URL) return `https://${process.env.VERCEL_URL}`; // SSR should use vercel url 8 | return `http://localhost:${process.env.PORT ?? 3000}`; // dev SSR should use localhost 9 | }; 10 | export const trpc = createTRPCNext({ 11 | config() { 12 | return { 13 | transformer: superjson, 14 | links: [ 15 | loggerLink({ 16 | enabled: (opts) => 17 | process.env.NODE_ENV === "development" || 18 | (opts.direction === "down" && opts.result instanceof Error), 19 | }), 20 | httpBatchLink({ 21 | url: `${getBaseUrl()}/api/trpc`, 22 | }), 23 | 24 | ], 25 | }; 26 | }, 27 | ssr: false, 28 | }); 29 | -------------------------------------------------------------------------------- /src/utils/updateSession.ts: -------------------------------------------------------------------------------- 1 | export default async function updateSession() { 2 | let res = await fetch("/api/auth/session?update=true"); 3 | console.log(await res.json()); 4 | setTimeout(async () => { 5 | let res = await fetch("/api/auth/session?update=true"); 6 | console.log(await res.json()); 7 | }, 1000); 8 | } 9 | -------------------------------------------------------------------------------- /tailwind.config.cjs: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | const defaultTheme = require("tailwindcss/defaultTheme"); 3 | module.exports = { 4 | darkMode: "class", 5 | content: ["./src/**/*.{js,ts,jsx,tsx}"], 6 | theme: { 7 | screens: { 8 | xs: "475px", 9 | ...defaultTheme.screens, 10 | }, 11 | extend: { 12 | animation: { 13 | "spin-fast": "spin 0.7s linear infinite", 14 | }, 15 | }, 16 | }, 17 | plugins: [], 18 | }; 19 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2017", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "checkJs": true, 7 | "skipLibCheck": true, 8 | "strict": true, 9 | "forceConsistentCasingInFileNames": true, 10 | "noEmit": true, 11 | "esModuleInterop": true, 12 | "module": "esnext", 13 | "moduleResolution": "node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "jsx": "preserve", 17 | "incremental": true, 18 | "noUncheckedIndexedAccess": true, 19 | "baseUrl": "src", 20 | "paths": { 21 | "@components/*": ["components/*"], 22 | "@hooks/*": ["hooks/*"], 23 | "@icons/*": ["icons/*"], 24 | "@context/*": ["context/*"], 25 | "@utils/*": ["./utils/*"], 26 | }, 27 | "incremental": true 28 | }, 29 | "include": [ 30 | "next-env.d.ts", 31 | "**/*.ts", 32 | "**/*.tsx" 33 | ], 34 | "exclude": [ 35 | "node_modules" 36 | ] 37 | } 38 | --------------------------------------------------------------------------------