├── .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 |
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 |
16 |
17 |
{user.login.username}
18 |
19 | @{user.login.username}
20 | {/* {flowedMe && Follows you } */}
21 |
22 |
23 |
24 |
25 | Follow
26 |
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 |
29 | Go back
30 |
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 |
Back to home
32 |
33 |
34 | {
39 | rest();
40 | }}
41 | >
42 | Try again
43 |
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 |
{
34 | setNumber((prev) => (prev > mockedTrending.length ? 3 : prev + 3));
35 | }}
36 | >
37 | {`${number < mockedTrending.length ? 'See More' : 'Show less'} `}
38 |
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 |
{
39 | setArticlesNumber((prev) => (articlesNumber > initialResult.length ? 3 : prev + 3));
40 | }}
41 | >
42 | {`${articlesNumber < initialResult.length ? 'See More' : 'Show less'} `}
43 |
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 |
28 | Log in
29 |
30 |
31 |
32 |
35 | Sign up
36 |
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 |
{
43 | setUsersNumber((prev) =>
44 | usersNumber > initialResult.length ? 3 : prev + 3,
45 | )
46 | }}>
47 | {`${usersNumber < initialResult.length ? 'See More' : 'Show less'} `}
48 |
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 |
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 |
50 |
51 |
52 |
53 |
54 |
{session?.user?.name}
55 |
56 |
57 |
58 |
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 |
121 | Tweet
122 |
123 |
124 | ) : null}
125 |
{' '}
126 | {/* Add a placeholder for the profile section */}
127 |
128 |
129 | {session ? (
130 |
133 |
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 |
68 | X
69 |
70 |
71 |
78 |
79 |
80 |
81 |
Join Twitter today
82 |
signIn('twitter')}
86 | isLoading={false}
87 | Icon={FaTwitter}
88 | >
89 | Sign Up with Twitter
90 |
91 |
92 |
signIn('google')}
96 | isLoading={false}
97 | Icon={FcGoogle}
98 | >
99 | Sign Up with Google
100 |
101 |
102 |
signIn('linkedin')}
106 | isLoading={false}
107 | Icon={GrLinkedin}
108 | >
109 | Sign Up with Linkedin
110 |
111 |
112 |
119 | Sign Up with Apple
120 |
121 |
122 |
signIn('github')}
126 | isLoading={false}
127 | Icon={FaGithub}
128 | >
129 | Sign Up with Github
130 |
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 |
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 |
74 | X
75 |
76 |
77 |
84 |
85 |
86 |
87 |
Sign in to Twitter
88 |
signIn('twitter')}
92 | isLoading={false}
93 | Icon={FaTwitter}
94 | >
95 | Sign In with Twitter
96 |
97 |
98 |
signIn('google')}
104 | >
105 | Sign In with Google
106 |
107 |
108 |
signIn('google')}
112 | isLoading={false}
113 | Icon={AiFillApple}
114 | >
115 | Sign In with Apple
116 |
117 |
118 |
signIn('linkedin')}
122 | isLoading={false}
123 | Icon={GrLinkedin}
124 | >
125 | Sign In with Linkedin
126 |
127 |
signIn('github')}
131 | isLoading={false}
132 | Icon={FaGithub}
133 | >
134 | Sign In with Github
135 |
136 |
137 |
138 |
139 |
140 |
or
141 |
142 |
143 |
144 |
145 |
146 | setEmail(e.target.value)}
152 | placeholder='Email'
153 | />
154 |
155 |
156 |
162 | Next
163 |
164 |
165 |
166 | Forgot password?
167 |
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 |
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 |
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 |
--------------------------------------------------------------------------------