├── .eslintrc.json ├── public ├── favicon.ico └── vercel.svg ├── postcss.config.js ├── next.config.js ├── pages ├── api │ ├── hello.js │ └── auth │ │ └── [...nextauth].js ├── _app.js ├── index.js └── auth │ └── signin.js ├── atom ├── userAtom.js └── modalAtom.js ├── styles └── globals.css ├── tailwind.config.js ├── components ├── Story.js ├── Posts.js ├── Feed.js ├── MiniProfile.js ├── Stories.js ├── Suggestions.js ├── Header.js ├── UploadModal.js └── Post.js ├── .gitignore ├── package.json ├── firebase.js └── README.md /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sahandghavidel/insta-v4/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | reactStrictMode: true, 3 | images: { 4 | domains: ["www.jennexplores.com", "upload.wikimedia.org"], 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /pages/api/hello.js: -------------------------------------------------------------------------------- 1 | // Next.js API route support: https://nextjs.org/docs/api-routes/introduction 2 | 3 | export default function handler(req, res) { 4 | res.status(200).json({ name: 'John Doe' }) 5 | } 6 | -------------------------------------------------------------------------------- /atom/userAtom.js: -------------------------------------------------------------------------------- 1 | import { atom } from "recoil"; 2 | 3 | export const userState = atom({ 4 | key: "userState", // unique ID (with respect to other atoms/selectors) 5 | default: null, // default value (aka initial value) 6 | }); 7 | -------------------------------------------------------------------------------- /atom/modalAtom.js: -------------------------------------------------------------------------------- 1 | import { atom } from "recoil"; 2 | 3 | export const modalState = atom({ 4 | key: "modalState", // unique ID (with respect to other atoms/selectors) 5 | default: false, // default value (aka initial value) 6 | }); 7 | -------------------------------------------------------------------------------- /styles/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer components { 6 | .btn { 7 | @apply h-7 hover:scale-125 transition-transform duration-200 ease-out cursor-pointer 8 | } 9 | } -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | content: [ 3 | "./pages/**/*.{js,ts,jsx,tsx}", 4 | "./components/**/*.{js,ts,jsx,tsx}", 5 | ], 6 | theme: { 7 | extend: {}, 8 | }, 9 | plugins: [require('@tailwindcss/forms'), require('tailwind-scrollbar')], 10 | } -------------------------------------------------------------------------------- /pages/_app.js: -------------------------------------------------------------------------------- 1 | import "../styles/globals.css"; 2 | import { SessionProvider } from "next-auth/react"; 3 | import { RecoilRoot } from "recoil"; 4 | 5 | function MyApp({ Component, pageProps: { session, ...pageProps } }) { 6 | return ( 7 | 8 | 9 | 10 | 11 | 12 | ); 13 | } 14 | 15 | export default MyApp; 16 | -------------------------------------------------------------------------------- /components/Story.js: -------------------------------------------------------------------------------- 1 | import {PlusIcon} from "@heroicons/react/solid" 2 | export default function Story({img, username, isUser}) { 3 | return
4 | {username} 5 | {isUser && } 6 |

{username}

7 |
; 8 | } 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env.local 29 | .env.development.local 30 | .env.test.local 31 | .env.production.local 32 | 33 | # vercel 34 | .vercel 35 | -------------------------------------------------------------------------------- /pages/index.js: -------------------------------------------------------------------------------- 1 | import Head from "next/head"; 2 | import Feed from "../components/Feed"; 3 | import Header from "../components/Header"; 4 | import UploadModal from "../components/UploadModal"; 5 | 6 | export default function Home() { 7 | return ( 8 |
9 | 10 | Instagram App 11 | 12 | 13 | 14 | 15 | {/* Header */} 16 | 17 |
18 | 19 | {/* Feed */} 20 | 21 | 22 | 23 | {/* Modal */} 24 | 25 | 26 |
27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /pages/api/auth/[...nextauth].js: -------------------------------------------------------------------------------- 1 | import NextAuth from "next-auth"; 2 | import GoogleProvider from "next-auth/providers/google"; 3 | 4 | export default NextAuth({ 5 | // Configure one or more authentication providers 6 | providers: [ 7 | GoogleProvider({ 8 | clientId: process.env.GOOGLE_CLIENT_ID, 9 | clientSecret: process.env.GOOGLE_CLIENT_SECRET, 10 | }), 11 | // ...add more providers here 12 | ], 13 | secret: process.env.SECRET, 14 | pages: { 15 | signIn: "/auth/signin", 16 | }, 17 | 18 | callbacks: { 19 | async session({ session, token, user }) { 20 | session.user.username = session.user.name 21 | .split(" ") 22 | .join("") 23 | .toLocaleLowerCase(); 24 | session.user.uid = token.sub; 25 | return session; 26 | }, 27 | }, 28 | }); 29 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "insta-v4", 3 | "private": true, 4 | "scripts": { 5 | "dev": "next dev", 6 | "build": "next build", 7 | "start": "next start", 8 | "lint": "next lint" 9 | }, 10 | "dependencies": { 11 | "@heroicons/react": "^1.0.5", 12 | "@tailwindcss/forms": "^0.4.0", 13 | "firebase": "^9.6.6", 14 | "minifaker": "^1.34.0", 15 | "moment": "^2.29.1", 16 | "next": "12.0.9", 17 | "next-auth": "^4.2.1", 18 | "react": "17.0.2", 19 | "react-dom": "17.0.2", 20 | "react-modal": "^3.14.4", 21 | "react-moment": "^1.1.1", 22 | "recoil": "^0.6.1" 23 | }, 24 | "devDependencies": { 25 | "autoprefixer": "^10.4.2", 26 | "eslint": "8.8.0", 27 | "eslint-config-next": "12.0.9", 28 | "postcss": "^8.4.5", 29 | "tailwind-scrollbar": "^1.3.1", 30 | "tailwindcss": "^3.0.18" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /components/Posts.js: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import Post from "./Post"; 3 | import { collection, onSnapshot, orderBy, query } from "firebase/firestore"; 4 | import { db } from "../firebase"; 5 | 6 | export default function Posts() { 7 | const [posts, setPosts] = useState([]); 8 | useEffect(() => { 9 | const unsubscribe = onSnapshot( 10 | query(collection(db, "posts"), orderBy("timestamp", "desc")), 11 | (snapshot) => { 12 | setPosts(snapshot.docs); 13 | } 14 | ); 15 | return unsubscribe; 16 | }, [db]); 17 | return ( 18 |
19 | {posts.map((post) => ( 20 | 28 | ))} 29 |
30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /components/Feed.js: -------------------------------------------------------------------------------- 1 | import { useRecoilState } from "recoil"; 2 | import { userState } from "../atom/userAtom"; 3 | import MiniProfile from "./MiniProfile"; 4 | import Posts from "./Posts"; 5 | import Stories from "./Stories"; 6 | import Suggestions from "./Suggestions"; 7 | 8 | export default function Feed() { 9 | const [currentUser] = useRecoilState(userState) 10 | return ( 11 |
12 |
13 | {/* Stories */} 14 | 15 | 16 | {/* Posts */} 17 | 18 |
19 | 20 |
21 |
22 | {/* Mini Profile */} 23 | 24 | 25 | {/* Suggestions */} 26 | 27 | 28 |
29 |
30 |
31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /components/MiniProfile.js: -------------------------------------------------------------------------------- 1 | import { getAuth, signOut } from "firebase/auth"; 2 | import { useRecoilState } from "recoil"; 3 | import { userState } from "../atom/userAtom"; 4 | 5 | export default function MiniProfile() { 6 | const [currentUser, setCurrentUser] = useRecoilState(userState); 7 | const auth = getAuth(); 8 | function onSignOut() { 9 | signOut(auth); 10 | setCurrentUser(null); 11 | } 12 | return ( 13 |
14 | user-image 19 |
20 |

{currentUser?.username}

21 |

Welcome to instagram

22 |
23 | 29 |
30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /firebase.js: -------------------------------------------------------------------------------- 1 | // Import the functions you need from the SDKs you need 2 | import { initializeApp, getApp, getApps } from "firebase/app"; 3 | import { getFirestore } from "firebase/firestore"; 4 | import { getStorage } from "firebase/storage"; 5 | // TODO: Add SDKs for Firebase products that you want to use 6 | // https://firebase.google.com/docs/web/setup#available-libraries 7 | 8 | // Your web app's Firebase configuration 9 | const firebaseConfig = { 10 | apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY, 11 | authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN, 12 | projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID, 13 | storageBucket: process.env.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET, 14 | messagingSenderId: process.env.NEXT_PUBLIC_FIREBASE_MESSAGE_SENDER_ID, 15 | appId: process.env.NEXT_PUBLIC_FIREBASE_APP_ID, 16 | }; 17 | 18 | // Initialize Firebase 19 | const app = !getApps().length ? initializeApp(firebaseConfig) : getApp(); 20 | const db = getFirestore(); 21 | const storage = getStorage(); 22 | 23 | export { app, db, storage }; 24 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /components/Stories.js: -------------------------------------------------------------------------------- 1 | import minifaker from "minifaker"; 2 | import "minifaker/locales/en"; 3 | import { useEffect, useState } from "react"; 4 | import { useRecoilState } from "recoil"; 5 | import { userState } from "../atom/userAtom"; 6 | import Story from "./Story"; 7 | 8 | export default function Stories() { 9 | const [storyUsers, setSoryUsers] = useState([]); 10 | const [currentUser] = useRecoilState(userState) 11 | useEffect(() => { 12 | const storyUsers = minifaker.array(20, (i) => ({ 13 | username: minifaker.username({ locale: "en" }).toLowerCase(), 14 | img: `https://i.pravatar.cc/150?img=${Math.ceil(Math.random() * 70)}`, 15 | id: i, 16 | })); 17 | setSoryUsers(storyUsers); 18 | console.log(storyUsers); 19 | }, []); 20 | return ( 21 |
22 | {currentUser && ( 23 | 24 | )} 25 | {storyUsers.map((user) => ( 26 | 27 | ))} 28 |
29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /components/Suggestions.js: -------------------------------------------------------------------------------- 1 | import minifaker from "minifaker"; 2 | import "minifaker/locales/en"; 3 | import { useEffect, useState } from "react"; 4 | 5 | export default function Suggestions() { 6 | const [suggestions, setSuggestions] = useState([]); 7 | useEffect(() => { 8 | const suggestions = minifaker.array(5, (i) => ({ 9 | username: minifaker.username({ locale: "en" }).toLowerCase(), 10 | jobTitle: minifaker.jobTitle(), 11 | id: i, 12 | })); 13 | setSuggestions(suggestions); 14 | }, []); 15 | return ( 16 |
17 |
18 |

Suggestion for you

19 | 20 |
21 | {suggestions.map((suggestion) => ( 22 |
26 | 33 |
34 |

{suggestion.username}

35 |

36 | {suggestion.jobTitle} 37 |

38 |
39 | 42 |
43 | ))} 44 |
45 | ); 46 | } 47 | -------------------------------------------------------------------------------- /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 | ``` 12 | 13 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 14 | 15 | You can start editing the page by modifying `pages/index.js`. The page auto-updates as you edit the file. 16 | 17 | [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.js`. 18 | 19 | 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. 20 | 21 | ## Learn More 22 | 23 | To learn more about Next.js, take a look at the following resources: 24 | 25 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 26 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 27 | 28 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 29 | 30 | ## Deploy on Vercel 31 | 32 | 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. 33 | 34 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 35 | -------------------------------------------------------------------------------- /pages/auth/signin.js: -------------------------------------------------------------------------------- 1 | import Header from "../../components/Header"; 2 | import { getAuth, GoogleAuthProvider, signInWithPopup } from "firebase/auth"; 3 | import { db } from "../../firebase"; 4 | import { doc, getDoc, serverTimestamp, setDoc } from "firebase/firestore"; 5 | import { useRouter } from "next/router"; 6 | export default function Signin() { 7 | const router = useRouter(); 8 | async function onGoogleClick() { 9 | try { 10 | const auth = getAuth(); 11 | const provider = new GoogleAuthProvider(); 12 | await signInWithPopup(auth, provider); 13 | const user = auth.currentUser.providerData[0]; 14 | const docRef = doc(db, "users", user.uid); 15 | const docSnap = await getDoc(docRef); 16 | if (!docSnap.exists()) { 17 | await setDoc(docRef, { 18 | name: user.displayName, 19 | email: user.email, 20 | userImg: user.photoURL, 21 | uid: user.uid, 22 | timestamp: serverTimestamp(), 23 | username: user.displayName.split(" ").join("").toLocaleLowerCase(), 24 | }); 25 | } 26 | router.push("/"); 27 | } catch (error) { 28 | console.log(error); 29 | } 30 | } 31 | return ( 32 | <> 33 |
34 |
35 | instagram-image 40 |
41 |
42 | 47 |

48 | This app is created for learning purposes 49 |

50 | 56 |
57 |
58 |
59 | 60 | ); 61 | } 62 | -------------------------------------------------------------------------------- /components/Header.js: -------------------------------------------------------------------------------- 1 | import Image from "next/image"; 2 | import { useEffect } from "react"; 3 | import { SearchIcon, PlusCircleIcon } from "@heroicons/react/outline"; 4 | import { HomeIcon } from "@heroicons/react/solid"; 5 | import { useRecoilState } from "recoil"; 6 | import { modalState } from "../atom/modalAtom"; 7 | import { useRouter } from "next/router"; 8 | import { getAuth, onAuthStateChanged, signOut } from "firebase/auth"; 9 | import { doc, getDoc } from "firebase/firestore"; 10 | import { userState } from "../atom/userAtom"; 11 | import { db } from "../firebase"; 12 | export default function Header() { 13 | const [open, setOpen] = useRecoilState(modalState); 14 | const [currentUser, setCurrentUser] = useRecoilState(userState); 15 | const router = useRouter(); 16 | const auth = getAuth(); 17 | 18 | useEffect(() => { 19 | onAuthStateChanged(auth, (user) => { 20 | if (user) { 21 | const fetchUser = async () => { 22 | console.log(user); 23 | const docRef = doc( 24 | db, 25 | "users", 26 | user.auth.currentUser.providerData[0].uid 27 | ); 28 | const docSnap = await getDoc(docRef); 29 | if (docSnap.exists()) { 30 | setCurrentUser(docSnap.data()); 31 | } 32 | }; 33 | fetchUser(); 34 | } 35 | }); 36 | }, []); 37 | 38 | function onSignOut() { 39 | signOut(auth); 40 | setCurrentUser(null); 41 | } 42 | return ( 43 |
44 |
45 | {/* Left */} 46 |
47 | router.push("/")} 52 | /> 53 |
54 |
55 | router.push("/")} 60 | /> 61 |
62 | {/* Middle */} 63 | 64 |
65 |
66 | 67 |
68 | 73 |
74 | 75 | {/* Right */} 76 | 77 |
78 | router.push("/")} 80 | className="hidden md:inline-flex h-6 cursor-pointer hover:scale-125 transition-tranform duration-200 ease-out" 81 | /> 82 | {currentUser ? ( 83 | <> 84 | setOpen(true)} 86 | className="h-6 cursor-pointer hover:scale-125 transition-tranform duration-200 ease-out" 87 | /> 88 | user-image 94 | 95 | ) : ( 96 | 97 | )} 98 |
99 |
100 |
101 | ); 102 | } 103 | -------------------------------------------------------------------------------- /components/UploadModal.js: -------------------------------------------------------------------------------- 1 | import { useRecoilState } from "recoil"; 2 | import { modalState } from "../atom/modalAtom"; 3 | import Modal from "react-modal"; 4 | import { CameraIcon } from "@heroicons/react/outline"; 5 | import { useRef, useState } from "react"; 6 | import { 7 | addDoc, 8 | collection, 9 | doc, 10 | serverTimestamp, 11 | updateDoc, 12 | } from "firebase/firestore"; 13 | import { db, storage } from "../firebase"; 14 | import { getDownloadURL, ref, uploadString } from "firebase/storage"; 15 | import { userState } from "../atom/userAtom"; 16 | 17 | export default function UploadModal() { 18 | const [open, setOpen] = useRecoilState(modalState); 19 | const [selectedFile, setSelectedFile] = useState(null); 20 | const [loading, setLoading] = useState(false); 21 | const [currentUser] = useRecoilState(userState) 22 | async function uploadPost() { 23 | if (loading) return; 24 | 25 | setLoading(true); 26 | 27 | const docRef = await addDoc(collection(db, "posts"), { 28 | caption: captionRef.current.value, 29 | username: currentUser?.username, 30 | profileImg: currentUser.userImg, 31 | timestamp: serverTimestamp(), 32 | }); 33 | 34 | const imageRef = ref(storage, `posts/${docRef.id}/image`); 35 | await uploadString(imageRef, selectedFile, "data_url").then( 36 | async (snapshot) => { 37 | const downloadURL = await getDownloadURL(imageRef); 38 | await updateDoc(doc(db, "posts", docRef.id), { 39 | image: downloadURL, 40 | }); 41 | } 42 | ); 43 | setOpen(false); 44 | setLoading(false); 45 | setSelectedFile(null); 46 | } 47 | 48 | function addImageToPost(event) { 49 | const reader = new FileReader(); 50 | if (event.target.files[0]) { 51 | reader.readAsDataURL(event.target.files[0]); 52 | } 53 | 54 | reader.onload = (readerEvent) => { 55 | setSelectedFile(readerEvent.target.result); 56 | }; 57 | } 58 | const filePickerRef = useRef(null); 59 | const captionRef = useRef(null); 60 | return ( 61 |
62 | {open && ( 63 | { 67 | setOpen(false); 68 | setSelectedFile(null); 69 | }} 70 | > 71 |
72 | {selectedFile ? ( 73 | setSelectedFile(null)} 75 | src={selectedFile} 76 | alt="" 77 | className="w-full max-h-[250px] object-cover cursor-pointer" 78 | /> 79 | ) : ( 80 | filePickerRef.current.click()} 82 | className="cursor-pointer h-14 bg-red-200 p-2 rounded-full border-2 text-red-500" 83 | /> 84 | )} 85 | 86 | 92 | 99 | 106 |
107 |
108 | )} 109 |
110 | ); 111 | } 112 | -------------------------------------------------------------------------------- /components/Post.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import Moment from "react-moment"; 3 | import { db } from "../firebase"; 4 | import { 5 | DotsHorizontalIcon, 6 | HeartIcon, 7 | ChatIcon, 8 | BookmarkIcon, 9 | EmojiHappyIcon, 10 | } from "@heroicons/react/outline"; 11 | import { HeartIcon as HeartIconFilled } from "@heroicons/react/solid"; 12 | import { 13 | addDoc, 14 | collection, 15 | deleteDoc, 16 | doc, 17 | onSnapshot, 18 | orderBy, 19 | query, 20 | serverTimestamp, 21 | setDoc, 22 | } from "firebase/firestore"; 23 | import { useRecoilState } from "recoil"; 24 | import { userState } from "../atom/userAtom"; 25 | export default function Post({ img, userImg, caption, username, id }) { 26 | const [comment, setComment] = useState(""); 27 | const [comments, setComments] = useState([]); 28 | const [likes, setLikes] = useState([]); 29 | const [hasLiked, setHasLiked] = useState(false); 30 | const [currentUser] = useRecoilState(userState) 31 | useEffect(() => { 32 | const unsubscribe = onSnapshot( 33 | query( 34 | collection(db, "posts", id, "comments"), 35 | orderBy("timestamp", "desc") 36 | ), 37 | (snapshot) => { 38 | setComments(snapshot.docs); 39 | } 40 | ); 41 | }, [db, id]); 42 | useEffect(() => { 43 | const unsubscribe = onSnapshot( 44 | collection(db, "posts", id, "likes"), 45 | (snapshot) => setLikes(snapshot.docs) 46 | ); 47 | }, [db]); 48 | useEffect(() => { 49 | setHasLiked( 50 | likes.findIndex((like) => like.id === currentUser?.uid) !== -1 51 | ); 52 | }, [likes]); 53 | async function likePost() { 54 | if (hasLiked) { 55 | await deleteDoc(doc(db, "posts", id, "likes", currentUser?.uid)); 56 | } else { 57 | await setDoc(doc(db, "posts", id, "likes", currentUser?.uid), { 58 | username: currentUser?.username, 59 | }); 60 | } 61 | } 62 | async function sendComment(event) { 63 | event.preventDefault(); 64 | const commentToSend = comment; 65 | setComment(""); 66 | await addDoc(collection(db, "posts", id, "comments"), { 67 | comment: commentToSend, 68 | username: currentUser?.username, 69 | userImage: currentUser?.userImg, 70 | timestamp: serverTimestamp(), 71 | }); 72 | } 73 | return ( 74 |
75 | {/* Post Header */} 76 | 77 |
78 | {username} 83 |

{username}

84 | 85 |
86 | 87 | {/* Post Image */} 88 | 89 | 90 | 91 | {/* Post Buttons */} 92 | 93 | {currentUser && ( 94 |
95 |
96 | {hasLiked ? ( 97 | 101 | ) : ( 102 | 103 | )} 104 | 105 | 106 |
107 | 108 |
109 | )} 110 | 111 | {/* Post comments */} 112 | 113 |

114 | {likes.length > 0 && ( 115 |

{likes.length} likes

116 | )} 117 | {username} 118 | {caption} 119 |

120 | {comments.length > 0 && ( 121 |
122 | {comments.map((comment) => ( 123 |
127 | user-image 132 |

{comment.data().username}

133 |

{comment.data().comment}

134 | {comment.data().timestamp?.toDate()} 135 |
136 | ))} 137 |
138 | )} 139 | 140 | {/* Post input box */} 141 | {currentUser && ( 142 |
143 | 144 | setComment(event.target.value)} 147 | className="border-none flex-1 focus:ring-0" 148 | type="text" 149 | placeholder="Enter your comment..." 150 | /> 151 | 159 | 160 | )} 161 |
162 | ); 163 | } 164 | --------------------------------------------------------------------------------