├── LEARN.md ├── .eslintrc.json ├── public ├── favicon.ico └── vercel.svg ├── postcss.config.js ├── next.config.js ├── lib └── next-auth.d.ts ├── typings.d.ts ├── pages ├── api │ ├── hello.ts │ └── auth │ │ └── [...nextauth].ts ├── _app.tsx ├── profile │ └── [id].tsx ├── auth │ └── login.tsx └── index.tsx ├── tailwind.config.js ├── .env.example ├── icons ├── plus-circle.tsx └── ellipsis-vertical.tsx ├── .gitignore ├── .github └── dependabot.yml ├── tsconfig.json ├── components ├── Story.tsx ├── Skeleton │ ├── AvatarSkeleton.tsx │ └── Skeleton.tsx ├── MiniProfile.tsx ├── HeartTip.tsx ├── Feed.tsx ├── profilePage │ ├── ProfileFeed.tsx │ ├── MainProfile.tsx │ ├── CustomPosts.tsx │ ├── Tag.tsx │ ├── Intro.tsx │ └── ProfileHeader.tsx ├── Stories.tsx ├── Suggestions.tsx ├── Posts.tsx ├── SearchTip.tsx ├── modal │ └── PostModal.tsx ├── Header.tsx └── Post.tsx ├── hooks └── useSelectFile.tsx ├── firebase └── firebase.ts ├── package.json ├── styles └── globals.css └── README.md /LEARN.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SashenJayathilaka/Instagram-Clone/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | reactStrictMode: true, 4 | swcMinify: true, 5 | }; 6 | 7 | module.exports = nextConfig; 8 | -------------------------------------------------------------------------------- /lib/next-auth.d.ts: -------------------------------------------------------------------------------- 1 | import "next-auth"; 2 | 3 | declare module "next-auth" { 4 | interface Session { 5 | user: User; 6 | } 7 | 8 | interface User { 9 | uid: string; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /typings.d.ts: -------------------------------------------------------------------------------- 1 | export interface UserData { 2 | caption: string; 3 | company: string; 4 | image: string; 5 | profileImage: string; 6 | timestamp: Timestamp; 7 | userId: string; 8 | username: string; 9 | } 10 | 11 | export interface Timestamp { 12 | nanoseconds: number; 13 | seconds: number; 14 | } 15 | -------------------------------------------------------------------------------- /pages/api/hello.ts: -------------------------------------------------------------------------------- 1 | // Next.js API route support: https://nextjs.org/docs/api-routes/introduction 2 | import type { NextApiRequest, NextApiResponse } from 'next' 3 | 4 | type Data = { 5 | name: string 6 | } 7 | 8 | export default function handler( 9 | req: NextApiRequest, 10 | res: NextApiResponse 11 | ) { 12 | res.status(200).json({ name: 'John Doe' }) 13 | } 14 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: [ 4 | "./pages/**/*.{js,ts,jsx,tsx}", 5 | "./components/**/*.{js,ts,jsx,tsx}", 6 | ], 7 | theme: { 8 | extend: {}, 9 | }, 10 | plugins: [ 11 | require("@tailwindcss/forms"), 12 | require("tailwind-scrollbar"), 13 | require("tailwind-scrollbar-hide"), 14 | ], 15 | }; 16 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | GOOGLE_CLIENT_ID=value 2 | GOOGLE_CLIENT_SECRET=value 3 | NEXT_PUBLIC_SECRET=anything 4 | NEXTAUTH_URL=http://localhost:3000 5 | 6 | # Firebase 7 | NEXT_PUBLIC_FIREBASE_API_KEY=value 8 | NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN=value 9 | NEXT_PUBLIC_FIREBASE_PROJECT_ID=value 10 | NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET=value 11 | NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID=value 12 | NEXT_PUBLIC_FIREBASE_APP_ID=value 13 | NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID=value -------------------------------------------------------------------------------- /icons/plus-circle.tsx: -------------------------------------------------------------------------------- 1 | export default function Plus(): any { 2 | return ( 3 | 11 | 16 | 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import "../styles/globals.css"; 2 | import type { AppProps } from "next/app"; 3 | import { Session } from "next-auth"; 4 | import { SessionProvider } from "next-auth/react"; 5 | 6 | function MyApp({ 7 | Component, 8 | pageProps, 9 | }: AppProps<{ 10 | session: Session; 11 | }>) { 12 | return ( 13 | 14 | 15 | 16 | ); 17 | } 18 | 19 | export default MyApp; 20 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "npm" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | -------------------------------------------------------------------------------- /icons/ellipsis-vertical.tsx: -------------------------------------------------------------------------------- 1 | export default function Ellipsis(): any { 2 | return ( 3 | 12 | 17 | 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /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 | }, 18 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], 19 | "exclude": ["node_modules"] 20 | } 21 | -------------------------------------------------------------------------------- /components/Story.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | type StoryProps = { 4 | img: string; 5 | username: string; 6 | }; 7 | 8 | const Story: React.FC = ({ img, username }) => { 9 | return ( 10 |
11 | 19 |

{username}

20 |
21 | ); 22 | }; 23 | export default Story; 24 | -------------------------------------------------------------------------------- /hooks/useSelectFile.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | 3 | const useSelectFile = () => { 4 | const [selectedFile, setSelectedFile] = useState(); 5 | 6 | const onSelectedFile = (event: React.ChangeEvent) => { 7 | const reader = new FileReader(); 8 | 9 | if (event.target.files?.[0]) { 10 | reader.readAsDataURL(event.target.files[0]); 11 | } 12 | 13 | reader.onload = (readerEvent) => { 14 | if (readerEvent.target?.result) { 15 | setSelectedFile(readerEvent.target.result as string); 16 | } 17 | }; 18 | }; 19 | 20 | return { selectedFile, setSelectedFile, onSelectedFile }; 21 | }; 22 | export default useSelectFile; 23 | -------------------------------------------------------------------------------- /pages/api/auth/[...nextauth].ts: -------------------------------------------------------------------------------- 1 | import NextAuth from "next-auth"; 2 | import GoogleProvider from "next-auth/providers/google"; 3 | 4 | export default NextAuth({ 5 | providers: [ 6 | GoogleProvider({ 7 | clientId: process.env.GOOGLE_CLIENT_ID as string, 8 | clientSecret: process.env.GOOGLE_CLIENT_SECRET as string, 9 | }), 10 | // ...add more providers here 11 | ], 12 | 13 | callbacks: { 14 | async session({ session, token, user }: any) { 15 | session.user.username = session?.user?.name 16 | .split(" ") 17 | .join("") 18 | .toLocaleLowerCase(); 19 | 20 | session.user.uid = token.sub; 21 | return session; 22 | }, 23 | }, 24 | 25 | secret: process.env.NEXT_PUBLIC_SECRET, 26 | }); 27 | -------------------------------------------------------------------------------- /components/Skeleton/AvatarSkeleton.tsx: -------------------------------------------------------------------------------- 1 | import { CardHeader, Skeleton, Avatar, IconButton } from "@mui/material"; 2 | import React from "react"; 3 | 4 | type Props = {}; 5 | 6 | function AvatarSkeleton({}: Props) { 7 | const loading = true; 8 | 9 | return ( 10 | 18 | ) : ( 19 | 23 | ) 24 | } 25 | /> 26 | ); 27 | } 28 | 29 | export default AvatarSkeleton; 30 | -------------------------------------------------------------------------------- /components/MiniProfile.tsx: -------------------------------------------------------------------------------- 1 | import { signOut, useSession } from "next-auth/react"; 2 | import React from "react"; 3 | 4 | type MiniProfileProps = {}; 5 | 6 | const MiniProfile: React.FC = () => { 7 | const { data: session } = useSession(); 8 | 9 | return ( 10 |
11 | 16 |
17 |

{session?.user?.name}

18 |

Welcome to Instagram

19 |
20 | 26 |
27 | ); 28 | }; 29 | export default MiniProfile; 30 | -------------------------------------------------------------------------------- /components/HeartTip.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | type HeartIconProps = {}; 4 | 5 | const HeartTip: React.FC = () => { 6 | return ( 7 |
8 |
9 | 15 | 20 | 21 | 22 |
1
23 |
24 |
25 | ); 26 | }; 27 | export default HeartTip; 28 | -------------------------------------------------------------------------------- /components/Feed.tsx: -------------------------------------------------------------------------------- 1 | import { Session } from "next-auth"; 2 | import React from "react"; 3 | 4 | import MiniProfile from "./MiniProfile"; 5 | import Posts from "./Posts"; 6 | import Stories from "./Stories"; 7 | import Suggestions from "./Suggestions"; 8 | 9 | type FeedProps = { 10 | session: Session; 11 | }; 12 | 13 | const Feed: React.FC = ({ session }) => { 14 | return ( 15 |
19 |
20 | 21 | 22 |
23 | 24 | {session && ( 25 |
26 |
27 | 28 | 29 |
30 |
31 | )} 32 |
33 | ); 34 | }; 35 | export default Feed; 36 | -------------------------------------------------------------------------------- /firebase/firebase.ts: -------------------------------------------------------------------------------- 1 | import { initializeApp, getApp, getApps } from "firebase/app"; 2 | import { getAuth } from "firebase/auth"; 3 | import { getFirestore } from "firebase/firestore"; 4 | import { getStorage } from "firebase/storage"; 5 | 6 | // Your web app's Firebase configuration 7 | // For Firebase JS SDK v7.20.0 and later, measurementId is optional 8 | const firebaseConfig = { 9 | apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY, 10 | authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN, 11 | projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID, 12 | storageBucket: process.env.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET, 13 | messagingSenderId: process.env.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID, 14 | appId: process.env.NEXT_PUBLIC_FIREBASE_APP_ID, 15 | }; 16 | // Initialize Firebase 17 | const app = !getApps().length ? initializeApp(firebaseConfig) : getApp(); 18 | const firestore = getFirestore(app); 19 | const auth = getAuth(app); 20 | const storage = getStorage(app); 21 | 22 | // export 23 | export { app, auth, firestore, storage }; 24 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "project", 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 | "@emotion/react": "^11.10.6", 13 | "@emotion/styled": "^11.10.4", 14 | "@faker-js/faker": "^7.6.0", 15 | "@mui/icons-material": "^5.10.16", 16 | "@mui/material": "^5.11.0", 17 | "@tailwindcss/forms": "^0.5.3", 18 | "firebase": "^9.18.0", 19 | "framer-motion": "^7.6.12", 20 | "moment": "^2.29.4", 21 | "next": "13.0.6", 22 | "next-auth": "^4.20.1", 23 | "react": "18.2.0", 24 | "react-dom": "18.2.0", 25 | "tailwind-scrollbar": "^2.1.0-preview.0", 26 | "tailwind-scrollbar-hide": "^1.1.7" 27 | }, 28 | "devDependencies": { 29 | "@types/node": "18.15.3", 30 | "@types/react": "18.0.26", 31 | "@types/react-dom": "18.0.9", 32 | "autoprefixer": "^10.4.13", 33 | "eslint": "8.29.0", 34 | "eslint-config-next": "13.1.1", 35 | "postcss": "^8.4.20", 36 | "tailwindcss": "^3.2.4", 37 | "typescript": "5.0.2" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /components/profilePage/ProfileFeed.tsx: -------------------------------------------------------------------------------- 1 | import { onSnapshot, query, collection, orderBy } from "firebase/firestore"; 2 | import React, { useEffect, useState } from "react"; 3 | 4 | import { firestore } from "../../firebase/firebase"; 5 | import CustomPosts from "./CustomPosts"; 6 | 7 | type ProfileFeedProps = { 8 | userId: string; 9 | setUserData: Function; 10 | }; 11 | 12 | const ProfileFeed: React.FC = ({ userId, setUserData }) => { 13 | const [posts, setPosts] = useState([]); 14 | 15 | useEffect( 16 | () => 17 | onSnapshot( 18 | query(collection(firestore, "posts"), orderBy("timestamp", "desc")), 19 | (snapshot) => { 20 | setPosts(snapshot.docs); 21 | setUserData(snapshot.docs); 22 | } 23 | ), 24 | [firestore] 25 | ); 26 | 27 | return ( 28 |
29 | {posts.map((post) => ( 30 | 37 | ))} 38 |
39 | ); 40 | }; 41 | export default ProfileFeed; 42 | -------------------------------------------------------------------------------- /components/Stories.tsx: -------------------------------------------------------------------------------- 1 | import { faker } from "@faker-js/faker"; 2 | import { useSession } from "next-auth/react"; 3 | import React, { useEffect, useState } from "react"; 4 | 5 | import Story from "./Story"; 6 | 7 | type StoriesProps = {}; 8 | 9 | const Stories: React.FC = () => { 10 | const { data: session } = useSession(); 11 | const [suggestions, setSuggestions] = useState([]); 12 | 13 | useEffect(() => { 14 | const suggestions = [...Array(20)].map((_, i) => ({ 15 | userId: faker.datatype.uuid(), 16 | username: faker.internet.userName(), 17 | avatar: faker.image.avatar(), 18 | id: i, 19 | })); 20 | setSuggestions(suggestions); 21 | }, []); 22 | 23 | return ( 24 |
28 | {session && ( 29 | 30 | )} 31 | 32 | {suggestions.map((profile) => ( 33 | 38 | ))} 39 |
40 | ); 41 | }; 42 | export default Stories; 43 | -------------------------------------------------------------------------------- /pages/profile/[id].tsx: -------------------------------------------------------------------------------- 1 | import Head from "next/head"; 2 | import React, { useEffect } from "react"; 3 | import { motion } from "framer-motion"; 4 | import { getSession } from "next-auth/react"; 5 | import { useRouter } from "next/router"; 6 | 7 | import Header from "../../components/Header"; 8 | import MainProfile from "../../components/profilePage/MainProfile"; 9 | 10 | type ProfilePageProps = { 11 | session: any; 12 | }; 13 | 14 | const ProfilePage: React.FC = ({ session }) => { 15 | const router = useRouter(); 16 | 17 | useEffect(() => { 18 | if (!session) { 19 | router.push("/"); 20 | } else return; 21 | }, [session]); 22 | 23 | return ( 24 | 29 | 30 | Instagram Clone 31 | 32 | 36 | 37 |
38 | 39 | 40 | ); 41 | }; 42 | export default ProfilePage; 43 | 44 | export async function getServerSideProps(context: any) { 45 | const session = await getSession(context); 46 | 47 | return { 48 | props: { 49 | session: session, 50 | }, 51 | }; 52 | } 53 | -------------------------------------------------------------------------------- /components/Suggestions.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import { faker } from "@faker-js/faker"; 3 | 4 | type SuggestionsProps = {}; 5 | 6 | const Suggestions: React.FC = () => { 7 | const [suggestions, setSuggestions] = useState([]); 8 | 9 | useEffect(() => { 10 | const suggestions = [...Array(5)].map((_, i) => ({ 11 | userId: faker.datatype.uuid(), 12 | username: faker.internet.userName(), 13 | avatar: faker.image.avatar(), 14 | id: i, 15 | })); 16 | setSuggestions(suggestions); 17 | //console.log(suggestions); 18 | }, []); 19 | 20 | return ( 21 |
22 |
23 |

Suggestions for you

24 | 25 |
26 | {suggestions.map((profile) => ( 27 |
28 | 34 |
35 |

{profile.username}

36 |

{profile.email}

37 |
38 | 39 |
40 | ))} 41 |
42 | ); 43 | }; 44 | export default Suggestions; 45 | -------------------------------------------------------------------------------- /components/profilePage/MainProfile.tsx: -------------------------------------------------------------------------------- 1 | import { useSession } from "next-auth/react"; 2 | import { useRouter } from "next/router"; 3 | import { useEffect, useState } from "react"; 4 | 5 | import { UserData } from "../../typings"; 6 | import Intro from "./Intro"; 7 | import ProfileFeed from "./ProfileFeed"; 8 | import ProfileHeader from "./ProfileHeader"; 9 | import Tag from "./Tag"; 10 | 11 | type Props = {}; 12 | 13 | export default function MainProfile({}: Props) { 14 | const { data: session } = useSession(); 15 | const router = useRouter(); 16 | const { userId } = router.query; 17 | const [userData, setUserData] = useState({}); 18 | const [userDetails, setUserDetails] = useState([]); 19 | const [isShow, setIsShow] = useState(false); 20 | 21 | const filterUserData = () => { 22 | try { 23 | userDetails.map((data) => { 24 | if (data.data().userId === userId) { 25 | setUserData(data.data()); 26 | 27 | if (data.data().username === session?.user?.name) { 28 | setIsShow(true); 29 | } 30 | } 31 | }); 32 | } catch (error) { 33 | alert(error); 34 | } 35 | }; 36 | 37 | useEffect(() => { 38 | filterUserData(); 39 | }, [userDetails]); 40 | 41 | return ( 42 |
43 |
44 | 45 | 46 |
47 | 48 | 49 |
50 |
51 |
52 | ); 53 | } 54 | -------------------------------------------------------------------------------- /pages/auth/login.tsx: -------------------------------------------------------------------------------- 1 | import { motion } from "framer-motion"; 2 | import { signIn } from "next-auth/react"; 3 | import Head from "next/head"; 4 | import React from "react"; 5 | 6 | import Header from "../../components/Header"; 7 | 8 | type LoginProps = {}; 9 | 10 | const Login: React.FC = () => { 11 | return ( 12 | 18 | 19 | Instagram Clone Sign in 20 | 21 | 25 | 26 |
27 |
28 | 33 |

34 | This is not a Real app, it is build for educational purpose only 35 |

36 |
37 |
38 | 44 |
45 |
46 |
47 | 48 | ); 49 | }; 50 | export default Login; 51 | -------------------------------------------------------------------------------- /components/Posts.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import Post from "./Post"; 3 | import { onSnapshot, collection, query, orderBy } from "firebase/firestore"; 4 | import { firestore } from "../firebase/firebase"; 5 | 6 | import PostSkeleton from "./Skeleton/Skeleton"; 7 | 8 | type PostsProps = {}; 9 | 10 | const Posts: React.FC = () => { 11 | const [posts, setPosts] = useState([]); 12 | const [loading, setLoading] = useState(false); 13 | 14 | const fetchPost = () => { 15 | try { 16 | setLoading(false); 17 | 18 | const fetchQuery = onSnapshot( 19 | query(collection(firestore, "posts"), orderBy("timestamp", "desc")), 20 | (snapshot) => { 21 | setPosts(snapshot.docs); 22 | } 23 | ); 24 | 25 | fetchQuery; 26 | 27 | setTimeout(() => { 28 | setLoading(true); 29 | }, 1000); 30 | } catch (error: any) { 31 | console.log(error.message); 32 | } 33 | }; 34 | 35 | useEffect(() => { 36 | fetchPost(); 37 | }, [firestore]); 38 | 39 | return ( 40 |
41 | {loading ? ( 42 | <> 43 | {posts.map((post) => ( 44 | 53 | ))} 54 | 55 | ) : ( 56 | <> 57 | {[...Array(5)].map((_, i) => ( 58 | 59 | ))} 60 | 61 | )} 62 |
63 | ); 64 | }; 65 | export default Posts; 66 | -------------------------------------------------------------------------------- /styles/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | /* target button */ 6 | @layer components { 7 | .navBtn { 8 | @apply hidden h-6 md:inline-flex 9 | cursor-pointer hover:scale-125 10 | transition-all duration-150 ease-out; 11 | } 12 | .btn { 13 | @apply h-7 hover:scale-125 cursor-pointer transition-all duration-150 ease-out; 14 | } 15 | } 16 | 17 | .pb-full { 18 | padding-bottom: 100%; 19 | } 20 | 21 | .bioclass { 22 | color: #8e8e8e; 23 | } 24 | 25 | /* hide search icon on search focus */ 26 | .search-bar:focus + .fa-search { 27 | display: none; 28 | } 29 | 30 | @media screen and (min-width: 768px) { 31 | .post:hover .overlay { 32 | display: block; 33 | } 34 | } 35 | 36 | .heart_tip::before { 37 | position: absolute; 38 | content: ""; 39 | border: 15px solid transparent; 40 | border-bottom-color: #ef4444; 41 | left: 50%; 42 | transform: translate(-50%, -130%); 43 | } 44 | 45 | .heart_tip, 46 | .heart_tip::before { 47 | filter: drop-shadow(0px 0px 5px rgba(0, 0, 0, 0.2)); 48 | } 49 | 50 | .heart_tip::after { 51 | height: 30%; 52 | width: 40%; 53 | background-color: #ef4444; 54 | position: absolute; 55 | top: 0; 56 | left: 50%; 57 | transform: translate(-50%); 58 | content: ""; 59 | } 60 | 61 | .search-tip::before { 62 | position: absolute; 63 | content: ""; 64 | border: 20px solid transparent; 65 | border-bottom-color: #fff; 66 | left: 50%; 67 | transform: translate(-50%, -130%); 68 | } 69 | 70 | .search-tip, 71 | .search-tip::before { 72 | filter: drop-shadow(0px 0px 5px rgba(0, 0, 0, 0.2)); 73 | } 74 | 75 | .search-tip::after { 76 | height: 15%; 77 | width: 40%; 78 | background-color: #fff; 79 | position: absolute; 80 | top: 0; 81 | left: 50%; 82 | transform: translate(-50%); 83 | content: ""; 84 | } 85 | -------------------------------------------------------------------------------- /components/SearchTip.tsx: -------------------------------------------------------------------------------- 1 | import { faker } from "@faker-js/faker"; 2 | import React from "react"; 3 | 4 | type SearchTipProps = { 5 | searchValue: string; 6 | }; 7 | 8 | const SearchTip: React.FC = ({ searchValue }) => { 9 | return ( 10 |
11 |
12 |

Recent

13 | 14 |
15 | 16 |
17 |
18 |
19 |
20 | {faker.name.firstName()} 25 |
26 |
27 |
28 |
29 | {faker.name.firstName()} 30 |
31 |
{faker.company.bs()}
32 |
33 | 39 | 44 | 45 |
46 |
47 |
48 | ); 49 | }; 50 | export default SearchTip; 51 | -------------------------------------------------------------------------------- /pages/index.tsx: -------------------------------------------------------------------------------- 1 | import { doc, getDoc, setDoc } from "firebase/firestore"; 2 | import { motion } from "framer-motion"; 3 | import { getSession } from "next-auth/react"; 4 | import Head from "next/head"; 5 | import { useEffect, useState } from "react"; 6 | 7 | import { Session } from "next-auth"; 8 | import Feed from "../components/Feed"; 9 | import Header from "../components/Header"; 10 | import { firestore } from "../firebase/firebase"; 11 | 12 | type Props = { 13 | session: Session; 14 | }; 15 | 16 | const Home = ({ session }: Props) => { 17 | const [userCreates, setUserCreate] = useState(false); 18 | 19 | const getUserData = async () => { 20 | if (session) { 21 | try { 22 | const docRef = doc(firestore, "users", session?.user?.uid); 23 | const docSnap = await getDoc(docRef); 24 | 25 | if (docSnap.exists()) { 26 | console.log("User Already Created"); 27 | setUserCreate(false); 28 | } else { 29 | setUserCreate(true); 30 | } 31 | } catch (error) { 32 | console.log(error); 33 | } 34 | } else return; 35 | }; 36 | 37 | const userCreate = async (session: Session) => { 38 | const userDocRef = doc(firestore, "users", session?.user?.uid); 39 | await setDoc(userDocRef, JSON.parse(JSON.stringify(session))); 40 | }; 41 | 42 | useEffect(() => { 43 | getUserData(); 44 | 45 | if (userCreates) { 46 | userCreate(session); 47 | } else return; 48 | }, [session, firestore, userCreates]); 49 | 50 | return ( 51 | 56 | 57 | Instagram Clone 58 | 59 | 63 | 64 |
65 | 66 | 67 | ); 68 | }; 69 | 70 | export default Home; 71 | 72 | export async function getServerSideProps(context: any) { 73 | const session = await getSession(context); 74 | 75 | return { 76 | props: { 77 | session: session, 78 | }, 79 | }; 80 | } 81 | -------------------------------------------------------------------------------- /components/profilePage/CustomPosts.tsx: -------------------------------------------------------------------------------- 1 | import { onSnapshot, collection } from "firebase/firestore"; 2 | import React, { useEffect, useState } from "react"; 3 | import { motion } from "framer-motion"; 4 | 5 | import { firestore } from "../../firebase/firebase"; 6 | 7 | type CustomPostsProps = { 8 | img: string; 9 | userId: string; 10 | userDBId: string; 11 | id: string; 12 | }; 13 | 14 | const CustomPosts: React.FC = ({ 15 | img, 16 | userId, 17 | userDBId, 18 | id, 19 | }) => { 20 | const [likes, setLikes] = useState([]); 21 | 22 | useEffect( 23 | () => 24 | onSnapshot(collection(firestore, "posts", id, "likes"), (snapshot) => 25 | setLikes(snapshot.docs) 26 | ), 27 | [firestore, id] 28 | ); 29 | return ( 30 | <> 31 | {userDBId === userId && ( 32 | 38 |
39 | image 44 | 45 |
46 |
47 | 48 | 54 | 55 | 56 |

{likes.length}

57 |
58 |
59 |
60 |
61 |
62 | )} 63 | 64 | ); 65 | }; 66 | export default CustomPosts; 67 | -------------------------------------------------------------------------------- /components/profilePage/Tag.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | type TagProps = {}; 4 | 5 | const Tag: React.FC = () => { 6 | return ( 7 |
8 |
9 |
10 | 14 |
15 |

Fun

16 |
17 | 18 |
19 |
20 | 24 |
25 |

Travel

26 |
27 | 28 |
29 |
30 | 34 |
35 |

Food

36 |
37 | 38 |
39 |
40 | 44 |
45 |

Hobby

46 |
47 | 48 |
49 |
50 | 54 |
55 |

My Work

56 |
57 |
58 | ); 59 | }; 60 | export default Tag; 61 | -------------------------------------------------------------------------------- /components/Skeleton/Skeleton.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Card from "@mui/material/Card"; 3 | import CardHeader from "@mui/material/CardHeader"; 4 | import CardContent from "@mui/material/CardContent"; 5 | import CardMedia from "@mui/material/CardMedia"; 6 | import Avatar from "@mui/material/Avatar"; 7 | import Typography from "@mui/material/Typography"; 8 | import IconButton from "@mui/material/IconButton"; 9 | import MoreVertIcon from "@mui/icons-material/MoreVert"; 10 | import Skeleton from "@mui/material/Skeleton"; 11 | 12 | type Props = { 13 | loading: boolean; 14 | }; 15 | 16 | function PostSkeleton({ loading }: Props) { 17 | return ( 18 |
19 | 20 | 29 | ) : ( 30 | 34 | ) 35 | } 36 | action={ 37 | loading ? null : ( 38 | 39 | 40 | 41 | ) 42 | } 43 | title={ 44 | loading ? ( 45 | 51 | ) : ( 52 | "Ted" 53 | ) 54 | } 55 | subheader={ 56 | loading ? ( 57 | 58 | ) : ( 59 | "5 hours ago" 60 | ) 61 | } 62 | /> 63 | {loading ? ( 64 | 69 | ) : ( 70 | 76 | )} 77 | 78 | {loading ? ( 79 | 80 | 85 | 86 | 87 | ) : ( 88 | 89 | { 90 | "Why First Minister of Scotland Nicola Sturgeon thinks GDP is the wrong measure of a country's success:" 91 | } 92 | 93 | )} 94 | 95 | 96 |
97 | ); 98 | } 99 | 100 | export default PostSkeleton; 101 | -------------------------------------------------------------------------------- /components/profilePage/Intro.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | type IntroProps = {}; 4 | 5 | const Intro: React.FC = () => { 6 | return ( 7 | <> 8 |
    9 |
    10 |
    11 |
    12 | 25 |
    26 |
    27 |

    POSTS

    28 |
    29 |
    30 | 31 |
    32 |
    33 | 46 |
    47 |
    48 |

    IGTV

    49 |
    50 |
    51 | 52 |
    53 |
    54 | 67 |
    68 |
    69 |

    SAVED

    70 |
    71 |
    72 | 73 |
    74 |
    75 | 88 |
    89 |
    90 |

    TAGGED

    91 |
    92 |
    93 |
    94 |
95 | 96 | ); 97 | }; 98 | export default Intro; 99 | -------------------------------------------------------------------------------- /components/profilePage/ProfileHeader.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { faker } from "@faker-js/faker"; 3 | import AvatarSkeleton from "../Skeleton/AvatarSkeleton"; 4 | import { motion } from "framer-motion"; 5 | 6 | import { UserData } from "../../typings"; 7 | 8 | type ProfileHeaderProps = { 9 | isShow: boolean; 10 | userData: UserData; 11 | }; 12 | 13 | const ProfileHeader: React.FC = ({ isShow, userData }) => { 14 | return ( 15 |
16 |
17 | {userData.profileImage ? ( 18 | 23 | profile 28 | 29 | ) : ( 30 | 35 | 36 | 37 | )} 38 |
39 |
40 |
41 |

42 | {userData.username} 43 |

44 | 45 | 51 | 52 |
53 | Follow 54 |
55 | {isShow && ( 56 | 57 | 74 | 75 | )} 76 |
77 | 78 |
    79 |
  • 80 | {faker.random.numeric()} 81 | posts 82 |
  • 83 | 84 |
  • 85 | {faker.random.numeric()}K 86 | followers 87 |
  • 88 |
  • 89 | {faker.random.numeric()} 90 | following 91 |
  • 92 |
93 | 94 |
95 |

{userData.company}

96 | {faker.company.name()} 97 |

{faker.commerce.productDescription()}

98 | 99 | {faker.internet.domainName()} 100 | 101 |
102 |
103 | 104 |
105 |

ByteWebster

106 | Internet company 107 |

108 | ByteWebster is a web development and coding blog website. Where we 109 | provide professional web projects🌍 110 |

111 | 112 | www.bytewebster.com 113 | 114 |
115 |
116 | ); 117 | }; 118 | export default ProfileHeader; 119 | -------------------------------------------------------------------------------- /components/modal/PostModal.tsx: -------------------------------------------------------------------------------- 1 | import CameraAltIcon from "@mui/icons-material/CameraAlt"; 2 | import LoopIcon from "@mui/icons-material/Loop"; 3 | import Box from "@mui/material/Box"; 4 | import Modal from "@mui/material/Modal"; 5 | import Typography from "@mui/material/Typography"; 6 | import { 7 | addDoc, 8 | collection, 9 | doc, 10 | serverTimestamp, 11 | Timestamp, 12 | updateDoc, 13 | } from "firebase/firestore"; 14 | import { getDownloadURL, ref, uploadString } from "firebase/storage"; 15 | import { useSession } from "next-auth/react"; 16 | import React, { useRef, useState } from "react"; 17 | 18 | import { firestore, storage } from "../../firebase/firebase"; 19 | import useSelectFile from "../../hooks/useSelectFile"; 20 | 21 | const style = { 22 | position: "absolute" as "absolute", 23 | top: "50%", 24 | left: "50%", 25 | transform: "translate(-50%, -50%)", 26 | width: 400, 27 | bgcolor: "background.paper", 28 | boxShadow: 24, 29 | p: 0, 30 | }; 31 | 32 | type PostModalProps = { 33 | open: boolean; 34 | setOpen: any; 35 | }; 36 | 37 | const PostModal: React.FC = ({ open, setOpen }) => { 38 | const { data: session } = useSession(); 39 | const { selectedFile, setSelectedFile, onSelectedFile } = useSelectFile(); 40 | const selectedFileRef = useRef(null); 41 | const [caption, setCaption] = useState(""); 42 | const [loading, setLoading] = useState(false); 43 | const handleOpen = () => setOpen(true); 44 | const handleClose = () => setOpen(false); 45 | 46 | const handleCreateCommunity = async () => { 47 | setLoading(true); 48 | try { 49 | const docRef = await addDoc(collection(firestore, "posts"), { 50 | userId: session?.user?.uid, 51 | username: session?.user?.name, 52 | caption: caption, 53 | profileImage: session?.user?.image, 54 | company: session?.user?.email, 55 | timestamp: serverTimestamp() as Timestamp, 56 | }); 57 | 58 | if (selectedFile) { 59 | const imageRef = ref(storage, `posts/${docRef.id}/image`); 60 | 61 | await uploadString(imageRef, selectedFile as string, "data_url").then( 62 | async (snapshot) => { 63 | const downloadUrl = await getDownloadURL(imageRef); 64 | await updateDoc(doc(firestore, "posts", docRef.id), { 65 | image: downloadUrl, 66 | }); 67 | } 68 | ); 69 | } else { 70 | console.log("No Image"); 71 | } 72 | } catch (error) { 73 | console.log(error); 74 | } 75 | setSelectedFile(""); 76 | setCaption(""); 77 | setLoading(false); 78 | handleClose(); 79 | }; 80 | 81 | return ( 82 |
83 | 89 | 90 |
91 |
98 |
99 | {selectedFile ? ( 100 | setSelectedFile("")} 104 | alt="" 105 | /> 106 | ) : ( 107 |
selectedFileRef.current?.click()} 111 | > 112 |
117 | )} 118 | 119 |
120 |
121 | {!selectedFile && ( 122 | 123 | Upload a Photo 124 | 125 | )} 126 |
127 | 133 |
134 |
135 | setCaption(e.target.value)} 142 | /> 143 |
144 |
145 |
146 |
147 | {loading ? ( 148 | 159 | ) : ( 160 | 172 | )} 173 |
174 |
175 |
176 |
177 |
178 |
179 |
180 | ); 181 | }; 182 | export default PostModal; 183 | -------------------------------------------------------------------------------- /components/Header.tsx: -------------------------------------------------------------------------------- 1 | import MenuIcon from "@mui/icons-material/Menu"; 2 | import SearchIcon from "@mui/icons-material/Search"; 3 | import { motion } from "framer-motion"; 4 | import { signOut, useSession } from "next-auth/react"; 5 | import { useRouter } from "next/router"; 6 | import React, { useState, useEffect } from "react"; 7 | 8 | import HeartTip from "./HeartTip"; 9 | import PostModal from "./modal/PostModal"; 10 | import SearchTip from "./SearchTip"; 11 | 12 | type HeaderProps = {}; 13 | 14 | const Header: React.FC = () => { 15 | const { data: session } = useSession(); 16 | const router = useRouter(); 17 | const [open, setOpen] = useState(false); 18 | const [isOpen, setIsOpen] = useState(false); 19 | const [heartTipOpen, setHartTipOpen] = useState(true); 20 | const [searchValue, setSearchValue] = useState(""); 21 | 22 | useEffect(() => { 23 | setTimeout(() => { 24 | setHartTipOpen(false); 25 | }, 20000); 26 | }, [heartTipOpen]); 27 | 28 | return ( 29 | <> 30 |
31 |
32 |
router.push("/")} 34 | className="relative hidden lg:inline-grid w-24" 35 | > 36 | 41 |
42 |
router.push("/")} 44 | className="relative w-10 lg:hidden flex-shrink-0 cursor-pointer" 45 | > 46 | 51 |
52 |
53 |
54 |
!searchValue && setIsOpen(false)} 56 | onMouseEnter={() => setIsOpen(true)} 57 | > 58 |
59 | 60 |
61 | setSearchValue(e.target.value)} 67 | /> 68 |
69 | {isOpen && ( 70 | !searchValue && setIsOpen(false)} 75 | onMouseEnter={() => setIsOpen(true)} 76 | > 77 | 78 | 79 | )} 80 |
81 |
82 |
83 | router.push("/")} 89 | > 90 | 95 | 96 | 97 |
98 | 99 |
100 | 101 | {session ? ( 102 | <> 103 |
104 | 112 | 117 | 118 | 119 |
120 | 3 121 |
122 |
123 | 127 | setOpen(true)} 129 | xmlns="http://www.w3.org/2000/svg" 130 | fill="none" 131 | viewBox="0 0 24 24" 132 | strokeWidth={1.5} 133 | stroke="currentColor" 134 | className="navBtn" 135 | > 136 | 141 | 142 | 143 | 144 | 152 | 157 | 158 | setHartTipOpen(true)} 166 | > 167 | 172 | 173 | {heartTipOpen && } 174 | 175 | signOut()} 177 | className="h-10 rounded-full cursor-pointer" 178 | src={session?.user?.image as string} 179 | alt="" 180 | /> 181 | 182 | ) : ( 183 | 186 | )} 187 |
188 |
189 |
190 | 191 | 192 | ); 193 | }; 194 | export default Header; 195 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | logo 4 | 5 |

Instagram Clone with React.JS

6 | 7 |

8 | Instagram 2.0 with REACT.JS! (Next.js, NextAuth.js v4.17.0, Tailwind CSS, Firebase v9, Image Uploading, Google Authentication,, Instagram Profile) 9 |

10 | 11 | 12 | 13 | 14 | ![](https://img.shields.io/website-up-down-green-red/http/monip.org.svg) 15 | ![](https://img.shields.io/badge/Maintained-Yes-indigo) 16 | ![](https://img.shields.io/github/forks/SashenJayathilaka/Instagram-Clone.svg) 17 | ![](https://img.shields.io/github/stars/SashenJayathilaka/Instagram-Clone.svg) 18 | ![](https://img.shields.io/github/issues/SashenJayathilaka/Instagram-Clone) 19 | ![](https://img.shields.io/github/last-commit/SashenJayathilaka/Instagram-Clone) 20 | 21 |

22 | View Demo 23 | · 24 | Documentation 25 | · 26 | Report Bug 27 | · 28 | Request Feature 29 |

30 |
31 | 32 |
33 | 34 | 35 | 36 | ## :notebook_with_decorative_cover: Table of Contents 37 | 38 | - [About the Project](#star2-about-the-project) 39 | - [Screenshots](#camera-screenshots) 40 | - [Tech Stack](#space_invader-tech-stack) 41 | - [Environment Variables](#key-environment-variables) 42 | - [Getting Started](#toolbox-getting-started) 43 | - [Prerequisites](#bangbang-prerequisites) 44 | - [Installation](#gear-installation) 45 | - [Run Locally](#running-run-locally) 46 | - [Deployment](#triangular_flag_on_post-deployment) 47 | - [Contact](#handshake-contact) 48 | 49 | 50 | 51 | ## :star2: About the Project 52 | 53 | 54 | 55 | ### :camera: Screenshots 56 | 57 |
58 | image 59 |
60 | 61 | ## LIVE DEMO 💥 62 | 63 | ![forthebadge](https://forthebadge.com/images/badges/built-with-love.svg) 64 | ![forthebadge](https://forthebadge.com/images/badges/for-you.svg) 65 | ![forthebadge](https://forthebadge.com/images/badges/powered-by-coffee.svg) 66 | 67 | ### :space_invader: Tech Stack 68 | 69 |
70 | Client 71 | 77 |
78 | 79 |
80 | Database 81 | 84 |
85 |
86 | 87 | 88 | 89 | 92 | 95 | 98 | 101 | 104 | 107 | 110 | 111 |
90 | 91 | 93 | Google 94 | 96 | 97 | 99 | 100 | 102 | 103 | 105 | 106 | 108 | 109 |
112 | 113 | ## :toolbox: Getting Started 114 | 115 | ### :bangbang: Prerequisites 116 | 117 | - Sign up for a Firebase account HERE 118 | - Install Node JS in your computer HERE 119 | 120 | 121 | 122 | ### :key: Environment Variables 123 | 124 | To run this project, you will need to add the following environment variables to your .env file 125 | 126 | `NEXTAUTH_URL` 127 | 128 | `GOOGLE_CLIENT_ID` 129 | 130 | `NEXT_PUBLIC_SECRET` 131 | 132 | `GOOGLE_CLIENT_SECRET` 133 | 134 | `NEXT_PUBLIC_BASE_URL` 135 | 136 | `NEXT_PUBLIC_FIREBASE_APP_ID` 137 | 138 | `NEXT_PUBLIC_FIREBASE_API_KEY` 139 | 140 | `NEXT_PUBLIC_FIREBASE_PROJECT_ID` 141 | 142 | `NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN` 143 | 144 | `NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET` 145 | 146 | `NEXT_PUBLIC_FIREBASE_MESSAGING_SET` 147 | 148 | 149 | ### :gear: Installation 150 | 151 | Install my-project with npm 152 | 153 | ``` 154 | npx create-next-app instagram_clone 155 | ``` 156 | 157 | ``` 158 | cd instagram_clone 159 | ``` 160 | 161 | Install dependencies 162 | 163 | ### :test_tube: Install Tailwind CSS with Next.js 164 | 165 | #### Install Tailwind CSS 166 | 167 | Install tailwindcss and its peer dependencies via npm, and then run the init command to generate both `tailwind.config.js` and `postcss.config.js`. 168 | 169 | ``` 170 | npm install -D tailwindcss postcss autoprefixer 171 | ``` 172 | 173 | ``` 174 | npx tailwindcss init -p 175 | ``` 176 | 177 | #### Configure your template paths 178 | 179 | Add the paths to all of your template files in your `tailwind.config.js` file. 180 |
181 | 182 | ``` 183 | module.exports = { 184 | content: [ 185 | "./pages/**/*.{js,ts,jsx,tsx}", 186 | "./components/**/*.{js,ts,jsx,tsx}", 187 | ], 188 | theme: { 189 | extend: {}, 190 | }, 191 | plugins: [], 192 | } 193 | ``` 194 | 195 | #### Add the Tailwind directives to your CSS 196 | 197 | Add the `@tailwind` directives for each of Tailwind’s layers to your `./styles/globals.css` file. 198 | 199 | ``` 200 | @tailwind base; 201 | @tailwind components; 202 | @tailwind utilities; 203 | ``` 204 | 205 | Install dependencies 206 | 207 | 🔶 Other Dependency Info 208 | 209 | 210 | 211 | ### :running: Run Locally 212 | 213 | Clone the project 214 | 215 | ```bash 216 | git clone https://github.com/SashenJayathilaka/Instagram-Clone.git 217 | ``` 218 | 219 | Install dependencies 220 | 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). 221 | 222 | ```bash 223 | npm install 224 | ``` 225 | 226 | ## Getting Started 227 | 228 | Start the server 229 | First, run the development server: 230 | 231 | ```bash 232 | npm run dev 233 | ``` 234 | 235 | 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). 236 | 237 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 238 | 239 | You can start editing the page by modifying `pages/index.js`. The page auto-updates as you edit the file. 240 | 241 | [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`. 242 | 243 | 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. 244 | 245 | ### Learn More 246 | 247 | To learn more about Next.js, take a look at the following resources: 248 | 249 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 250 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 251 | 252 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 253 | 254 | 255 | 256 | ### :triangular_flag_on_post: Deployment 257 | 258 | To deploy this project run 259 | 260 | ##### Deploy on Vercel 261 | 262 | 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. 263 | 264 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 265 | 266 | ## :handshake: Contact 267 | 268 | Your Name - [@twitter_handle](https://twitter.com/SashenHasinduJ) - sashenjayathilaka95@gmail.com 269 | 270 | Project Link: [https://github.com/SashenJayathilaka/Instagram-Clone.git](https://github.com/SashenJayathilaka/Instagram-Clone.git) 271 | 272 |
273 | 274 |
275 | image 276 |
277 | 278 |
279 | 280 |
Don't forget to leave a star ⭐️
281 | -------------------------------------------------------------------------------- /components/Post.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | addDoc, 3 | collection, 4 | deleteDoc, 5 | doc, 6 | onSnapshot, 7 | orderBy, 8 | query, 9 | serverTimestamp, 10 | setDoc, 11 | } from "firebase/firestore"; 12 | import { motion } from "framer-motion"; 13 | import moment from "moment"; 14 | import { useSession } from "next-auth/react"; 15 | import { useRouter } from "next/router"; 16 | import React, { useEffect, useState } from "react"; 17 | 18 | import { firestore } from "../firebase/firebase"; 19 | 20 | type PostProps = { 21 | id: string; 22 | username: string; 23 | userImage: string; 24 | img: string; 25 | caption: string; 26 | userId: string; 27 | }; 28 | 29 | const Post: React.FC = ({ 30 | id, 31 | username, 32 | userImage, 33 | img, 34 | caption, 35 | userId, 36 | }) => { 37 | const { data: session } = useSession(); 38 | const router = useRouter(); 39 | const [comment, setComment] = useState(""); 40 | const [comments, setComments] = useState([]); 41 | const [likes, setLikes] = useState([]); 42 | const [hasLikes, setHasLikes] = useState(false); 43 | const [loading, setLoading] = useState(false); 44 | 45 | const sendComment = async (e: any) => { 46 | e.preventDefault(); 47 | 48 | if (comment) { 49 | setLoading(true); 50 | try { 51 | await addDoc(collection(firestore, "posts", id, "comments"), { 52 | comment: comment, 53 | username: session?.user?.name, 54 | userImage: session?.user?.image, 55 | timestamp: serverTimestamp(), 56 | }); 57 | 58 | setLoading(false); 59 | } catch (error) { 60 | console.log(error); 61 | } 62 | 63 | setComment(""); 64 | } else { 65 | console.log("err"); 66 | } 67 | }; 68 | 69 | useEffect( 70 | () => 71 | onSnapshot( 72 | query( 73 | collection(firestore, "posts", id, "comments"), 74 | orderBy("timestamp", "desc") 75 | ), 76 | (snapshot) => setComments(snapshot.docs) 77 | ), 78 | [firestore, id] 79 | ); 80 | 81 | useEffect( 82 | () => 83 | onSnapshot(collection(firestore, "posts", id, "likes"), (snapshot) => 84 | setLikes(snapshot.docs) 85 | ), 86 | [firestore, id] 87 | ); 88 | 89 | useEffect( 90 | () => 91 | setHasLikes( 92 | likes.findIndex((like) => like.id === session?.user?.uid) !== -1 93 | ), 94 | [likes] 95 | ); 96 | 97 | const likePost = async () => { 98 | try { 99 | if (hasLikes) { 100 | await deleteDoc( 101 | doc(firestore, "posts", id, "likes", session?.user?.uid!) 102 | ); 103 | } else { 104 | await setDoc( 105 | doc(firestore, "posts", id, "likes", session?.user?.uid!), 106 | { 107 | username: session?.user?.name, 108 | } 109 | ); 110 | } 111 | } catch (error) { 112 | console.log(error); 113 | } 114 | }; 115 | 116 | const handleChangePage = () => { 117 | if (session) { 118 | router.push({ 119 | pathname: `profile/${userId}`, 120 | query: { 121 | userId: userId.toString(), 122 | }, 123 | }); 124 | } else { 125 | router.push("/auth/login"); 126 | } 127 | }; 128 | 129 | return ( 130 | 136 |
137 | 144 |

148 | {username} 149 |

150 | 158 | 163 | 164 |
165 | 166 | {session && ( 167 |
168 |
169 | {hasLikes ? ( 170 | 174 | 181 | 182 | 183 | 184 | ) : ( 185 | 189 | 198 | 203 | 204 | 205 | )} 206 | 210 | 218 | 223 | 224 | 225 | 226 | 230 | 238 | 243 | 244 | 245 |
246 | 247 | 248 | 256 | 261 | 262 | 263 |
264 | )} 265 | 266 |

267 | {likes.length > 0 && ( 268 |

{likes.length} likes

269 | )} 270 | {username} 271 | {caption} 272 |

273 | 274 | {/* comments */} 275 | {comments.length > 0 && ( 276 |
277 | {comments.map((comment) => ( 278 |
283 | 288 |

289 | {comment.data().username} 290 | {comment.data().comment} 291 |

292 |

293 | {moment( 294 | new Date(comment.data().timestamp?.seconds * 1000) 295 | ).fromNow()} 296 |

297 |
298 | ))} 299 |
300 | )} 301 | 302 | {session && ( 303 |
304 | 312 | 317 | 318 | 319 | setComment(e.target.value)} 323 | placeholder="Add a comment..." 324 | className="border-none flex-1 focus:ring-0 outline-none" 325 | /> 326 | {loading ? ( 327 | 335 | 340 | 341 | ) : ( 342 | 350 | Post 351 | 352 | )} 353 |
354 | )} 355 |
356 | ); 357 | }; 358 | export default Post; 359 | --------------------------------------------------------------------------------