├── .eslintrc.json ├── src ├── utils │ ├── index.ts │ ├── helpers.ts │ └── mockedData.ts ├── app │ ├── favicon.ico │ ├── api │ │ └── hello │ │ │ └── route.ts │ ├── [...not-found] │ │ └── page.tsx │ ├── .prettierrc │ ├── page.tsx │ ├── not-found.tsx │ ├── Providers.tsx │ ├── error.tsx │ ├── layout.tsx │ ├── [profile] │ │ └── page.tsx │ ├── sign-up │ │ └── page.tsx │ └── sign-in │ │ └── page.tsx ├── styles │ └── globals.css ├── types │ ├── news.ts │ ├── randomUser.ts │ └── tweets.ts ├── components │ ├── Comments.tsx │ ├── Search.tsx │ ├── Trend.tsx │ ├── Article.tsx │ ├── RandomUser.tsx │ ├── Widget.tsx │ ├── Messages.tsx │ ├── Comment.tsx │ ├── DarkModSwitch.tsx │ ├── SideBarOption.tsx │ ├── Trending.tsx │ ├── News.tsx │ ├── Auth.tsx │ ├── WhoToFollow.tsx │ ├── Button.tsx │ ├── Sidebar.tsx │ ├── TweetTemp.tsx │ ├── Tweets.tsx │ ├── TweetInput.tsx │ └── Tweet.tsx ├── config │ └── firebase.ts └── pages │ └── api │ └── auth │ └── [...nextauth].tsx ├── postcss.config.js ├── public ├── images │ ├── a-apple.png │ ├── banner.jpeg │ ├── favicon.ico │ ├── saddam.jpg │ ├── G-google.png │ ├── Twitter-logo.png │ ├── Twitter-logo.svg.png │ ├── twitter-banner.jpeg │ ├── linkedin-banner-01.jpg │ ├── vercel.svg │ ├── loading.svg │ ├── thirteen.svg │ ├── next.svg │ └── svg │ │ └── Spinner-1s-200px.svg ├── vercel.svg ├── thirteen.svg └── next.svg ├── next.config.js ├── .prettierignore ├── .gitignore ├── .vscode └── settings.json ├── tsconfig.json ├── package.json ├── README.md └── tailwind.config.js /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | // all util will be inside utils file 2 | export const until = 'test'; 3 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saddamarbaa/twitter-clone-app-nextjs-typescript/HEAD/src/app/favicon.ico -------------------------------------------------------------------------------- /src/app/api/hello/route.ts: -------------------------------------------------------------------------------- 1 | export async function GET(request: Request) { 2 | return new Response('Hello, Next.js!'); 3 | } 4 | -------------------------------------------------------------------------------- /public/images/a-apple.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saddamarbaa/twitter-clone-app-nextjs-typescript/HEAD/public/images/a-apple.png -------------------------------------------------------------------------------- /public/images/banner.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saddamarbaa/twitter-clone-app-nextjs-typescript/HEAD/public/images/banner.jpeg -------------------------------------------------------------------------------- /public/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saddamarbaa/twitter-clone-app-nextjs-typescript/HEAD/public/images/favicon.ico -------------------------------------------------------------------------------- /public/images/saddam.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saddamarbaa/twitter-clone-app-nextjs-typescript/HEAD/public/images/saddam.jpg -------------------------------------------------------------------------------- /public/images/G-google.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saddamarbaa/twitter-clone-app-nextjs-typescript/HEAD/public/images/G-google.png -------------------------------------------------------------------------------- /public/images/Twitter-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saddamarbaa/twitter-clone-app-nextjs-typescript/HEAD/public/images/Twitter-logo.png -------------------------------------------------------------------------------- /src/app/[...not-found]/page.tsx: -------------------------------------------------------------------------------- 1 | import { notFound } from 'next/navigation'; 2 | 3 | export default function page() { 4 | notFound(); 5 | } 6 | -------------------------------------------------------------------------------- /public/images/Twitter-logo.svg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saddamarbaa/twitter-clone-app-nextjs-typescript/HEAD/public/images/Twitter-logo.svg.png -------------------------------------------------------------------------------- /public/images/twitter-banner.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saddamarbaa/twitter-clone-app-nextjs-typescript/HEAD/public/images/twitter-banner.jpeg -------------------------------------------------------------------------------- /public/images/linkedin-banner-01.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saddamarbaa/twitter-clone-app-nextjs-typescript/HEAD/public/images/linkedin-banner-01.jpg -------------------------------------------------------------------------------- /src/utils/helpers.ts: -------------------------------------------------------------------------------- 1 | export function getRandomIntNumberBetween(min = 1, max = 10) { 2 | // min: 5, max: 10 3 | return Math.floor(Math.random() * (max - min + 1) + min); // 10.999999999999 => 10 4 | } -------------------------------------------------------------------------------- /src/app/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "jsxSingleQuote": true, 3 | "singleQuote": true, 4 | "semi": true, 5 | "tabWidth": 2, 6 | "trailingComma": "all", 7 | "printWidth": 120, 8 | "bracketSameLine": false, 9 | "bracketSpacing": true, 10 | "useTabs": false, 11 | "arrowParens": "always", 12 | "endOfLine": "auto" 13 | } 14 | -------------------------------------------------------------------------------- /src/styles/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer base { 6 | body { 7 | @apply min-h-screen; 8 | } 9 | 10 | html { 11 | scroll-behavior: smooth; 12 | } 13 | } 14 | 15 | @layer components { 16 | .test { 17 | @apply mb-5 w-full max-w-[305px] cursor-pointer; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/app/page.tsx: -------------------------------------------------------------------------------- 1 | import { Inter } from 'next/font/google'; 2 | import TweetInput from '../components/TweetInput'; 3 | import Tweets from '../components/Tweets'; 4 | 5 | const inter = Inter({ subsets: ['latin'] }); 6 | 7 | export default function Home() { 8 | return ( 9 | <> 10 | 11 | 12 | 13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | experimental: { 4 | appDir: true, 5 | }, 6 | images: { 7 | domains: [ 8 | 'lh3.googleusercontent.com', 9 | 'avatars.githubusercontent.com', 10 | 'githubusercontent.com', 11 | 'githubusercontent.com', 12 | 'pbs.twimg.com', 13 | 'media.licdn.com', 14 | ], 15 | }, 16 | } 17 | 18 | module.exports = nextConfig 19 | -------------------------------------------------------------------------------- /src/types/news.ts: -------------------------------------------------------------------------------- 1 | export interface ArticleT { 2 | source: { 3 | id: string | null; 4 | name: string; 5 | }; 6 | author: string; 7 | title: string; 8 | description: string; 9 | url: string; 10 | urlToImage: string; 11 | publishedAt: string; 12 | content: string; 13 | } 14 | 15 | export interface NewsApiResponse { 16 | status: string; 17 | totalResults: number; 18 | articles: ArticleT[]; 19 | } 20 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | /node_modules 3 | /.pnp 4 | .pnp.js 5 | 6 | # testing 7 | /coverage 8 | 9 | # next.js 10 | /.next/ 11 | /out/ 12 | 13 | # production 14 | /build 15 | 16 | # misc 17 | .DS_Store 18 | *.pem 19 | 20 | # debug 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | 25 | # local env files 26 | .env.local 27 | .env.development.local 28 | .env.test.local 29 | .env.production.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | .pnpm-debug.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/images/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/Comments.tsx: -------------------------------------------------------------------------------- 1 | import { CommentT } from '@/types/tweets' 2 | import React from 'react' 3 | import Comment from './Comment' 4 | 5 | type Props = { 6 | comments: CommentT[] 7 | } 8 | 9 | export default function Comments({ comments }: Props) { 10 | return ( 11 |
12 | {comments.map((comment, index) => ( 13 | 18 | ))} 19 |
20 | ) 21 | } 22 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib", 3 | "typescript.enablePromptUseWorkspaceTsdk": true, 4 | "colorize.languages": [ 5 | "css", 6 | "sass", 7 | "scss", 8 | "less", 9 | "postcss", 10 | "sss", 11 | "stylus", 12 | "xml", 13 | "svg", 14 | "json", 15 | "ts", 16 | "js", 17 | "tsx", 18 | "jsx" 19 | ], 20 | "editor.formatOnPaste": true, 21 | "editor.formatOnSave": true, 22 | "editor.defaultFormatter": "esbenp.prettier-vscode", 23 | "editor.codeActionsOnSave": { 24 | "source.fixAll.eslint": true, 25 | "source.fixAll.format": true 26 | }, 27 | "cSpell.words": ["ENDPOIN", "semibold"] 28 | } 29 | -------------------------------------------------------------------------------- /public/images/loading.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true, 17 | "plugins": [ 18 | { 19 | "name": "next" 20 | } 21 | ], 22 | "paths": { 23 | "@/*": ["./src/*"] 24 | } 25 | }, 26 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 27 | "exclude": ["node_modules"] 28 | } 29 | -------------------------------------------------------------------------------- /src/components/Search.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import React from 'react' 4 | import { useSession } from 'next-auth/react' 5 | import { AiOutlineSearch } from 'react-icons/ai' 6 | 7 | export default function Search() { 8 | const { data: session } = useSession() 9 | return session ? ( 10 |
11 | 12 | 17 |
18 | ) : ( 19 | null 20 | ) 21 | } 22 | -------------------------------------------------------------------------------- /src/components/Trend.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { IoIosMore } from 'react-icons/io'; 3 | 4 | type Props = { 5 | title: string; 6 | hasTag: string; 7 | tweets: string; 8 | }; 9 | 10 | export default function Trend({ tweets, hasTag, title }: Props) { 11 | return ( 12 |
13 |
14 | {title} 15 | 16 |
17 |

{hasTag}

18 | {tweets}Tweets 19 |
20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /src/components/ Article.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import Link from 'next/link'; 4 | import { ArticleT } from '../types/news'; 5 | 6 | type Props = { 7 | article: ArticleT; 8 | }; 9 | 10 | export default function Article({ article }: Props) { 11 | return ( 12 | 17 |
18 |
{article.title}
19 |

{article.source.name}

20 |
21 | 22 | 23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /src/types/randomUser.ts: -------------------------------------------------------------------------------- 1 | export interface RandomUserT { 2 | gender: string; 3 | name: { 4 | title: string; 5 | first: string; 6 | last: string; 7 | }; 8 | location: Record; 9 | email: string; 10 | login: { 11 | uuid: string; 12 | username: string; 13 | password: string; 14 | salt: string; 15 | md5: string; 16 | sha1: string; 17 | sha256: string; 18 | }; 19 | dob: { 20 | date: string; 21 | age: number; 22 | }; 23 | registered: { 24 | date: string; 25 | age: number; 26 | }; 27 | phone: string; 28 | cell: string; 29 | id: { 30 | name: string; 31 | value: string; 32 | }; 33 | picture: { 34 | large: string; 35 | medium: string; 36 | thumbnail: string; 37 | }; 38 | nat: string; 39 | } 40 | 41 | export interface RandomUserApiResponse { 42 | results: RandomUserT[]; 43 | info: { 44 | seed: string; 45 | results: number; 46 | page: number; 47 | version: string; 48 | }; 49 | } 50 | -------------------------------------------------------------------------------- /public/thirteen.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/images/thirteen.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/RandomUser.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Button from './Button'; 3 | import { RandomUserT } from '../types/randomUser'; 4 | 5 | type Props = { 6 | user: RandomUserT; 7 | }; 8 | 9 | export default function RandomUser({ user }: Props) { 10 | return ( 11 |
15 | {`${user.name}'s 16 |
17 |

{user.login.username}

18 |
19 | @{user.login.username} 20 | {/* {flowedMe && Follows you} */} 21 |
22 |
23 |
24 | 27 |
28 |
29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /src/components/Widget.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import WhoToFollow from './WhoToFollow' 4 | import News from './News' 5 | import { ArticleT } from '../types/news' 6 | import Trending from './Trending' 7 | import { RandomUserT } from '../types/randomUser' 8 | import Search from './Search' 9 | 10 | type Props = { 11 | initialNewsResult: ArticleT[] 12 | randomUsers: RandomUserT[] 13 | } 14 | 15 | export default function Widget({ initialNewsResult, randomUsers }: Props) { 16 | return ( 17 |
18 | {/* Search */} 19 | 20 | 21 | {/* Whats happening Session */} 22 | 23 | 24 |
25 | {/* Who to follow session */} 26 | 27 | 28 | {/* Trending session */} 29 | 30 | 31 |

32 | Terms of Service Privacy Policy Cookie Policy Accessibility Ads info 33 | More © 2023 Twitter, Inc. 34 |

35 |
36 |
37 | ) 38 | } 39 | -------------------------------------------------------------------------------- /src/app/not-found.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import Button from '@/components/Button'; 4 | import { Metadata } from 'next'; 5 | import { useRouter } from 'next/navigation'; 6 | import React from 'react'; 7 | 8 | export const metadata: Metadata = { 9 | title: 'Twitter Clone app | Page not found', 10 | description: 'Twitter Clone build with + Typescript.', 11 | }; 12 | 13 | export default function PageNotFound() { 14 | const router = useRouter(); 15 | const handleClick = () => { 16 | router.push('/'); 17 | }; 18 | 19 | return ( 20 |
21 |
22 |

404

23 |

Oops! Page not found

24 |

25 | We re sorry. The page you requested could not be found. Please go back to the homepage or contact us. 26 |

27 |
28 | 31 |
32 |
33 |
34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "twitter-clone-app", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@types/node": "18.15.11", 13 | "@types/react": "18.0.31", 14 | "@types/react-dom": "18.0.11", 15 | "@types/react-modal": "^3.13.1", 16 | "eslint": "8.40.0", 17 | "eslint-config-next": "^13.4.1", 18 | "firebase": "^9.21.0", 19 | "framer-motion": "^10.11.2", 20 | "moment": "^2.29.4", 21 | "next": "^13.4.1", 22 | "next-auth": "^4.22.1", 23 | "next-themes": "^0.2.1", 24 | "nodemailer": "^6.9.1", 25 | "react": "^18.2.0", 26 | "react-dom": "^18.2.0", 27 | "react-hot-toast": "^2.4.0", 28 | "react-icon": "^1.0.0", 29 | "react-icons": "^4.8.0", 30 | "react-modal": "^3.16.1", 31 | "react-twitter-embed": "^4.0.4", 32 | "typescript": "5.0.4", 33 | "uuid": "^9.0.0" 34 | }, 35 | "devDependencies": { 36 | "@types/uuid": "^9.0.1", 37 | "autoprefixer": "^10.4.14", 38 | "postcss": "^8.4.21", 39 | "prettier": "^2.8.7", 40 | "prettier-plugin-tailwindcss": "^0.2.6", 41 | "tailwindcss": "^3.2.7" 42 | }, 43 | "repository": "https://github.com/saddamarbaa/twitter-clone-app-nextjs-typescript", 44 | "author": "Saddam Arbaa" 45 | } 46 | -------------------------------------------------------------------------------- /src/components/Messages.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import React from 'react' 4 | import { MdKeyboardDoubleArrowUp, MdOutlineAttachEmail } from 'react-icons/md' 5 | import { useSession } from 'next-auth/react' 6 | 7 | export default function Messages() { 8 | const { data: session } = useSession() 9 | return null 10 | return session?( 11 |
14 |
15 |
18 | 23 | 24 | 25 |
26 |
27 |
28 | ):null 29 | } 30 | -------------------------------------------------------------------------------- /src/app/Providers.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { motion, AnimatePresence } from 'framer-motion'; 4 | import { ThemeProvider } from 'next-themes'; 5 | import React, { ReactNode } from 'react'; 6 | import { Toaster as ToastProvider } from 'react-hot-toast'; 7 | import { Session } from 'next-auth'; 8 | import { SessionProvider } from 'next-auth/react'; 9 | 10 | type Props = { 11 | children: ReactNode; 12 | session: Session | null; 13 | }; 14 | 15 | const pageVariants = { 16 | initial: { opacity: 0 }, 17 | enter: { opacity: 1, transition: { duration: 0.5 } }, 18 | exit: { opacity: 0, transition: { duration: 0.5 } }, 19 | }; 20 | 21 | export default function Providers({ children, session }: Props) { 22 | return ( 23 | 24 | 25 |
32 | 33 | {children} 34 |
35 |
36 |
37 | ); 38 | } 39 | -------------------------------------------------------------------------------- /public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/images/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/config/firebase.ts: -------------------------------------------------------------------------------- 1 | // Import the functions you need from the SDKs you need 2 | import { getApps, initializeApp } from 'firebase/app' 3 | import { 4 | createUserWithEmailAndPassword, 5 | getAuth, 6 | GoogleAuthProvider, 7 | sendPasswordResetEmail, 8 | signInWithEmailAndPassword, 9 | signInWithPopup, 10 | signOut, 11 | updateProfile, 12 | } from 'firebase/auth' 13 | import { getFirestore } from 'firebase/firestore' 14 | 15 | const googleProvider = new GoogleAuthProvider() 16 | 17 | const firebaseConfig = { 18 | apiKey: process.env.NEXT_PUBLIC_API_KEY, 19 | authDomain: process.env.NEXT_PUBLIC_AUTH_DOMAIN, 20 | databaseURL: process.env.NEXT_PUBLIC_DATABASE_URL, 21 | projectId: process.env.NEXT_PUBLIC_PROJECT_ID, 22 | storageBucket: process.env.NEXT_PUBLIC_STORAGE_BUCKET, 23 | messagingSenderId: process.env.NEXT_PUBLIC_MESSAIN_SENDER_ID, 24 | appId: process.env.NEXT_PUBLIC_APPID, 25 | } 26 | 27 | // Initialize Firebase 28 | const app = getApps().length > 0 ? getApps() : initializeApp(firebaseConfig) 29 | 30 | const auth = getAuth() 31 | const db = getFirestore() 32 | 33 | export { 34 | app, 35 | auth, 36 | createUserWithEmailAndPassword, 37 | db, 38 | getAuth, 39 | GoogleAuthProvider, 40 | googleProvider, 41 | sendPasswordResetEmail, 42 | signInWithEmailAndPassword, 43 | signInWithPopup, 44 | signOut, 45 | updateProfile, 46 | } 47 | -------------------------------------------------------------------------------- /src/components/Comment.tsx: -------------------------------------------------------------------------------- 1 | import { CommentT } from '@/types/tweets' 2 | import React from 'react' 3 | import { motion } from 'framer-motion' 4 | import Moment from 'moment' 5 | 6 | type Props = { 7 | comment: CommentT 8 | hideBorder: boolean 9 | } 10 | 11 | const itemVariants = { 12 | hidden: { opacity: 0, y: -10 }, 13 | visible: { opacity: 1, y: 0, transition: { duration: 0.1 } }, 14 | } 15 | 16 | export default function Comment({ comment, hideBorder }: Props) { 17 | return ( 18 |
21 | {!hideBorder && ( 22 |
23 | )} 24 | 25 | 33 |
34 |
35 |

{comment.user.name}

36 |

37 | @{comment.user.name?.toLowerCase()}· 38 |

39 | 40 | {comment.timestamp?.seconds && ( 41 | 42 | {Moment.unix(comment.timestamp.seconds).fromNow()} 43 | 44 | )} 45 |
46 |

{comment.text}

47 |
48 |
49 | ) 50 | } 51 | -------------------------------------------------------------------------------- /src/types/tweets.ts: -------------------------------------------------------------------------------- 1 | export interface TweetTemp { 2 | id: number 3 | user: { 4 | username: string 5 | name: string 6 | avatar: string 7 | } 8 | title: string 9 | content: string 10 | media: { 11 | type: 'image' | 'video' 12 | url: string 13 | }[] 14 | timestamp: string 15 | likes: { 16 | user: { 17 | username: string 18 | name: string 19 | avatar: string 20 | } 21 | timestamp: string 22 | }[] 23 | comments: { 24 | user: { 25 | username: string 26 | name: string 27 | avatar: string 28 | } 29 | content: string 30 | timestamp: string 31 | }[] 32 | retweets: { 33 | user: { 34 | username: string 35 | name: string 36 | avatar: string 37 | } 38 | timestamp: string 39 | }[] 40 | views: { 41 | user: { 42 | username: string 43 | name: string 44 | avatar: string 45 | } 46 | timestamp: string 47 | }[] 48 | shares: { 49 | user: { 50 | username: string 51 | name: string 52 | avatar: string 53 | } 54 | timestamp: string 55 | }[] 56 | } 57 | 58 | export interface UserT { 59 | email?: string 60 | name?: string 61 | image: string 62 | } 63 | 64 | export interface TimestampT { 65 | seconds: number 66 | nanoseconds: number 67 | } 68 | 69 | export interface CommentT { 70 | text: string 71 | user: UserT 72 | timestamp: TimestampT 73 | } 74 | 75 | export interface TweetT { 76 | id: string 77 | title: string 78 | user: UserT 79 | content: string 80 | timestamp: TimestampT 81 | userRef: string 82 | images: string[] 83 | likes?: UserT[] 84 | comments?: CommentT[] 85 | } 86 | -------------------------------------------------------------------------------- /src/components/DarkModSwitch.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import React, { useEffect, useState } from 'react'; 4 | import { MdLightMode } from 'react-icons/md'; 5 | import { BsFillMoonFill } from 'react-icons/bs'; 6 | import { useTheme } from 'next-themes'; 7 | 8 | export default function DarkModSwitch() { 9 | const { systemTheme, theme, setTheme } = useTheme(); 10 | const [mounted, setMounted] = useState(false); 11 | 12 | useEffect(() => { 13 | setMounted(true); 14 | }, []); 15 | 16 | const renderThemeChanger = () => { 17 | if (!mounted) return null; 18 | 19 | const currentTheme = theme === 'system' ? systemTheme : theme; 20 | 21 | if (currentTheme === 'dark') { 22 | return ( 23 |
setTheme('light' as const)} 26 | > 27 | 28 |

Light mode

29 |
30 | ); 31 | } 32 | 33 | return ( 34 |
setTheme('dark' as const)} 37 | > 38 | 39 |

Dark mode

40 |
41 | ); 42 | }; 43 | return
{renderThemeChanger()}
; 44 | } 45 | -------------------------------------------------------------------------------- /src/app/error.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import Button from '@/components/Button'; 4 | import { Metadata } from 'next'; 5 | import Link from 'next/link'; 6 | import React, { useEffect } from 'react'; 7 | 8 | interface Props { 9 | error: Error | null; 10 | rest: () => void; 11 | } 12 | 13 | export const metadata: Metadata = { 14 | title: 'Twitter Clone app | Page not found', 15 | description: 'Twitter Clone build with + Typescript.', 16 | }; 17 | 18 | export default function ErrorPage({ error, rest }: Props) { 19 | useEffect(() => { 20 | return () => {}; 21 | }, [error]); 22 | 23 | return ( 24 |
25 |
26 |

Oops!

27 |

{error?.message || 'Unknown error occurred.'}

28 |

Please try again later or contact our support team if the problem persists.

29 |
30 | 31 | 32 | 33 |
34 | 44 |
45 |
46 |
47 |
48 | ); 49 | } 50 | -------------------------------------------------------------------------------- /src/components/SideBarOption.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { IconType } from 'react-icons'; 4 | 5 | type Props = { 6 | title?: string; 7 | Icon: IconType; 8 | handleClick?: (isLogOut?:boolean) => void; 9 | isLogo?: boolean; 10 | notification?: number; 11 | }; 12 | 13 | export default function SideBarOption({ notification, title, handleClick, Icon, isLogo = false }: Props) { 14 | const handleClickOption = () => { 15 | if (handleClick) { 16 | if (title === 'Log out') {handleClick(true); } 17 | else { 18 | handleClick(); 19 | } 20 | } 21 | }; 22 | 23 | return ( 24 |
30 |
31 | 32 | {notification ? ( 33 | 38 | {notification} 39 | 40 | ) : null} 41 | 42 | {title === 'Home' ? ( 43 | 46 | {notification} 47 | 48 | ) : null} 49 |
50 | {title} 51 |
52 | ); 53 | } 54 | -------------------------------------------------------------------------------- /src/components/Trending.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable @next/next/no-img-element */ 2 | 'use client'; 3 | import { AnimatePresence, motion } from 'framer-motion'; 4 | import React, { useState } from 'react'; 5 | 6 | import Button from './Button'; 7 | import { mockedTrending } from '../utils/mockedData'; 8 | import Trend from './Trend'; 9 | 10 | export default function Trending() { 11 | const [number, setNumber] = useState(2); 12 | 13 | return mockedTrending?.length ? ( 14 |
15 |
16 |

Trends for you

17 | 18 | {mockedTrending.slice(0, number).map((item) => ( 19 | 26 | 27 | 28 | ))} 29 | 30 |
31 | 39 |
40 | ) : ( 41 |
42 | ); 43 | } 44 | -------------------------------------------------------------------------------- /src/components/News.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { AnimatePresence, motion } from 'framer-motion'; 4 | import React, { useState } from 'react'; 5 | import { ArticleT } from '../types/news'; 6 | import Article from './ Article'; 7 | import Button from './Button'; 8 | 9 | type Props = { 10 | initialResult: ArticleT[]; 11 | }; 12 | 13 | export default function News({ initialResult }: Props) { 14 | const [articlesNumber, setArticlesNumber] = useState(3); 15 | 16 | return initialResult?.length ? ( 17 |
18 |
19 |

Whats happening

20 | 21 | 22 | {initialResult.slice(0, articlesNumber).map((article) => ( 23 | 30 |
31 | 32 | ))} 33 | 34 |
35 | 36 | 44 |
45 | ) : ( 46 |
47 | ); 48 | } 49 | -------------------------------------------------------------------------------- /src/components/Auth.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import React from 'react' 4 | import Link from 'next/link' 5 | import Button from './Button' 6 | import { useSession } from 'next-auth/react' 7 | 8 | export default function Auth() { 9 | const { data: session } = useSession() 10 | 11 | if (session) { 12 | return null 13 | } else { 14 | return ( 15 |
16 |
17 |
18 |
19 |

Don’t miss what’s happening

20 |

People on Twitter are the first to know.

21 |
22 |
23 |
24 | 25 | 30 | 31 | 32 | 37 | 38 |
39 |
40 |
41 |
42 | ) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | # or 12 | pnpm dev 13 | ``` 14 | 15 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 16 | 17 | You can start editing the page by modifying `pages/index.tsx`. The page auto-updates as you edit the file. 18 | 19 | [API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.ts`. 20 | 21 | The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages. 22 | 23 | This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. 24 | 25 | ## Learn More 26 | 27 | To learn more about Next.js, take a look at the following resources: 28 | 29 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 30 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 31 | 32 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 33 | 34 | ## Deploy on Vercel 35 | 36 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 37 | 38 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 39 | -------------------------------------------------------------------------------- /src/components/WhoToFollow.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable @next/next/no-img-element */ 2 | 'use client' 3 | 4 | import React, { useState } from 'react' 5 | import { useSession } from 'next-auth/react' 6 | import Button from './Button' 7 | import RandomUser from './RandomUser' 8 | import { RandomUserT } from '../types/randomUser' 9 | import { AnimatePresence, motion } from 'framer-motion' 10 | 11 | type Props = { 12 | initialResult: RandomUserT[] 13 | } 14 | 15 | export default function WhoToFollow({ initialResult }: Props) { 16 | const [usersNumber, setUsersNumber] = useState(3) 17 | const { data: session } = useSession() 18 | 19 | return initialResult?.length && session ? ( 20 |
21 |
22 |

23 | Who to follow 24 |

25 | 26 | 27 | {initialResult.slice(0, usersNumber).map((user) => ( 28 | 34 | 35 | 36 | ))} 37 | 38 |
39 | 40 | 49 |
50 | ) : ( 51 |
52 | ) 53 | } 54 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: [ 4 | './app/**/*.{js,ts,jsx,tsx}', 5 | './pages/**/*.{js,ts,jsx,tsx}', 6 | './components/**/*.{js,ts,jsx,tsx}', 7 | 8 | // Or if using `src` directory: 9 | './src/**/*.{js,ts,jsx,tsx}', 10 | ], 11 | theme: { 12 | extend: { 13 | colors: { 14 | twitterBlue: { 15 | 50: '#F2F9FE', 16 | 100: '#D7EEFB', 17 | 200: '#AED2F6', 18 | 300: '#85B5F1', 19 | 400: '#5897EC', 20 | 500: '#1DA1F2', 21 | 600: '#1678BD', 22 | 700: '#10508A', 23 | 800: '#0A294F', 24 | 900: '#010101', 25 | }, 26 | 27 | twitterGray: { 28 | 50: '#F7FAFC', 29 | 100: '#EDF2F7', 30 | 200: '#E2E8F0', 31 | 300: '#CBD5E0', 32 | 400: '#A0AEC0', 33 | 500: '#718096', 34 | 600: '#4A5568', 35 | 700: '#2D3748', 36 | 800: '#1A202C', 37 | 900: '#14171A', 38 | 999: '#34373B', 39 | }, 40 | twitterBlack: '#14171A', 41 | twitterWhite: '#F5F8FA', 42 | }, 43 | screens: { 44 | ss: '300px', 45 | xs: '320px', 46 | sm: '640px', 47 | md: '768px', 48 | lg: '1024px', 49 | xl: '1280px', 50 | }, 51 | }, 52 | maxWidth: ({ theme, breakpoints }) => ({ 53 | none: 'none', 54 | 0: '0rem', 55 | ss: '19rem', 56 | xs: '20rem', 57 | sm: '24rem', 58 | md: '28rem', 59 | lg: '32rem', 60 | xl: '36rem', 61 | '2xl': '42rem', 62 | '3xl': '48rem', 63 | '4xl': '56rem', 64 | '5xl': '64rem', 65 | '6xl': '72rem', 66 | '7xl': '80rem', 67 | '8xl': '90rem', 68 | '9xl': '95rem', 69 | '10xl': '100rem', 70 | full: '100%', 71 | min: 'min-content', 72 | max: 'max-content', 73 | fit: 'fit-content', 74 | prose: '65ch', 75 | ...breakpoints(theme('screens')), 76 | }), 77 | }, 78 | variants: { 79 | lineClamp: ['responsive'], 80 | }, 81 | plugins: [], 82 | mode: 'jit', 83 | darkMode: 'class', 84 | } 85 | -------------------------------------------------------------------------------- /src/pages/api/auth/[...nextauth].tsx: -------------------------------------------------------------------------------- 1 | import NextAuth, { NextAuthOptions } from 'next-auth' 2 | import GoogleProvider from 'next-auth/providers/google' 3 | import FacebookProvider from 'next-auth/providers/facebook' 4 | import GithubProvider from 'next-auth/providers/github' 5 | import TwitterProvider from 'next-auth/providers/twitter' 6 | import Auth0Provider from 'next-auth/providers/auth0' 7 | import AppleProvider from 'next-auth/providers/apple' 8 | import EmailProvider from 'next-auth/providers/email' 9 | import LinkedInProvider from "next-auth/providers/linkedin"; 10 | 11 | export const authOptions: NextAuthOptions = { 12 | providers: [ 13 | // EmailProvider({ 14 | // server: { 15 | // host: process.env.EMAIL_SERVER_HOST, 16 | // port: process.env.EMAIL_SERVER_PORT, 17 | // auth: { 18 | // user: process.env.EMAIL_SERVER_USER, 19 | // pass: process.env.EMAIL_SERVER_PASSWORD, 20 | // }, 21 | // }, 22 | // from: process.env.EMAIL_FROM, 23 | // }), 24 | // AppleProvider({ 25 | // clientId: process.env.APPLE_ID!, 26 | // clientSecret: { 27 | // appleId: process.env.APPLE_ID!, 28 | // teamId: process.env.APPLE_TEAM_ID!, 29 | // privateKey: process.env.APPLE_PRIVATE_KEY!, 30 | // keyId: process.env.APPLE_KEY_ID!, 31 | // }, 32 | // }), 33 | 34 | // FacebookProvider({ 35 | // clientId: process.env.FACEBOOK_ID!, 36 | // clientSecret: process.env.FACEBOOK_SECRET!, 37 | // }), 38 | GithubProvider({ 39 | clientId: process.env.GITHUB_ID!, 40 | clientSecret: process.env.GITHUB_SECRET!, 41 | }), 42 | GoogleProvider({ 43 | clientId: process.env.GOOGLE_ID!, 44 | clientSecret: process.env.GOOGLE_SECRET!, 45 | }), 46 | TwitterProvider({ 47 | clientId: process.env.TWITTER_ID!, 48 | clientSecret: process.env.TWITTER_SECRET!, 49 | }), 50 | LinkedInProvider({ 51 | clientId: process.env.LINKEDIN_ID!, 52 | clientSecret: process.env.LINKEDIN_SECRET! 53 | }) 54 | // Auth0Provider({ 55 | // clientId: process.env.AUTH0_ID!, 56 | // clientSecret: process.env.AUTH0_SECRET!, 57 | // issuer: process.env.AUTH0_ISSUER, 58 | // }), 59 | ], 60 | theme: { 61 | colorScheme: 'dark', 62 | }, 63 | callbacks: { 64 | async jwt({ token }) { 65 | token.userRole = 'admin' 66 | return token 67 | }, 68 | }, 69 | } 70 | 71 | export default NextAuth(authOptions) 72 | 73 | -------------------------------------------------------------------------------- /src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import { getServerSession } from 'next-auth'; 2 | 3 | import '../styles/globals.css'; 4 | import Auth from '../components/Auth'; 5 | import Sidebar from '../components/Sidebar'; 6 | import Widget from '../components/Widget'; 7 | import Providers from './Providers'; 8 | import Messages from '../components/Messages'; 9 | import { ArticleT, NewsApiResponse } from '../types/news'; 10 | import { RandomUserT, RandomUserApiResponse } from '../types/randomUser'; 11 | import { authOptions } from '@/pages/api/auth/[...nextauth]'; 12 | import { Metadata } from 'next'; 13 | 14 | const metadata: Metadata = { 15 | title: 'Twitter Clone app', 16 | description: 'Twitter Clone build with + Typescript .', 17 | }; 18 | 19 | export default async function RootLayout({ children }: { children: React.ReactNode }) { 20 | // https:saurav.tech/NewsAPI/top-headlines/category/health/in.json 21 | const newsUrl = 'https://saurav.tech/NewsAPI/top-headlines/category/business/us.json'; 22 | const randomUserUrl = 'https://randomuser.me/api/?results=30&inc=name,login,picture'; 23 | const res = await fetch(newsUrl, { next: { revalidate: 10000 } }); 24 | if (!res.ok) { 25 | throw new Error('Failed to fetch data'); 26 | } 27 | const data: NewsApiResponse = await res.json(); 28 | const articles = data.articles || ([] as ArticleT[]); 29 | 30 | const randomUserResponse = await fetch(randomUserUrl, { next: { revalidate: 10000 } }); 31 | const randomUserResult: RandomUserApiResponse = await randomUserResponse.json(); 32 | const randomUsers = randomUserResult.results || ([] as RandomUserT[]); 33 | 34 | const session = await getServerSession(authOptions); 35 | 36 | console.log(' session', session); 37 | 38 | return ( 39 | 40 | 41 | 42 |
43 | {/* Sidebar */} 44 | 45 | 46 |
47 | {/* */} 48 | 49 | {children} 50 |
51 | 52 | {/* */} 53 | 54 |
55 | 56 | 57 |
58 | 59 | 60 | ); 61 | } 62 | -------------------------------------------------------------------------------- /src/components/Button.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { motion, MotionProps } from 'framer-motion' 4 | import Image from 'next/image' 5 | import { ReactNode } from 'react' 6 | import { IconType } from 'react-icons' 7 | 8 | type Props = MotionProps & { 9 | color?: string 10 | onClick?: () => void 11 | size?: 'small' | 'medium' | 'large' 12 | isLoading?: boolean 13 | isDisabled?: boolean 14 | children: ReactNode 15 | buttonClassName?: string 16 | type?: 'submit' | 'button' | 'reset' 17 | id?: string 18 | preStyled?: string 19 | Icon?: IconType 20 | } 21 | 22 | export default function Button({ 23 | color = 'twitterBlue', 24 | onClick, 25 | size = 'medium', 26 | children, 27 | isLoading = false, 28 | isDisabled = false, 29 | buttonClassName = 'text-white uppercase', 30 | type = 'submit', 31 | id, 32 | preStyled, 33 | Icon, 34 | ...rest 35 | }: Props) { 36 | const sizes = { 37 | small: 'py-1 px-2 text-sm', 38 | medium: 'py-2 px-5 text-md', 39 | large: 'py-3 px-6 text-lg', 40 | } 41 | 42 | type Colors = { 43 | [key: string]: string 44 | } 45 | 46 | const colors: Colors = { 47 | blue: 'bg-blue-500 hover:bg-blue-700 active:bg-blue-800', 48 | red: 'bg-red-500 hover:bg-red-700 text-white', 49 | green: 'bg-green-500 hover:bg-green-700 active:bg-green-800', 50 | black: 'bg-black', 51 | slate: 'bg-slate-600 hover:bg-slate-700 active:bg-slate-800', 52 | white: 'bg-white border border-gray-300', 53 | gray: 'bg-gray-100 hover:bg-gray-200 active:bg-gray-300', 54 | twitterBlue: 55 | 'bg-twitterBlue-500 hover:bg-twitterBlue-700 active:bg-twitterBlue-800', 56 | } 57 | 58 | // add disabled and loading states to button 59 | const disabledClass = isDisabled ? 'opacity-50 cursor-not-allowed' : '' 60 | const loadingClass = isLoading ? 'animate-pulse' : '' 61 | 62 | return ( 63 | 80 | {Icon ? : null} 81 | 82 | {isLoading ? ( 83 |
84 | Loading 90 |
91 | ) : ( 92 | children 93 | )} 94 |
95 | ) 96 | } 97 | -------------------------------------------------------------------------------- /src/app/[profile]/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import React, { useEffect, useState } from 'react'; 4 | import { useSession } from 'next-auth/react'; 5 | import { useRouter } from 'next/navigation'; 6 | import { Metadata } from 'next'; 7 | import Tweets from '@/components/Tweets'; 8 | import Image from 'next/image'; 9 | import { IoArrowBackSharp } from 'react-icons/io5'; 10 | import Link from 'next/link'; 11 | 12 | const metadata: Metadata = { 13 | title: 'Twitter Profile', 14 | description: 'Profile page', 15 | }; 16 | 17 | export default function Profile() { 18 | const router = useRouter(); 19 | const { data: session, status } = useSession(); 20 | const adminReference = process.env.NEXT_PUBLIC_ADMIN_REF; 21 | 22 | useEffect(() => { 23 | if (status !== 'loading' && !session) { 24 | router.push('/sign-up'); 25 | } 26 | }, [session, router, status]); 27 | 28 | if (status === 'loading') { 29 | return ( 30 |
31 |

Initializing User...

32 |
33 | ); 34 | } 35 | 36 | const profileImage = '/images/twitter-banner.jpeg'; 37 | 38 | return ( 39 | <> 40 |
41 |
42 |
43 | Profile image 50 |
51 | 52 | 53 | 54 |

{session?.user?.name}

55 |
56 |
57 |
58 | Profile image 65 |
66 |
67 |

68 | {session?.user?.name} 69 |

70 |

@{session?.user?.name}

71 |

Forever a learner ☀ always growing ☀ Opinions are my own

72 |
73 |
74 |
75 |
76 | 77 | 78 | ); 79 | } 80 | -------------------------------------------------------------------------------- /public/images/svg/Spinner-1s-200px.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /src/components/Sidebar.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import React from 'react' 4 | import { useRouter } from 'next/navigation' 5 | import { FaTwitter, FaHome, FaCog, FaUserPlus } from 'react-icons/fa' 6 | import { MdNotificationsActive } from 'react-icons/md' 7 | import { HiDotsCircleHorizontal } from 'react-icons/hi' 8 | import { IoBookmarkSharp } from 'react-icons/io5' 9 | import { BiHash } from 'react-icons/bi' 10 | import { MdLogout } from 'react-icons/md' 11 | import { TiBook } from 'react-icons/ti' 12 | import { IoIosMore } from 'react-icons/io' 13 | 14 | import { signOut, useSession } from 'next-auth/react' 15 | import Image from 'next/image' 16 | import Link from 'next/link' 17 | 18 | import DarkModSwitch from './DarkModSwitch' 19 | import SideBarOption from './SideBarOption' 20 | import Button from './Button' 21 | 22 | export default function Sidebar() { 23 | const router = useRouter() 24 | const { data: session } = useSession() 25 | 26 | const handleSignOut = async () => { 27 | const data = await signOut({ 28 | redirect: false, 29 | // callbackUrl: '/some' 30 | }) 31 | // push(data.url) 32 | console.log(data.url) 33 | } 34 | 35 | const handleClick = (isLogOut?: boolean) => { 36 | if (!session) { 37 | console.log(session) 38 | router.push(`/sign-in`) 39 | } else if (session && isLogOut) { 40 | handleSignOut() 41 | } 42 | } 43 | 44 | return ( 45 |
46 |
49 |
50 | 51 | 52 | 53 | 54 | 59 | 60 | 65 | 71 | 77 | 82 | 87 | {session ? ( 88 | { 92 | const email = session?.user?.email || session?.user?.name 93 | const username = 94 | (email && email.substring(0, email.indexOf('@'))) || 95 | session?.user?.name 96 | router.push(`/${username}`) 97 | }} 98 | /> 99 | ) : null} 100 | 101 | 106 | {session ? ( 107 | 112 | ) : null} 113 | 118 | {session ? ( 119 |
120 | 123 |
124 | ) : null} 125 |
{' '} 126 | {/* Add a placeholder for the profile section */} 127 |
128 | 129 | {session ? ( 130 |
133 | profile image 140 | 141 |
142 |
143 |

{session?.user?.name}

144 | 145 |
146 |

147 | @{session?.user?.name} 148 |

149 |
150 |
151 | ) : null} 152 |
153 |
154 | ) 155 | } 156 | -------------------------------------------------------------------------------- /src/components/TweetTemp.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable @next/next/no-img-element */ 2 | import React from 'react'; 3 | import { FaHeart, FaComment, FaRetweet, FaEye, FaShare } from 'react-icons/fa'; 4 | import { motion } from 'framer-motion'; 5 | import { TweetT, TweetTemp } from '../types/tweets'; 6 | 7 | type Props = { 8 | tweet: TweetTemp; 9 | }; 10 | 11 | export default function TweetTemp({ tweet }: Props) { 12 | const { user, content, media, timestamp, likes, comments, retweets, views, shares } = tweet; 13 | 14 | const images = media && media.filter((item) => item.type === 'image'); 15 | const videos = media && media.filter((item) => item.type === 'video'); 16 | 17 | const itemVariants = { 18 | hidden: { opacity: 0, y: -10 }, 19 | visible: { opacity: 1, y: 0, transition: { duration: 0.3 } }, 20 | }; 21 | 22 | return ( 23 | 30 |
31 | 39 |
40 |
41 | 42 | {user.name} 43 | 44 | @{user.username} 45 | · 46 | {timestamp} 47 |
48 |

{content}

49 | 50 | 51 |
52 | {images && ( 53 |
1 ? 'grid-cols-2 gap-4' : 'grid-cols-1'}`}> 54 | {images.map((item, index) => ( 55 | item.type === 'image').length === 1 ? 'h-full w-full' : 'h-40 w-full' 64 | } transform cursor-pointer rounded-md object-cover transition duration-300 ease-in-out hover:scale-105`} 65 | /> 66 | ))} 67 |
68 | )} 69 | 70 | {videos && ( 71 |
1 ? 'grid-cols-2 gap-4' : 'grid-cols-1'} `}> 72 | {videos.map((item, index) => ( 73 |
77 | 87 |
88 | ))} 89 |
90 | )} 91 |
92 |
93 |
94 | 95 | {likes.length} 96 |
97 |
98 | 99 | {comments.length} 100 |
101 |
102 | 103 | {retweets.length} 104 |
105 |
106 | 107 | {views.length} 108 |
109 |
110 | 111 | {shares.length} 112 |
113 |
114 |
115 |
116 |
117 | ); 118 | } 119 | -------------------------------------------------------------------------------- /src/app/sign-up/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import React, { useEffect, useState } from 'react'; 4 | import Modal from 'react-modal'; 5 | import { signIn, signOut, useSession } from 'next-auth/react'; 6 | import Link from 'next/link'; 7 | import { useRouter } from 'next/navigation'; 8 | import { AiFillApple } from 'react-icons/ai'; 9 | import { FcGoogle } from 'react-icons/fc'; 10 | import Button from '@/components/Button'; 11 | import Image from 'next/image'; 12 | import { FaGithub, FaTwitter } from 'react-icons/fa'; 13 | import { GrLinkedin } from 'react-icons/gr'; 14 | import TweetInput from '@/components/TweetInput'; 15 | import { Metadata } from 'next'; 16 | 17 | const metadata: Metadata = { 18 | title: 'Sign Up to Twitter', 19 | description: 'Twitter Clone build with + Typescript .', 20 | }; 21 | 22 | export default function SignUp() { 23 | const router = useRouter(); 24 | const [isOpen, setIsOpen] = useState(false); 25 | const { data: session, status } = useSession(); 26 | 27 | useEffect(() => { 28 | if (session) { 29 | router.push('/'); 30 | } 31 | if (status !== 'loading' && !session) { 32 | setIsOpen(true); 33 | } 34 | }, [session, router, status]); 35 | 36 | const switchToLogin = () => { 37 | // setIsOpen(false); 38 | router.push('/'); 39 | }; 40 | 41 | const handleClick = () => { 42 | // Validate user 43 | // make api call 44 | 45 | // on success 46 | router.push('/'); 47 | }; 48 | 49 | if (status === 'loading') { 50 | return ( 51 |
52 |

Initializing User...

53 |
54 | ); 55 | } 56 | 57 | return ( 58 | <> 59 | 65 |
66 |
67 | 70 |
71 | Twitter-Logo 78 |
79 |
80 |
81 |

Join Twitter today

82 | 91 | 92 | 101 | 102 | 111 | 112 | 121 | 122 | 131 | 132 |
133 |

134 | By signing up, you agree to the 135 | 136 | Terms of Service 137 | 138 |

139 |

140 | and 141 | Privacy Policy 142 | , including 143 | Cookie Use. 144 |

145 |
146 | 147 |
148 | Have an account already?{' '} 149 | 153 | Login 154 | 155 |
156 |
157 |
158 |
159 | 160 | 161 | ); 162 | } 163 | -------------------------------------------------------------------------------- /src/components/Tweets.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import React, { useEffect, useState } from 'react' 4 | import { AnimatePresence, motion } from 'framer-motion' 5 | import { useSession } from 'next-auth/react' 6 | import Tweet from './Tweet' 7 | import { db } from '@/config/firebase' 8 | import { 9 | collection, 10 | doc, 11 | getDoc, 12 | getDocs, 13 | limit, 14 | onSnapshot, 15 | orderBy, 16 | query, 17 | where, 18 | } from 'firebase/firestore' 19 | import { toast } from 'react-hot-toast' 20 | import { TweetT } from '@/types/tweets' 21 | import Image from 'next/image' 22 | 23 | const adminRef = process.env.NEXT_PUBLIC_ADMIN_REF 24 | 25 | export default function Tweets({ 26 | fetchUser, 27 | isAdminRef = true, 28 | }: { 29 | fetchUser?: boolean 30 | isAdminRef?: boolean 31 | }) { 32 | const [tweets, setTweets] = useState([]) 33 | const [loading, setLoading] = useState(true) 34 | const { data: session, status } = useSession() 35 | const [adminTweets, setAdminTweets] = useState([]) 36 | 37 | useEffect(() => { 38 | if ( 39 | (!fetchUser && isAdminRef) || 40 | (isAdminRef && fetchUser && session?.user?.email === adminRef) 41 | ) { 42 | setLoading(true) 43 | toast.loading('Loading tweets') 44 | const tweetsRef = collection(db, 'tweets') 45 | const unsubscribe = onSnapshot( 46 | query( 47 | tweetsRef, 48 | where('userRef', '==', adminRef), 49 | orderBy('timestamp', 'desc'), 50 | ), 51 | (querySnapshot) => { 52 | const fetchedTweets = querySnapshot.docs.map((doc) => ({ 53 | id: doc.id, 54 | ...doc.data(), 55 | })) as TweetT[] 56 | setAdminTweets(fetchedTweets) 57 | setLoading(false) 58 | toast.dismiss() 59 | toast.success('Admin tweets fetched successfully!') 60 | }, 61 | (error) => { 62 | console.error('Error fetching admin tweets:', error) 63 | setLoading(false) 64 | toast.dismiss() 65 | toast.error('Failed to fetch admin tweets.') 66 | }, 67 | ) 68 | return unsubscribe 69 | } 70 | }, [isAdminRef, fetchUser, session]) 71 | 72 | useEffect(() => { 73 | const fetchUserTweets = async () => { 74 | try { 75 | toast.dismiss() 76 | setLoading(true) 77 | toast.loading('Loading tweets') 78 | const userRef = session?.user?.email || session?.user?.name 79 | const tweetsRef = collection(db, 'tweets') 80 | const q = query( 81 | tweetsRef, 82 | where('userRef', '==', userRef), 83 | orderBy('timestamp', 'desc'), 84 | ) 85 | const querySnap = await getDocs(q) 86 | const fetchedTweets = querySnap.docs.map((doc) => { 87 | return { 88 | id: doc.id, 89 | ...doc.data(), 90 | } 91 | }) as TweetT[] 92 | setTweets(fetchedTweets) 93 | setLoading(false) 94 | toast.dismiss() 95 | toast.success('Tweets fetched successfully!') 96 | } catch (error) { 97 | console.error('Error fetching user tweets:', error) 98 | setLoading(false) 99 | toast.dismiss() 100 | toast.error('Failed to fetch user tweets.') 101 | } 102 | } 103 | if (fetchUser && session?.user?.email !== adminRef) { 104 | fetchUserTweets() 105 | } else if (!fetchUser) { 106 | toast.dismiss() 107 | setLoading(true) 108 | toast.loading('Loading tweets') 109 | const unsubscribe = onSnapshot( 110 | query( 111 | collection(db, 'tweets'), 112 | where('userRef', '!=', adminRef), 113 | orderBy('userRef'), 114 | orderBy('timestamp', 'desc'), 115 | limit(2), 116 | ), 117 | (querySnapshot) => { 118 | const fetchedTweets = querySnapshot.docs.map((doc) => ({ 119 | id: doc.id, 120 | ...doc.data(), 121 | })) as TweetT[] 122 | setTweets(fetchedTweets) 123 | setLoading(false) 124 | toast.dismiss() 125 | toast.success('Tweets fetched successfully!') 126 | }, 127 | (error) => { 128 | console.error('Error fetching tweets:', error) 129 | setLoading(false) 130 | toast.dismiss() 131 | toast.error('Failed to fetch tweets.') 132 | }, 133 | ) 134 | return unsubscribe 135 | } 136 | }, [fetchUser, session?.user?.email, session?.user?.name]) 137 | 138 | if (loading && !adminTweets?.length && !tweets?.length) { 139 | return ( 140 |
141 | Loading... 148 | {/*

Loading...

*/} 149 |
150 | ) 151 | } 152 | 153 | if (!loading && !adminTweets?.length && !tweets?.length) { 154 | return ( 155 |
156 |

157 | No tweets found 158 |

159 |
160 | ) 161 | } 162 | 163 | return ( 164 |
165 | 166 | {(tweets?.length > 0 || adminTweets?.length > 0) && ( 167 | <> 168 | {tweets 169 | .filter( 170 | (tweet) => tweet?.userRef !== process.env.NEXT_PUBLIC_ADMIN_REF, 171 | ) 172 | .map((tweet) => ( 173 | 178 | 179 | 180 | ))} 181 | {isAdminRef && adminTweets?.length > 0 && ( 182 | <> 183 | {/*

Admin tweets

*/} 184 | {adminTweets.map((tweet) => ( 185 | 190 | 191 | 192 | ))} 193 | 194 | )} 195 | 196 | )} 197 |
198 |
199 | ) 200 | } 201 | -------------------------------------------------------------------------------- /src/app/sign-in/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import React, { useEffect, useState } from 'react'; 4 | import Modal from 'react-modal'; 5 | import { signIn, signOut, useSession } from 'next-auth/react'; 6 | import Link from 'next/link'; 7 | import { useRouter } from 'next/navigation'; 8 | import { AiFillApple } from 'react-icons/ai'; 9 | import { FcGoogle } from 'react-icons/fc'; 10 | import Button from '@/components/Button'; 11 | import Image from 'next/image'; 12 | import { FaGithub, FaTwitter } from 'react-icons/fa'; 13 | import { GrLinkedin } from 'react-icons/gr'; 14 | import TweetInput from '@/components/TweetInput'; 15 | import { Metadata } from 'next'; 16 | 17 | const metadata: Metadata = { 18 | title: 'Sign in to Twitter', 19 | description: 'Twitter Clone build with + Typescript .', 20 | }; 21 | 22 | export default function SignIn() { 23 | const router = useRouter(); 24 | const [isOpen, setIsOpen] = useState(false); 25 | const { data: session, status } = useSession(); 26 | const [email, setEmail] = useState(''); 27 | 28 | useEffect(() => { 29 | if (session) { 30 | router.push('/'); 31 | } 32 | if (status !== 'loading' && !session) { 33 | setIsOpen(true); 34 | } 35 | }, [session, router, status]); 36 | 37 | const switchToLogin = () => { 38 | // setIsOpen(false); 39 | router.push('/'); 40 | }; 41 | 42 | const handleClick = () => { 43 | // Validate user 44 | // make api call 45 | 46 | // on success 47 | router.push('/'); 48 | }; 49 | 50 | if (status === 'loading') { 51 | return ( 52 |
53 |

Initializing User...

54 |
55 | ); 56 | } 57 | 58 | const handleSubmit = () => { 59 | if (!email) return false; 60 | signIn('email', { email, redirect: false }); 61 | }; 62 | 63 | return ( 64 | <> 65 | 71 |
72 |
73 | 76 |
77 | Twitter-Logo 84 |
85 |
86 |
87 |

Sign in to Twitter

88 | 97 | 98 | 107 | 108 | 117 | 118 | 127 | 136 | 137 |
138 |
139 |
140 |
or
141 |
142 |
143 |
144 | 145 |
146 | setEmail(e.target.value)} 152 | placeholder='Email' 153 | /> 154 |
155 | 156 | 164 | 165 | 168 | 169 |
170 |

171 | By signing up, you agree to the 172 | 173 | Terms of Service 174 | 175 |

176 |

177 | and 178 | Privacy Policy 179 | , including 180 | Cookie Use. 181 |

182 |
183 | 184 |
185 | Dont have an account?{' '} 186 | 190 | Sign up 191 | 192 |
193 |
194 |
195 |
196 | 197 | 198 | ); 199 | } 200 | -------------------------------------------------------------------------------- /src/components/TweetInput.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { addDoc, collection, serverTimestamp } from 'firebase/firestore' 4 | import { 5 | getDownloadURL, 6 | getStorage, 7 | ref, 8 | uploadBytes, 9 | uploadBytesResumable, 10 | } from 'firebase/storage' 11 | import { v4 as uuidv4 } from 'uuid' 12 | import Link from 'next/link' 13 | import React, { useRef, useState } from 'react' 14 | import Button from './Button' 15 | import { MdOutlineLocalSee } from 'react-icons/md' 16 | import { AiOutlineFileGif, AiOutlineSchedule } from 'react-icons/ai' 17 | import { BsEmojiNeutral } from 'react-icons/bs' 18 | import { useSession } from 'next-auth/react' 19 | import { RiMapPinLine } from 'react-icons/ri' 20 | import { BiPoll } from 'react-icons/bi' 21 | import { AiFillSetting } from 'react-icons/ai' 22 | import Image from 'next/image' 23 | import { toast } from 'react-hot-toast' 24 | 25 | import { db } from '@/config/firebase' 26 | import { FaTrash } from 'react-icons/fa' 27 | 28 | export default function TweetInput() { 29 | const { data: session } = useSession() 30 | 31 | const [tweet, setTweet] = useState('') 32 | const fileInputRef = useRef(null) 33 | const [imageFiles, setImageFiles] = useState([]) 34 | const [imagePreviews, setImagePreviews] = useState([]) 35 | const [loading, setLoading] = useState(false) 36 | 37 | const removeImage = (index: number) => { 38 | const newFiles = [...imageFiles] 39 | const newPreviews = [...imagePreviews] 40 | newFiles.splice(index, 1) 41 | newPreviews.splice(index, 1) 42 | setImageFiles(newFiles) 43 | setImagePreviews(newPreviews) 44 | } 45 | 46 | const handleImageChange = (e: React.ChangeEvent) => { 47 | const { files } = e.target 48 | if (files) { 49 | // @ts-ignore 50 | setImageFiles([...files]) 51 | const fileArray = Array.from(files) 52 | const previewArray = fileArray.map((file) => URL.createObjectURL(file)) 53 | setImagePreviews((prev) => [...prev, ...previewArray]) 54 | } 55 | } 56 | 57 | async function storeImage(image: File): Promise { 58 | return new Promise((resolve, reject) => { 59 | const storage = getStorage() 60 | const filename = `${uuidv4()}` 61 | const storageRef = ref(storage, filename) 62 | const uploadTask = uploadBytesResumable(storageRef, image) 63 | 64 | // Register three observers: 65 | // 1. 'state_changed' observer, called any time the state changes 66 | // 2. Error observer, called on failure 67 | // 3. Completion observer, called on successful completion 68 | uploadTask.on( 69 | 'state_changed', 70 | (snapshot) => { 71 | // Observe state change events such as progress, pause, and resume 72 | // Get task progress, including the number of bytes uploaded and the total number of bytes to be uploaded 73 | const progress = 74 | (snapshot.bytesTransferred / snapshot.totalBytes) * 100 75 | console.log(`Upload is ${progress}% done`) 76 | // hide the loading toast and show a success toast after the upload is complete 77 | toast.dismiss() 78 | toast.success(`Upload is ${Math.round(progress)}% done`) 79 | // eslint-disable-next-line default-case 80 | switch (snapshot.state) { 81 | case 'paused': 82 | console.log('Upload is paused') 83 | break 84 | case 'running': 85 | console.log('Upload is running') 86 | break 87 | } 88 | }, 89 | (error) => { 90 | // Handle unsuccessful uploads 91 | console.log('Upload failed') 92 | // hide the loading toast and show an error toast if the upload fails 93 | toast.dismiss() 94 | toast.error('Upload failed') 95 | reject(error) 96 | }, 97 | () => { 98 | // Handle successful uploads on complete 99 | // For instance, get the download URL: https://firebasestorage.googleapis.com/... 100 | getDownloadURL(uploadTask.snapshot.ref).then((downloadURL) => { 101 | resolve(downloadURL) 102 | }) 103 | }, 104 | ) 105 | }) 106 | } 107 | 108 | const resetForm = () => { 109 | setTweet('') 110 | setImageFiles([]) 111 | setImagePreviews([]) 112 | setLoading(false) 113 | } 114 | 115 | const handleSubmit = async ( 116 | e: React.FormEvent | React.KeyboardEvent, 117 | ) => { 118 | e.preventDefault() 119 | 120 | if (tweet.trim() === '' && imageFiles.length === 0) { 121 | toast.error('Tweet cannot be empty') 122 | return 123 | } 124 | 125 | try { 126 | setLoading(true) 127 | 128 | let imgUrls: string[] = [] 129 | 130 | if (imageFiles.length > 0) { 131 | imgUrls = await Promise.all( 132 | [...imageFiles].map((image) => storeImage(image)), 133 | ).catch((error) => { 134 | toast.dismiss() 135 | toast.error('Images not uploaded') 136 | throw error 137 | }) 138 | } 139 | 140 | const formData = { 141 | title: '', 142 | user: session?.user, 143 | content: tweet, 144 | timestamp: serverTimestamp(), 145 | userRef: session?.user?.email || session?.user?.name, 146 | images: imgUrls, 147 | likes: [], 148 | comments: [], 149 | } 150 | 151 | await addDoc(collection(db, 'tweets'), formData) 152 | toast.dismiss() 153 | toast.success('Tweet created successfully') 154 | resetForm() // Reset all form states 155 | } catch (error) { 156 | console.error('Error creating tweet:', error) 157 | toast.error('Error creating tweet') 158 | setLoading(false) 159 | } 160 | } 161 | 162 | const handleChange = (e: React.ChangeEvent) => { 163 | setTweet(e.target.value) 164 | } 165 | 166 | const handleInputKeyDown = (e: React.KeyboardEvent) => { 167 | if (e.key === 'Enter' && !e.shiftKey) { 168 | e.preventDefault() 169 | handleSubmit(e) 170 | } 171 | } 172 | 173 | const handleIconHover = () => { 174 | if (fileInputRef.current) { 175 | fileInputRef.current.click() 176 | } 177 | } 178 | 179 | return session ? ( 180 |
181 |
182 |

183 | Home 184 |

185 |
186 |

187 | 188 | For you 189 | 190 |

191 |

192 | 193 | Following 194 | 195 |

196 |
197 |
198 | 199 |
202 |
203 | profile image 210 |
211 | 218 |
219 | {imagePreviews.map((preview, index) => ( 220 |
221 | uploaded preview 228 | 234 |
235 | ))} 236 |
237 |
238 |
239 | 240 |
241 |
242 |
243 |
244 |
245 | 257 | 262 | 263 | 264 | 265 | 266 | 267 |
268 |
269 | 276 |
277 |
278 |
279 |
280 |
281 |
282 | ) : ( 283 |
284 |

285 | Explore 286 |

287 | 288 | 289 | 293 | 294 |
295 | ) 296 | } 297 | -------------------------------------------------------------------------------- /src/components/Tweet.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react' 2 | import { FaHeart, FaComment, FaRetweet, FaEye, FaShare } from 'react-icons/fa' 3 | import { motion } from 'framer-motion' 4 | import Moment from 'moment' 5 | import { db } from '@/config/firebase' 6 | import { doc, getDoc, updateDoc } from 'firebase/firestore' 7 | import { useSession } from 'next-auth/react' 8 | import { toast } from 'react-hot-toast' 9 | import { TweetT } from '../types/tweets' 10 | import Button from './Button' 11 | import Comments from './Comments' 12 | 13 | type Props = { 14 | tweet: TweetT 15 | } 16 | 17 | const itemVariants = { 18 | hidden: { opacity: 0, y: -10 }, 19 | visible: { opacity: 1, y: 0, transition: { duration: 0.1 } }, 20 | } 21 | export default function Tweet({ tweet }: Props) { 22 | const { user, content, id, timestamp, title, images, likes } = tweet 23 | const { data: session } = useSession() 24 | const [commentText, setCommentText] = useState('') 25 | const [commentBoxVisible, setCommentBoxVisible] = useState(false) 26 | const [userAlreadyLiked, setUserAlreadyLiked] = useState(false) 27 | 28 | useEffect(() => { 29 | if (session?.user?.email) { 30 | const userEmail = session.user.email 31 | const userAlreadyLiked = likes?.some((like) => like.email === userEmail) 32 | setUserAlreadyLiked(userAlreadyLiked as boolean) 33 | } 34 | }, [likes, session]) 35 | 36 | const handleLike = async (tweet: TweetT) => { 37 | try { 38 | const tweetId = tweet.id as string 39 | const docRef = doc(db, 'tweets', tweetId) 40 | const docSnap = await getDoc(docRef) 41 | 42 | if (!docSnap.exists()) { 43 | console.log('Tweet document does not exist') 44 | toast.error('Tweet document does not exist') 45 | return 46 | } 47 | 48 | const existingData = docSnap.data() 49 | const likes = existingData.likes || [] 50 | const userEmail = session?.user?.email 51 | 52 | const userAlreadyLiked = likes.some( 53 | (like: { email: string | null | undefined }) => 54 | like.email === userEmail, 55 | ) 56 | 57 | if (userAlreadyLiked) { 58 | // User has already liked the tweet, so unlike it 59 | const updatedLikes = likes.filter( 60 | (like: { email: string | null | undefined }) => 61 | like.email !== userEmail, 62 | ) 63 | const formDataCopy = { 64 | ...existingData, 65 | likes: updatedLikes, 66 | } 67 | await updateDoc(docRef, formDataCopy) 68 | 69 | toast.dismiss() 70 | toast.success('Unliked the tweet!') 71 | } else if (!userAlreadyLiked) { 72 | // User has not liked the tweet, so like it 73 | const updatedLikes = [...likes, { email: userEmail }] 74 | const formDataCopy = { 75 | ...existingData, 76 | likes: updatedLikes, 77 | } 78 | await updateDoc(docRef, formDataCopy) 79 | 80 | toast.dismiss() 81 | toast.success('Liked the tweet!') 82 | } else { 83 | // User has already liked the tweet or an unexpected condition occurred 84 | toast.error('You have already liked this tweet.') 85 | } 86 | } catch (error) { 87 | console.log('Error updating tweet likes:', error) 88 | 89 | toast.dismiss() 90 | toast.error('Failed to update the tweet. Please try again.') 91 | } 92 | } 93 | 94 | const handleCommentSubmit = async ( 95 | event: React.FormEvent, 96 | ) => { 97 | event.preventDefault() 98 | 99 | if (commentText.trim() === '') { 100 | return 101 | } 102 | 103 | try { 104 | const tweetId = tweet.id as string 105 | const docRef = doc(db, 'tweets', tweetId) 106 | const docSnap = await getDoc(docRef) 107 | 108 | if (!docSnap.exists()) { 109 | console.log('Tweet document does not exist') 110 | toast.error('Tweet document does not exist') 111 | return 112 | } 113 | 114 | const existingData = docSnap.data() 115 | const comments = existingData?.comments || [] 116 | 117 | const commentData = { 118 | text: commentText.trim(), 119 | user: session?.user, 120 | timestamp: { 121 | seconds: Math.floor(Date.now() / 1000), // Get the current timestamp in seconds 122 | nanoseconds: 0, 123 | }, 124 | } 125 | 126 | const updatedComments = [commentData, ...comments] 127 | const formDataCopy = { 128 | ...existingData, 129 | comments: updatedComments, 130 | } 131 | 132 | await updateDoc(docRef, formDataCopy) 133 | 134 | setCommentText('') 135 | setCommentBoxVisible(false) 136 | 137 | toast.dismiss() 138 | toast.success('Comment submitted successfully!') 139 | } catch (error) { 140 | console.log('Error submitting comment:', error) 141 | 142 | toast.dismiss() 143 | toast.error('Failed to submit comment. Please try again.') 144 | } 145 | } 146 | 147 | const media = [ 148 | { 149 | type: 'image', 150 | url: 'https://pbs.twimg.com/media/Fs9KGdLXwAIyR0c?format=png&name=900x900', 151 | }, 152 | ] 153 | 154 | // const images = media && media.filter((item) => item.type === 'image') 155 | const videos = media && media.filter((item) => item.type === 'video') 156 | 157 | return ( 158 | 164 |
165 |
166 | 174 |
175 |
176 | 181 | {user.name} 182 | 183 | @{user.name} 184 | · 185 | 186 | {timestamp?.seconds && ( 187 | 188 | {Moment.unix(timestamp?.seconds).fromNow()} 189 | 190 | )} 191 |
192 |

{content}

193 | 194 |
195 | {images && ( 196 |
1 ? 'grid-cols-2 gap-4' : 'grid-cols-1' 199 | }`}> 200 | {images.map((item, index) => ( 201 | 214 | ))} 215 |
216 | )} 217 | 218 | {videos && ( 219 |
1 ? 'grid-cols-2 gap-4' : 'grid-cols-1' 222 | } `}> 223 | {videos.map((item, index) => ( 224 |
227 | 239 |
240 | ))} 241 |
242 | )} 243 |
244 | 245 |
246 |
247 | { 254 | if (tweet.id && session) { 255 | handleLike(tweet) 256 | } 257 | }} 258 | /> 259 | 260 | {tweet?.likes && tweet?.likes?.length > 0 261 | ? tweet?.likes.length 262 | : ''} 263 | 264 |
265 |
268 | session && setCommentBoxVisible(!commentBoxVisible) 269 | }> 270 | 271 | {tweet?.comments?.length || ''} 272 |
273 |
274 | 275 | 87 276 |
277 |
278 | 279 | 77 280 |
281 |
282 | 283 | 234 284 |
285 |
286 | 287 | {commentBoxVisible ? ( 288 |
292 | setCommentText(e.target.value)} 295 | type="text" 296 | className="w-full cursor-pointer rounded-full border border-gray-300 bg-gray-50 px-6 text-gray-900 outline-none transition duration-150 hover:border-gray-400 dark:cursor-pointer dark:border-gray-700 dark:bg-[#202327] dark:text-white dark:placeholder-gray-400 dark:hover:bg-gray-700" 297 | placeholder="Write a comment..." 298 | /> 299 | 300 |
301 | 306 |
307 |
308 | ) : null} 309 |
310 |
311 |
312 | 313 | {tweet?.comments && tweet?.comments?.length > 0 ? ( 314 | 315 | ) : null} 316 |
317 | ) 318 | } 319 | -------------------------------------------------------------------------------- /src/utils/mockedData.ts: -------------------------------------------------------------------------------- 1 | import { TweetT, TweetTemp } from '../types/tweets'; 2 | 3 | export const mockedTweets: TweetTemp[] = [ 4 | { 5 | id: 1, 6 | title: 'React and TypeScript', 7 | user: { 8 | username: 'Saddam-dev', 9 | name: 'Saddam Arbaa', 10 | avatar: 'https://avatars.githubusercontent.com/u/51326421?v=4', 11 | }, 12 | content: 'Just tried out Vite with React and TypeScript and it is amazing! 🚀🔥 #reactjs #typescript #vite', 13 | media: [ 14 | { 15 | type: 'image', 16 | url: 'https://pbs.twimg.com/media/Fs9KGdLXwAIyR0c?format=png&name=900x900', 17 | }, 18 | ], 19 | timestamp: '2023-04-01T09:30:00Z', 20 | likes: [ 21 | { 22 | user: { 23 | username: 'reactdev', 24 | name: 'React Dev', 25 | avatar: 'https://avatar.com/reactdev', 26 | }, 27 | timestamp: '2023-04-01T10:00:00Z', 28 | }, 29 | { 30 | user: { 31 | username: 'tailwindlover', 32 | name: 'Tailwind Lover', 33 | avatar: 'https://avatar.com/tailwindlover', 34 | }, 35 | timestamp: '2023-04-01T11:00:00Z', 36 | }, 37 | ], 38 | comments: [ 39 | { 40 | user: { 41 | username: 'reactdev', 42 | name: 'React Dev', 43 | avatar: 'https://avatar.com/reactdev', 44 | }, 45 | content: "I know right? I started using it recently too and it's a game changer!", 46 | timestamp: '2023-04-01T10:30:00Z', 47 | }, 48 | { 49 | user: { 50 | username: 'tailwindlover', 51 | name: 'Tailwind Lover', 52 | avatar: 'https://avatar.com/tailwindlover', 53 | }, 54 | content: "Yes, I couldn't agree more. It has saved me so much time on styling.", 55 | timestamp: '2023-04-01T11:30:00Z', 56 | }, 57 | ], 58 | retweets: [ 59 | { 60 | user: { 61 | username: 'reactdev', 62 | name: 'React Dev', 63 | avatar: 'https://avatar.com/reactdev', 64 | }, 65 | timestamp: '2023-04-01T12:00:00Z', 66 | }, 67 | { 68 | user: { 69 | username: 'tailwindlover', 70 | name: 'Tailwind Lover', 71 | avatar: 'https://avatar.com/tailwindlover', 72 | }, 73 | timestamp: '2023-04-01T13:00:00Z', 74 | }, 75 | ], 76 | views: [ 77 | { 78 | user: { 79 | username: 'reactdev', 80 | name: 'React Dev', 81 | avatar: 'https://avatar.com/reactdev', 82 | }, 83 | timestamp: '2023-04-01T10:00:00Z', 84 | }, 85 | { 86 | user: { 87 | username: 'tailwindlover', 88 | name: 'Tailwind Lover', 89 | avatar: 'https://avatar.com/tailwindlover', 90 | }, 91 | timestamp: '2023-04-01T11:00:00Z', 92 | }, 93 | ], 94 | shares: [ 95 | { 96 | user: { 97 | username: 'reactdev', 98 | name: 'React Dev', 99 | avatar: 'https://avatar.com/reactdev', 100 | }, 101 | timestamp: '2023-04-01T12:30:00Z', 102 | }, 103 | ], 104 | },{ 105 | id: 1, 106 | title: 'Next.js 13: The Latest Features and Improvements', 107 | user: { 108 | username: 'Tester now, aspiring Fullstack', 109 | name: 'Naveen Kolambage', 110 | avatar: 'https://pbs.twimg.com/profile_images/1560233396403507200/5hKYoyz1_400x400.jpg', 111 | }, 112 | content: 'TypeScript Course for Beginners - Learn TypeScript from Scratch!', 113 | media: [ 114 | { 115 | type: 'image', 116 | url: 'https://i.ytimg.com/vi/XShQO3BvOyM/maxresdefault.jpg', 117 | }, 118 | { 119 | type: 'video', 120 | url: 'https://www.youtube.com/embed/BwuLxPH8IDs?autoplay=1&mute=1', 121 | }, 122 | 123 | 124 | ], 125 | timestamp: '2023-04-01T09:30:00Z', 126 | likes: [ 127 | { 128 | user: { 129 | username: 'reactdev', 130 | name: 'React Dev', 131 | avatar: 'https://avatar.com/reactdev', 132 | }, 133 | timestamp: '2023-04-01T10:00:00Z', 134 | }, 135 | { 136 | user: { 137 | username: 'tailwindlover', 138 | name: 'Tailwind Lover', 139 | avatar: 'https://avatar.com/tailwindlover', 140 | }, 141 | timestamp: '2023-04-01T11:00:00Z', 142 | }, 143 | ], 144 | comments: [ 145 | { 146 | user: { 147 | username: 'reactdev', 148 | name: 'React Dev', 149 | avatar: 'https://avatar.com/reactdev', 150 | }, 151 | content: "I know right? I started using it recently too and it's a game changer!", 152 | timestamp: '2023-04-01T10:30:00Z', 153 | }, 154 | { 155 | user: { 156 | username: 'tailwindlover', 157 | name: 'Tailwind Lover', 158 | avatar: 'https://avatar.com/tailwindlover', 159 | }, 160 | content: "Yes, I couldn't agree more. It has saved me so much time on styling.", 161 | timestamp: '2023-04-01T11:30:00Z', 162 | }, 163 | ], 164 | retweets: [ 165 | { 166 | user: { 167 | username: 'reactdev', 168 | name: 'React Dev', 169 | avatar: 'https://avatar.com/reactdev', 170 | }, 171 | timestamp: '2023-04-01T12:00:00Z', 172 | }, 173 | { 174 | user: { 175 | username: 'tailwindlover', 176 | name: 'Tailwind Lover', 177 | avatar: 'https://avatar.com/tailwindlover', 178 | }, 179 | timestamp: '2023-04-01T13:00:00Z', 180 | }, 181 | ], 182 | views: [ 183 | { 184 | user: { 185 | username: 'reactdev', 186 | name: 'React Dev', 187 | avatar: 'https://avatar.com/reactdev', 188 | }, 189 | timestamp: '2023-04-01T10:00:00Z', 190 | }, 191 | { 192 | user: { 193 | username: 'tailwindlover', 194 | name: 'Tailwind Lover', 195 | avatar: 'https://avatar.com/tailwindlover', 196 | }, 197 | timestamp: '2023-04-01T11:00:00Z', 198 | }, 199 | ], 200 | shares: [ 201 | { 202 | user: { 203 | username: 'reactdev', 204 | name: 'React Dev', 205 | avatar: 'https://avatar.com/reactdev', 206 | }, 207 | timestamp: '2023-04-01T12:30:00Z', 208 | }, 209 | ], 210 | }, 211 | { 212 | id: 1, 213 | user: { 214 | username: 'code-with-saddam', 215 | name: 'Saddam Arbaa', 216 | avatar: 'https://avatars.githubusercontent.com/u/51326421?v=4', 217 | }, 218 | title: 'Free Code Camp', 219 | content: 220 | "Just discovered Free Code Camp and I'm loving it! It's a great resource for learning to code and it's completely free! #freecodecamp #codingeducation #learntocode", 221 | 222 | media: [ 223 | { 224 | type: 'image', 225 | url: 'http://www.goodworklabs.com/wp-content/uploads/2016/10/reactjs.png', 226 | }, 227 | { 228 | type: 'image', 229 | url: 'https://s3.amazonaws.com/coursesity-blog/2020/07/React_Js.png', 230 | }, 231 | { 232 | type: 'video', 233 | url: 'https://www.youtube.com/embed/1WmNXEVia8I?autoplay=1&mute=1', 234 | }, 235 | { 236 | type: 'video', 237 | url: 'https://www.youtube.com/embed/4UZrsTqkcW4?autoplay=1&mute=1', 238 | }, 239 | ], 240 | timestamp: '2023-03-31T12:30:00Z', 241 | likes: [ 242 | { 243 | user: { 244 | username: 'janedoe', 245 | name: 'Jane Doe', 246 | avatar: 'https://avatar.com/janedoe', 247 | }, 248 | timestamp: '2023-03-31T13:00:00Z', 249 | }, 250 | { 251 | user: { 252 | username: 'bobsmith', 253 | name: 'Bob Smith', 254 | avatar: 'https://avatar.com/bobsmith', 255 | }, 256 | timestamp: '2023-03-31T14:00:00Z', 257 | }, 258 | ], 259 | comments: [ 260 | { 261 | user: { 262 | username: 'janedoe', 263 | name: 'Jane Doe', 264 | avatar: 'https://avatar.com/janedoe', 265 | }, 266 | content: 'That sounds amazing! Where did you get it?', 267 | timestamp: '2023-03-31T13:30:00Z', 268 | }, 269 | ], 270 | retweets: [ 271 | { 272 | user: { 273 | username: 'janedoe', 274 | name: 'Jane Doe', 275 | avatar: 'https://avatar.com/janedoe', 276 | }, 277 | timestamp: '2023-03-31T13:30:00Z', 278 | }, 279 | ], 280 | views: [ 281 | { 282 | user: { 283 | username: 'janedoe', 284 | name: 'Jane Doe', 285 | avatar: 'https://avatar.com/janedoe', 286 | }, 287 | timestamp: '2023-03-31T13:00:00Z', 288 | }, 289 | { 290 | user: { 291 | username: 'bobsmith', 292 | name: 'Bob Smith', 293 | avatar: 'https://avatar.com/bobsmith', 294 | }, 295 | timestamp: '2023-03-31T14:00:00Z', 296 | }, 297 | ], 298 | shares: [ 299 | { 300 | user: { 301 | username: 'janedoe', 302 | name: 'Jane Doe', 303 | avatar: 'https://avatar.com/janedoe', 304 | }, 305 | timestamp: '2023-03-31T13:30:00Z', 306 | }, 307 | { 308 | user: { 309 | username: 'bobsmith', 310 | name: 'Bob Smith', 311 | avatar: 'https://avatar.com/bobsmith', 312 | }, 313 | timestamp: '2023-03-31T14:30:00Z', 314 | }, 315 | ], 316 | }, 317 | { 318 | id: 1, 319 | title: 'Modern JavaScript', 320 | user: { 321 | username: 'Tester now, aspiring Fullstack', 322 | name: 'Naveen Kolambage', 323 | avatar: 'https://pbs.twimg.com/profile_images/1560233396403507200/5hKYoyz1_400x400.jpg', 324 | }, 325 | content: 'Dividing an array into subarrays dynamically — using slice()', 326 | media: [ 327 | { 328 | type: 'image', 329 | url: 'https://miro.medium.com/v2/resize:fit:1400/1*EpU3JfnGq0iIDL2bIeDijQ.jpeg', 330 | }, 331 | ], 332 | timestamp: '2023-04-01T09:30:00Z', 333 | likes: [ 334 | { 335 | user: { 336 | username: 'reactdev', 337 | name: 'React Dev', 338 | avatar: 'https://avatar.com/reactdev', 339 | }, 340 | timestamp: '2023-04-01T10:00:00Z', 341 | }, 342 | { 343 | user: { 344 | username: 'tailwindlover', 345 | name: 'Tailwind Lover', 346 | avatar: 'https://avatar.com/tailwindlover', 347 | }, 348 | timestamp: '2023-04-01T11:00:00Z', 349 | }, 350 | ], 351 | comments: [ 352 | { 353 | user: { 354 | username: 'reactdev', 355 | name: 'React Dev', 356 | avatar: 'https://avatar.com/reactdev', 357 | }, 358 | content: "I know right? I started using it recently too and it's a game changer!", 359 | timestamp: '2023-04-01T10:30:00Z', 360 | }, 361 | { 362 | user: { 363 | username: 'tailwindlover', 364 | name: 'Tailwind Lover', 365 | avatar: 'https://avatar.com/tailwindlover', 366 | }, 367 | content: "Yes, I couldn't agree more. It has saved me so much time on styling.", 368 | timestamp: '2023-04-01T11:30:00Z', 369 | }, 370 | ], 371 | retweets: [ 372 | { 373 | user: { 374 | username: 'reactdev', 375 | name: 'React Dev', 376 | avatar: 'https://avatar.com/reactdev', 377 | }, 378 | timestamp: '2023-04-01T12:00:00Z', 379 | }, 380 | { 381 | user: { 382 | username: 'tailwindlover', 383 | name: 'Tailwind Lover', 384 | avatar: 'https://avatar.com/tailwindlover', 385 | }, 386 | timestamp: '2023-04-01T13:00:00Z', 387 | }, 388 | ], 389 | views: [ 390 | { 391 | user: { 392 | username: 'reactdev', 393 | name: 'React Dev', 394 | avatar: 'https://avatar.com/reactdev', 395 | }, 396 | timestamp: '2023-04-01T10:00:00Z', 397 | }, 398 | { 399 | user: { 400 | username: 'tailwindlover', 401 | name: 'Tailwind Lover', 402 | avatar: 'https://avatar.com/tailwindlover', 403 | }, 404 | timestamp: '2023-04-01T11:00:00Z', 405 | }, 406 | ], 407 | shares: [ 408 | { 409 | user: { 410 | username: 'reactdev', 411 | name: 'React Dev', 412 | avatar: 'https://avatar.com/reactdev', 413 | }, 414 | timestamp: '2023-04-01T12:30:00Z', 415 | }, 416 | ], 417 | }, 418 | { 419 | title: 'Tailwind CSS', 420 | id: 1, 421 | user: { 422 | username: 'tailwindfan', 423 | name: 'Tailwind Fan', 424 | avatar: 425 | 'https://images.unsplash.com/profile-1623795199834-f8109281554dimage?ixlib=rb-4.0.3&crop=faces&fit=crop&w=32&h=32', 426 | }, 427 | content: 'I just discovered Tailwind CSS and it makes styling so easy! 😍 #tailwindcss #react', 428 | media: [ 429 | { 430 | type: 'image', 431 | url: 'https://i.pinimg.com/originals/9d/69/fd/9d69fd497059b8c9f3942806acda6bed.png', 432 | }, 433 | { 434 | type: 'image', 435 | url: 'https://res.infoq.com/news/2020/12/tailwind-css-v2/en/headerimage/header+(1)-1608368148194.jpg', 436 | }, 437 | ], 438 | timestamp: '2023-04-01T09:30:00Z', 439 | likes: [ 440 | { 441 | user: { 442 | username: 'reactdev', 443 | name: 'React Dev', 444 | avatar: 'https://avatar.com/reactdev', 445 | }, 446 | timestamp: '2023-04-01T10:00:00Z', 447 | }, 448 | { 449 | user: { 450 | username: 'tailwindlover', 451 | name: 'Tailwind Lover', 452 | avatar: 'https://avatar.com/tailwindlover', 453 | }, 454 | timestamp: '2023-04-01T11:00:00Z', 455 | }, 456 | ], 457 | comments: [ 458 | { 459 | user: { 460 | username: 'reactdev', 461 | name: 'React Dev', 462 | avatar: 'https://avatar.com/reactdev', 463 | }, 464 | content: "I know right? I started using it recently too and it's a game changer!", 465 | timestamp: '2023-04-01T10:30:00Z', 466 | }, 467 | { 468 | user: { 469 | username: 'tailwindlover', 470 | name: 'Tailwind Lover', 471 | avatar: 'https://avatar.com/tailwindlover', 472 | }, 473 | content: "Yes, I couldn't agree more. It has saved me so much time on styling.", 474 | timestamp: '2023-04-01T11:30:00Z', 475 | }, 476 | ], 477 | retweets: [ 478 | { 479 | user: { 480 | username: 'reactdev', 481 | name: 'React Dev', 482 | avatar: 'https://avatar.com/reactdev', 483 | }, 484 | timestamp: '2023-04-01T12:00:00Z', 485 | }, 486 | { 487 | user: { 488 | username: 'tailwindlover', 489 | name: 'Tailwind Lover', 490 | avatar: 'https://avatar.com/tailwindlover', 491 | }, 492 | timestamp: '2023-04-01T13:00:00Z', 493 | }, 494 | ], 495 | views: [ 496 | { 497 | user: { 498 | username: 'reactdev', 499 | name: 'React Dev', 500 | avatar: 'https://avatar.com/reactdev', 501 | }, 502 | timestamp: '2023-04-01T10:00:00Z', 503 | }, 504 | { 505 | user: { 506 | username: 'tailwindlover', 507 | name: 'Tailwind Lover', 508 | avatar: 'https://avatar.com/tailwindlover', 509 | }, 510 | timestamp: '2023-04-01T11:00:00Z', 511 | }, 512 | ], 513 | shares: [ 514 | { 515 | user: { 516 | username: 'reactdev', 517 | name: 'React Dev', 518 | avatar: 'https://avatar.com/reactdev', 519 | }, 520 | timestamp: '2023-04-01T12:30:00Z', 521 | }, 522 | ], 523 | }, 524 | { 525 | id: 8, 526 | title: 'Building a Node.js API with Express, Mongoose, and TypeScript', 527 | user: { 528 | username: 'code-with-saddam', 529 | name: 'Saddam Arbaa', 530 | avatar: 'https://avatars.githubusercontent.com/u/51326421?v=4', 531 | }, 532 | content: 533 | 'In this article, we will walk through the process of building a Node.js API using the popular Express framework, MongoDB with Mongoose, and TypeScript. We will cover everything from setting up the project, to defining the endpoints, to handling errors and validating data. Whether you are new to Node.js or an experienced developer, this guide will help you build a robust and scalable API with ease.', 534 | media: [ 535 | { 536 | type: 'image', 537 | url: 'https://codersera.com/blog/wp-content/uploads/2019/10/nodejs-thumb.jpg', 538 | }, 539 | { 540 | type: 'video', 541 | url: 'https://www.youtube.com/embed/Oe421EPjeBE?autoplay=1&mute=1', 542 | }, 543 | ], 544 | timestamp: '2023-04-01T09:30:00Z', 545 | likes: [ 546 | { 547 | user: { 548 | username: 'reactdev', 549 | name: 'React Dev', 550 | avatar: 'https://avatar.com/reactdev', 551 | }, 552 | timestamp: '2023-04-01T10:00:00Z', 553 | }, 554 | { 555 | user: { 556 | username: 'tailwindlover', 557 | name: 'Tailwind Lover', 558 | avatar: 'https://avatar.com/tailwindlover', 559 | }, 560 | timestamp: '2023-04-01T11:00:00Z', 561 | }, 562 | ], 563 | comments: [ 564 | { 565 | user: { 566 | username: 'reactdev', 567 | name: 'React Dev', 568 | avatar: 'https://avatar.com/reactdev', 569 | }, 570 | content: "I know right? I started using it recently too and it's a game changer!", 571 | timestamp: '2023-04-01T10:30:00Z', 572 | }, 573 | { 574 | user: { 575 | username: 'tailwindlover', 576 | name: 'Tailwind Lover', 577 | avatar: 'https://avatar.com/tailwindlover', 578 | }, 579 | content: "Yes, I couldn't agree more. It has saved me so much time on styling.", 580 | timestamp: '2023-04-01T11:30:00Z', 581 | }, 582 | ], 583 | retweets: [ 584 | { 585 | user: { 586 | username: 'reactdev', 587 | name: 'React Dev', 588 | avatar: 'https://avatar.com/reactdev', 589 | }, 590 | timestamp: '2023-04-01T12:00:00Z', 591 | }, 592 | { 593 | user: { 594 | username: 'tailwindlover', 595 | name: 'Tailwind Lover', 596 | avatar: 'https://avatar.com/tailwindlover', 597 | }, 598 | timestamp: '2023-04-01T13:00:00Z', 599 | }, 600 | ], 601 | views: [ 602 | { 603 | user: { 604 | username: 'reactdev', 605 | name: 'React Dev', 606 | avatar: 'https://avatar.com/reactdev', 607 | }, 608 | timestamp: '2023-04-01T10:00:00Z', 609 | }, 610 | { 611 | user: { 612 | username: 'tailwindlover', 613 | name: 'Tailwind Lover', 614 | avatar: 'https://avatar.com/tailwindlover', 615 | }, 616 | timestamp: '2023-04-01T11:00:00Z', 617 | }, 618 | ], 619 | shares: [ 620 | { 621 | user: { 622 | username: 'reactdev', 623 | name: 'React Dev', 624 | avatar: 'https://avatar.com/reactdev', 625 | }, 626 | timestamp: '2023-04-01T12:30:00Z', 627 | }, 628 | ], 629 | }, 630 | { 631 | id: 1, 632 | title: 'React and TypeScript', 633 | user: { 634 | username: 'Tester now, aspiring Fullstack', 635 | name: 'Naveen Kolambage', 636 | avatar: 'https://pbs.twimg.com/profile_images/1560233396403507200/5hKYoyz1_400x400.jpg', 637 | }, 638 | content: 'Just tried out Vite with React and TypeScript and it is amazing! 🚀🔥 #reactjs #typescript #vite', 639 | media: [ 640 | { 641 | type: 'image', 642 | url: 'https://tse3.mm.bing.net/th?id=OIP.KJSD0dmEYMVYXqlRISNJUAHaD_&pid=Api&P=0', 643 | }, 644 | { 645 | type: 'video', 646 | url: 'https://www.youtube.com/embed/4UZrsTqkcW4?autoplay=1&mute=1', 647 | }, 648 | ], 649 | timestamp: '2023-04-01T09:30:00Z', 650 | likes: [ 651 | { 652 | user: { 653 | username: 'reactdev', 654 | name: 'React Dev', 655 | avatar: 'https://avatar.com/reactdev', 656 | }, 657 | timestamp: '2023-04-01T10:00:00Z', 658 | }, 659 | { 660 | user: { 661 | username: 'tailwindlover', 662 | name: 'Tailwind Lover', 663 | avatar: 'https://avatar.com/tailwindlover', 664 | }, 665 | timestamp: '2023-04-01T11:00:00Z', 666 | }, 667 | ], 668 | comments: [ 669 | { 670 | user: { 671 | username: 'reactdev', 672 | name: 'React Dev', 673 | avatar: 'https://avatar.com/reactdev', 674 | }, 675 | content: "I know right? I started using it recently too and it's a game changer!", 676 | timestamp: '2023-04-01T10:30:00Z', 677 | }, 678 | { 679 | user: { 680 | username: 'tailwindlover', 681 | name: 'Tailwind Lover', 682 | avatar: 'https://avatar.com/tailwindlover', 683 | }, 684 | content: "Yes, I couldn't agree more. It has saved me so much time on styling.", 685 | timestamp: '2023-04-01T11:30:00Z', 686 | }, 687 | ], 688 | retweets: [ 689 | { 690 | user: { 691 | username: 'reactdev', 692 | name: 'React Dev', 693 | avatar: 'https://avatar.com/reactdev', 694 | }, 695 | timestamp: '2023-04-01T12:00:00Z', 696 | }, 697 | { 698 | user: { 699 | username: 'tailwindlover', 700 | name: 'Tailwind Lover', 701 | avatar: 'https://avatar.com/tailwindlover', 702 | }, 703 | timestamp: '2023-04-01T13:00:00Z', 704 | }, 705 | ], 706 | views: [ 707 | { 708 | user: { 709 | username: 'reactdev', 710 | name: 'React Dev', 711 | avatar: 'https://avatar.com/reactdev', 712 | }, 713 | timestamp: '2023-04-01T10:00:00Z', 714 | }, 715 | { 716 | user: { 717 | username: 'tailwindlover', 718 | name: 'Tailwind Lover', 719 | avatar: 'https://avatar.com/tailwindlover', 720 | }, 721 | timestamp: '2023-04-01T11:00:00Z', 722 | }, 723 | ], 724 | shares: [ 725 | { 726 | user: { 727 | username: 'reactdev', 728 | name: 'React Dev', 729 | avatar: 'https://avatar.com/reactdev', 730 | }, 731 | timestamp: '2023-04-01T12:30:00Z', 732 | }, 733 | ], 734 | }, 735 | { 736 | id: 1, 737 | title: 'Next.js and GraphQL', 738 | user: { 739 | username: 'Saddam-dev', 740 | name: 'Saddam Arbaa', 741 | avatar: 'https://avatars.githubusercontent.com/u/51326421?v=4', 742 | }, 743 | content: 'Next.js and GraphQL! 🚀🔥 #reactjs #typescript #vite', 744 | media: [ 745 | { 746 | type: 'image', 747 | url: 'https://www.apollographql.com/blog/static/49-1-0d86b359ff07c6fbf68b5f5de87ac40b.png', 748 | }, 749 | ], 750 | timestamp: '2023-04-01T09:30:00Z', 751 | likes: [ 752 | { 753 | user: { 754 | username: 'reactdev', 755 | name: 'React Dev', 756 | avatar: 'https://avatar.com/reactdev', 757 | }, 758 | timestamp: '2023-04-01T10:00:00Z', 759 | }, 760 | { 761 | user: { 762 | username: 'tailwindlover', 763 | name: 'Tailwind Lover', 764 | avatar: 'https://avatar.com/tailwindlover', 765 | }, 766 | timestamp: '2023-04-01T11:00:00Z', 767 | }, 768 | ], 769 | comments: [ 770 | { 771 | user: { 772 | username: 'reactdev', 773 | name: 'React Dev', 774 | avatar: 'https://avatar.com/reactdev', 775 | }, 776 | content: "I know right? I started using it recently too and it's a game changer!", 777 | timestamp: '2023-04-01T10:30:00Z', 778 | }, 779 | { 780 | user: { 781 | username: 'tailwindlover', 782 | name: 'Tailwind Lover', 783 | avatar: 'https://avatar.com/tailwindlover', 784 | }, 785 | content: "Yes, I couldn't agree more. It has saved me so much time on styling.", 786 | timestamp: '2023-04-01T11:30:00Z', 787 | }, 788 | ], 789 | retweets: [ 790 | { 791 | user: { 792 | username: 'reactdev', 793 | name: 'React Dev', 794 | avatar: 'https://avatar.com/reactdev', 795 | }, 796 | timestamp: '2023-04-01T12:00:00Z', 797 | }, 798 | { 799 | user: { 800 | username: 'tailwindlover', 801 | name: 'Tailwind Lover', 802 | avatar: 'https://avatar.com/tailwindlover', 803 | }, 804 | timestamp: '2023-04-01T13:00:00Z', 805 | }, 806 | ], 807 | views: [ 808 | { 809 | user: { 810 | username: 'reactdev', 811 | name: 'React Dev', 812 | avatar: 'https://avatar.com/reactdev', 813 | }, 814 | timestamp: '2023-04-01T10:00:00Z', 815 | }, 816 | { 817 | user: { 818 | username: 'tailwindlover', 819 | name: 'Tailwind Lover', 820 | avatar: 'https://avatar.com/tailwindlover', 821 | }, 822 | timestamp: '2023-04-01T11:00:00Z', 823 | }, 824 | ], 825 | shares: [ 826 | { 827 | user: { 828 | username: 'reactdev', 829 | name: 'React Dev', 830 | avatar: 'https://avatar.com/reactdev', 831 | }, 832 | timestamp: '2023-04-01T12:30:00Z', 833 | }, 834 | ], 835 | }, 836 | { 837 | title: 'Tailwind CSS', 838 | id: 1, 839 | user: { 840 | username: 'tailwindfan', 841 | name: 'Tailwind Fan', 842 | avatar: 843 | 'https://images.unsplash.com/profile-1623795199834-f8109281554dimage?ixlib=rb-4.0.3&crop=faces&fit=crop&w=32&h=32', 844 | }, 845 | content: 'I just discovered Tailwind CSS and it makes styling so easy! 😍 #tailwindcss #react', 846 | media: [ 847 | { 848 | type: 'image', 849 | url: 'https://i.pinimg.com/originals/9d/69/fd/9d69fd497059b8c9f3942806acda6bed.png', 850 | }, 851 | { 852 | type: 'image', 853 | url: 'https://res.infoq.com/news/2020/12/tailwind-css-v2/en/headerimage/header+(1)-1608368148194.jpg', 854 | }, 855 | ], 856 | timestamp: '2023-04-01T09:30:00Z', 857 | likes: [ 858 | { 859 | user: { 860 | username: 'reactdev', 861 | name: 'React Dev', 862 | avatar: 'https://avatar.com/reactdev', 863 | }, 864 | timestamp: '2023-04-01T10:00:00Z', 865 | }, 866 | { 867 | user: { 868 | username: 'tailwindlover', 869 | name: 'Tailwind Lover', 870 | avatar: 'https://avatar.com/tailwindlover', 871 | }, 872 | timestamp: '2023-04-01T11:00:00Z', 873 | }, 874 | ], 875 | comments: [ 876 | { 877 | user: { 878 | username: 'reactdev', 879 | name: 'React Dev', 880 | avatar: 'https://avatar.com/reactdev', 881 | }, 882 | content: "I know right? I started using it recently too and it's a game changer!", 883 | timestamp: '2023-04-01T10:30:00Z', 884 | }, 885 | { 886 | user: { 887 | username: 'tailwindlover', 888 | name: 'Tailwind Lover', 889 | avatar: 'https://avatar.com/tailwindlover', 890 | }, 891 | content: "Yes, I couldn't agree more. It has saved me so much time on styling.", 892 | timestamp: '2023-04-01T11:30:00Z', 893 | }, 894 | ], 895 | retweets: [ 896 | { 897 | user: { 898 | username: 'reactdev', 899 | name: 'React Dev', 900 | avatar: 'https://avatar.com/reactdev', 901 | }, 902 | timestamp: '2023-04-01T12:00:00Z', 903 | }, 904 | { 905 | user: { 906 | username: 'tailwindlover', 907 | name: 'Tailwind Lover', 908 | avatar: 'https://avatar.com/tailwindlover', 909 | }, 910 | timestamp: '2023-04-01T13:00:00Z', 911 | }, 912 | ], 913 | views: [ 914 | { 915 | user: { 916 | username: 'reactdev', 917 | name: 'React Dev', 918 | avatar: 'https://avatar.com/reactdev', 919 | }, 920 | timestamp: '2023-04-01T10:00:00Z', 921 | }, 922 | { 923 | user: { 924 | username: 'tailwindlover', 925 | name: 'Tailwind Lover', 926 | avatar: 'https://avatar.com/tailwindlover', 927 | }, 928 | timestamp: '2023-04-01T11:00:00Z', 929 | }, 930 | ], 931 | shares: [ 932 | { 933 | user: { 934 | username: 'reactdev', 935 | name: 'React Dev', 936 | avatar: 'https://avatar.com/reactdev', 937 | }, 938 | timestamp: '2023-04-01T12:30:00Z', 939 | }, 940 | ], 941 | }, 942 | { 943 | id: 8, 944 | title: 'Building a Node.js API with Express, Mongoose, and TypeScript', 945 | user: { 946 | username: 'code-with-saddam', 947 | name: 'Saddam Arbaa', 948 | avatar: 'https://avatars.githubusercontent.com/u/51326421?v=4', 949 | }, 950 | content: 951 | 'In this article, we will walk through the process of building a Node.js API using the popular Express framework, MongoDB with Mongoose, and TypeScript. We will cover everything from setting up the project, to defining the endpoints, to handling errors and validating data. Whether you are new to Node.js or an experienced developer, this guide will help you build a robust and scalable API with ease.', 952 | media: [ 953 | { 954 | type: 'image', 955 | url: 'https://codersera.com/blog/wp-content/uploads/2019/10/nodejs-thumb.jpg', 956 | }, 957 | { 958 | type: 'video', 959 | url: 'https://www.youtube.com/embed/Oe421EPjeBE?autoplay=1&mute=1', 960 | }, 961 | ], 962 | timestamp: '2023-04-01T09:30:00Z', 963 | likes: [ 964 | { 965 | user: { 966 | username: 'reactdev', 967 | name: 'React Dev', 968 | avatar: 'https://avatar.com/reactdev', 969 | }, 970 | timestamp: '2023-04-01T10:00:00Z', 971 | }, 972 | { 973 | user: { 974 | username: 'tailwindlover', 975 | name: 'Tailwind Lover', 976 | avatar: 'https://avatar.com/tailwindlover', 977 | }, 978 | timestamp: '2023-04-01T11:00:00Z', 979 | }, 980 | ], 981 | comments: [ 982 | { 983 | user: { 984 | username: 'reactdev', 985 | name: 'React Dev', 986 | avatar: 'https://avatar.com/reactdev', 987 | }, 988 | content: "I know right? I started using it recently too and it's a game changer!", 989 | timestamp: '2023-04-01T10:30:00Z', 990 | }, 991 | { 992 | user: { 993 | username: 'tailwindlover', 994 | name: 'Tailwind Lover', 995 | avatar: 'https://avatar.com/tailwindlover', 996 | }, 997 | content: "Yes, I couldn't agree more. It has saved me so much time on styling.", 998 | timestamp: '2023-04-01T11:30:00Z', 999 | }, 1000 | ], 1001 | retweets: [ 1002 | { 1003 | user: { 1004 | username: 'reactdev', 1005 | name: 'React Dev', 1006 | avatar: 'https://avatar.com/reactdev', 1007 | }, 1008 | timestamp: '2023-04-01T12:00:00Z', 1009 | }, 1010 | { 1011 | user: { 1012 | username: 'tailwindlover', 1013 | name: 'Tailwind Lover', 1014 | avatar: 'https://avatar.com/tailwindlover', 1015 | }, 1016 | timestamp: '2023-04-01T13:00:00Z', 1017 | }, 1018 | ], 1019 | views: [ 1020 | { 1021 | user: { 1022 | username: 'reactdev', 1023 | name: 'React Dev', 1024 | avatar: 'https://avatar.com/reactdev', 1025 | }, 1026 | timestamp: '2023-04-01T10:00:00Z', 1027 | }, 1028 | { 1029 | user: { 1030 | username: 'tailwindlover', 1031 | name: 'Tailwind Lover', 1032 | avatar: 'https://avatar.com/tailwindlover', 1033 | }, 1034 | timestamp: '2023-04-01T11:00:00Z', 1035 | }, 1036 | ], 1037 | shares: [ 1038 | { 1039 | user: { 1040 | username: 'reactdev', 1041 | name: 'React Dev', 1042 | avatar: 'https://avatar.com/reactdev', 1043 | }, 1044 | timestamp: '2023-04-01T12:30:00Z', 1045 | }, 1046 | ], 1047 | }, 1048 | { 1049 | id: 1, 1050 | title: 'React and TypeScript', 1051 | user: { 1052 | username: 'Tester now, aspiring Fullstack', 1053 | name: 'Naveen Kolambage', 1054 | avatar: 'https://pbs.twimg.com/profile_images/1560233396403507200/5hKYoyz1_400x400.jpg', 1055 | }, 1056 | content: 'Just tried out Vite with React and TypeScript and it is amazing! 🚀🔥 #reactjs #typescript #vite', 1057 | media: [ 1058 | { 1059 | type: 'image', 1060 | url: 'https://tse3.mm.bing.net/th?id=OIP.KJSD0dmEYMVYXqlRISNJUAHaD_&pid=Api&P=0', 1061 | }, 1062 | { 1063 | type: 'video', 1064 | url: 'https://www.youtube.com/embed/4UZrsTqkcW4?autoplay=1&mute=1', 1065 | }, 1066 | ], 1067 | timestamp: '2023-04-01T09:30:00Z', 1068 | likes: [ 1069 | { 1070 | user: { 1071 | username: 'reactdev', 1072 | name: 'React Dev', 1073 | avatar: 'https://avatar.com/reactdev', 1074 | }, 1075 | timestamp: '2023-04-01T10:00:00Z', 1076 | }, 1077 | { 1078 | user: { 1079 | username: 'tailwindlover', 1080 | name: 'Tailwind Lover', 1081 | avatar: 'https://avatar.com/tailwindlover', 1082 | }, 1083 | timestamp: '2023-04-01T11:00:00Z', 1084 | }, 1085 | ], 1086 | comments: [ 1087 | { 1088 | user: { 1089 | username: 'reactdev', 1090 | name: 'React Dev', 1091 | avatar: 'https://avatar.com/reactdev', 1092 | }, 1093 | content: "I know right? I started using it recently too and it's a game changer!", 1094 | timestamp: '2023-04-01T10:30:00Z', 1095 | }, 1096 | { 1097 | user: { 1098 | username: 'tailwindlover', 1099 | name: 'Tailwind Lover', 1100 | avatar: 'https://avatar.com/tailwindlover', 1101 | }, 1102 | content: "Yes, I couldn't agree more. It has saved me so much time on styling.", 1103 | timestamp: '2023-04-01T11:30:00Z', 1104 | }, 1105 | ], 1106 | retweets: [ 1107 | { 1108 | user: { 1109 | username: 'reactdev', 1110 | name: 'React Dev', 1111 | avatar: 'https://avatar.com/reactdev', 1112 | }, 1113 | timestamp: '2023-04-01T12:00:00Z', 1114 | }, 1115 | { 1116 | user: { 1117 | username: 'tailwindlover', 1118 | name: 'Tailwind Lover', 1119 | avatar: 'https://avatar.com/tailwindlover', 1120 | }, 1121 | timestamp: '2023-04-01T13:00:00Z', 1122 | }, 1123 | ], 1124 | views: [ 1125 | { 1126 | user: { 1127 | username: 'reactdev', 1128 | name: 'React Dev', 1129 | avatar: 'https://avatar.com/reactdev', 1130 | }, 1131 | timestamp: '2023-04-01T10:00:00Z', 1132 | }, 1133 | { 1134 | user: { 1135 | username: 'tailwindlover', 1136 | name: 'Tailwind Lover', 1137 | avatar: 'https://avatar.com/tailwindlover', 1138 | }, 1139 | timestamp: '2023-04-01T11:00:00Z', 1140 | }, 1141 | ], 1142 | shares: [ 1143 | { 1144 | user: { 1145 | username: 'reactdev', 1146 | name: 'React Dev', 1147 | avatar: 'https://avatar.com/reactdev', 1148 | }, 1149 | timestamp: '2023-04-01T12:30:00Z', 1150 | }, 1151 | ], 1152 | }, 1153 | { 1154 | id: 1, 1155 | title: 'Next.js 13: The Latest Features and Improvements', 1156 | user: { 1157 | username: 'Tester now, aspiring Fullstack', 1158 | name: 'Naveen Kolambage', 1159 | avatar: 'https://pbs.twimg.com/profile_images/1560233396403507200/5hKYoyz1_400x400.jpg', 1160 | }, 1161 | content: 'Just tried out Vite with React and TypeScript and it is amazing! 🚀🔥 #reactjs #typescript #vite', 1162 | media: [ 1163 | { 1164 | type: 'image', 1165 | url: 'https://i.ytimg.com/vi/XShQO3BvOyM/maxresdefault.jpg', 1166 | }, 1167 | ], 1168 | timestamp: '2023-04-01T09:30:00Z', 1169 | likes: [ 1170 | { 1171 | user: { 1172 | username: 'reactdev', 1173 | name: 'React Dev', 1174 | avatar: 'https://avatar.com/reactdev', 1175 | }, 1176 | timestamp: '2023-04-01T10:00:00Z', 1177 | }, 1178 | { 1179 | user: { 1180 | username: 'tailwindlover', 1181 | name: 'Tailwind Lover', 1182 | avatar: 'https://avatar.com/tailwindlover', 1183 | }, 1184 | timestamp: '2023-04-01T11:00:00Z', 1185 | }, 1186 | ], 1187 | comments: [ 1188 | { 1189 | user: { 1190 | username: 'reactdev', 1191 | name: 'React Dev', 1192 | avatar: 'https://avatar.com/reactdev', 1193 | }, 1194 | content: "I know right? I started using it recently too and it's a game changer!", 1195 | timestamp: '2023-04-01T10:30:00Z', 1196 | }, 1197 | { 1198 | user: { 1199 | username: 'tailwindlover', 1200 | name: 'Tailwind Lover', 1201 | avatar: 'https://avatar.com/tailwindlover', 1202 | }, 1203 | content: "Yes, I couldn't agree more. It has saved me so much time on styling.", 1204 | timestamp: '2023-04-01T11:30:00Z', 1205 | }, 1206 | ], 1207 | retweets: [ 1208 | { 1209 | user: { 1210 | username: 'reactdev', 1211 | name: 'React Dev', 1212 | avatar: 'https://avatar.com/reactdev', 1213 | }, 1214 | timestamp: '2023-04-01T12:00:00Z', 1215 | }, 1216 | { 1217 | user: { 1218 | username: 'tailwindlover', 1219 | name: 'Tailwind Lover', 1220 | avatar: 'https://avatar.com/tailwindlover', 1221 | }, 1222 | timestamp: '2023-04-01T13:00:00Z', 1223 | }, 1224 | ], 1225 | views: [ 1226 | { 1227 | user: { 1228 | username: 'reactdev', 1229 | name: 'React Dev', 1230 | avatar: 'https://avatar.com/reactdev', 1231 | }, 1232 | timestamp: '2023-04-01T10:00:00Z', 1233 | }, 1234 | { 1235 | user: { 1236 | username: 'tailwindlover', 1237 | name: 'Tailwind Lover', 1238 | avatar: 'https://avatar.com/tailwindlover', 1239 | }, 1240 | timestamp: '2023-04-01T11:00:00Z', 1241 | }, 1242 | ], 1243 | shares: [ 1244 | { 1245 | user: { 1246 | username: 'reactdev', 1247 | name: 'React Dev', 1248 | avatar: 'https://avatar.com/reactdev', 1249 | }, 1250 | timestamp: '2023-04-01T12:30:00Z', 1251 | }, 1252 | ], 1253 | }, 1254 | { 1255 | id: 1, 1256 | title: 'Next.js and GraphQL', 1257 | user: { 1258 | username: 'Saddam-dev', 1259 | name: 'Saddam Arbaa', 1260 | avatar: 'https://avatars.githubusercontent.com/u/51326421?v=4', 1261 | }, 1262 | content: 'Next.js and GraphQL! 🚀🔥 #reactjs #typescript #vite', 1263 | media: [ 1264 | { 1265 | type: 'image', 1266 | url: 'https://tse2.mm.bing.net/th?id=OIP.ZxDw0j3ANBxpatoCdNW8JQHaEK&pid=Api&P=0', 1267 | }, 1268 | ], 1269 | timestamp: '2023-04-01T09:30:00Z', 1270 | likes: [ 1271 | { 1272 | user: { 1273 | username: 'reactdev', 1274 | name: 'React Dev', 1275 | avatar: 'https://avatar.com/reactdev', 1276 | }, 1277 | timestamp: '2023-04-01T10:00:00Z', 1278 | }, 1279 | { 1280 | user: { 1281 | username: 'tailwindlover', 1282 | name: 'Tailwind Lover', 1283 | avatar: 'https://avatar.com/tailwindlover', 1284 | }, 1285 | timestamp: '2023-04-01T11:00:00Z', 1286 | }, 1287 | ], 1288 | comments: [ 1289 | { 1290 | user: { 1291 | username: 'reactdev', 1292 | name: 'React Dev', 1293 | avatar: 'https://avatar.com/reactdev', 1294 | }, 1295 | content: "I know right? I started using it recently too and it's a game changer!", 1296 | timestamp: '2023-04-01T10:30:00Z', 1297 | }, 1298 | { 1299 | user: { 1300 | username: 'tailwindlover', 1301 | name: 'Tailwind Lover', 1302 | avatar: 'https://avatar.com/tailwindlover', 1303 | }, 1304 | content: "Yes, I couldn't agree more. It has saved me so much time on styling.", 1305 | timestamp: '2023-04-01T11:30:00Z', 1306 | }, 1307 | ], 1308 | retweets: [ 1309 | { 1310 | user: { 1311 | username: 'reactdev', 1312 | name: 'React Dev', 1313 | avatar: 'https://avatar.com/reactdev', 1314 | }, 1315 | timestamp: '2023-04-01T12:00:00Z', 1316 | }, 1317 | { 1318 | user: { 1319 | username: 'tailwindlover', 1320 | name: 'Tailwind Lover', 1321 | avatar: 'https://avatar.com/tailwindlover', 1322 | }, 1323 | timestamp: '2023-04-01T13:00:00Z', 1324 | }, 1325 | ], 1326 | views: [ 1327 | { 1328 | user: { 1329 | username: 'reactdev', 1330 | name: 'React Dev', 1331 | avatar: 'https://avatar.com/reactdev', 1332 | }, 1333 | timestamp: '2023-04-01T10:00:00Z', 1334 | }, 1335 | { 1336 | user: { 1337 | username: 'tailwindlover', 1338 | name: 'Tailwind Lover', 1339 | avatar: 'https://avatar.com/tailwindlover', 1340 | }, 1341 | timestamp: '2023-04-01T11:00:00Z', 1342 | }, 1343 | ], 1344 | shares: [ 1345 | { 1346 | user: { 1347 | username: 'reactdev', 1348 | name: 'React Dev', 1349 | avatar: 'https://avatar.com/reactdev', 1350 | }, 1351 | timestamp: '2023-04-01T12:30:00Z', 1352 | }, 1353 | ], 1354 | }, 1355 | ]; 1356 | 1357 | export const mockedTrending = [ 1358 | { 1359 | title: 'Trending in Coding', 1360 | hasTag: '#coding', 1361 | tweets: '62.6K', 1362 | id: '8uu', 1363 | }, 1364 | { 1365 | title: 'Trending in Vanilla JS', 1366 | hasTag: '#JavaScript', 1367 | tweets: '806.6K Tweets', 1368 | id: '875', 1369 | }, 1370 | { 1371 | title: 'Trending in 100daysofcode', 1372 | hasTag: '#100daysofcode', 1373 | tweets: '12752.6K', 1374 | id: '88', 1375 | }, 1376 | { 1377 | title: 'COVID-19', 1378 | hasTag: '#News', 1379 | tweets: '127528.6K', 1380 | id: '888', 1381 | }, 1382 | ]; 1383 | 1384 | export const mockedSuggestUser = [ 1385 | { 1386 | id: 1, 1387 | username: 'elon-m', 1388 | name: 'Elon Musk', 1389 | avatar: 1390 | 'https://www.businessinsider.in/photo/77782500/elon-musk-is-now-worth-100-billion-half-of-jeff-bezos.jpg?imgsize=241963', 1391 | flowedMe: true, 1392 | }, 1393 | { 1394 | id: 1, 1395 | username: 'susan', 1396 | name: 'CEO of YouTube', 1397 | avatar: 1398 | 'http://static5.businessinsider.com/image/541b179c6da8116e1dbb3e12/https://variety.com/wp-content/uploads/2015/10/susan-wojcicki-power-of-women-youtube.jpg?w=1000', 1399 | flowedMe: true, 1400 | }, 1401 | { 1402 | id: 1, 1403 | username: 'Naveen', 1404 | name: 'Naveen Kolambage', 1405 | avatar: 'https://pbs.twimg.com/profile_images/1560233396403507200/5hKYoyz1_400x400.jpg', 1406 | flowedMe: true, 1407 | }, 1408 | ]; 1409 | --------------------------------------------------------------------------------