├── 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 |
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 |
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 |
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 |
23 |
24 | {session && (
25 |
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 |
--------------------------------------------------------------------------------
/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 |
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 |
})
25 |
26 |
27 |
28 |
29 | {faker.name.firstName()}
30 |
31 |
{faker.company.bs()}
32 |
33 |
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 |

44 |
45 |
46 |
47 |
48 |
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 |
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 |
116 |
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 |
96 |
97 |
98 |
99 |
100 |
101 | {session ? (
102 | <>
103 |
104 |
118 |
119 |
120 | 3
121 |
122 |
123 |
127 |
142 |
143 |
144 |
158 |
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 |

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 |

15 | 
16 | 
17 | 
18 | 
19 | 
20 |
21 |
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 |

59 |
60 |
61 | ## LIVE DEMO 💥
62 |
63 | 
64 | 
65 | 
66 |
67 | ### :space_invader: Tech Stack
68 |
69 |
70 | Client
71 |
77 |
78 |
79 |
80 | Database
81 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 | |
92 |
93 |
94 | |
95 |
96 |
97 | |
98 |
99 |
100 | |
101 |
102 |
103 | |
104 |
105 |
106 | |
107 |
108 |
109 | |
110 |
111 |
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 |

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 |
164 |
165 |
166 | {session && (
167 |
168 |
169 | {hasLikes ? (
170 |
174 |
183 |
184 | ) : (
185 |
189 |
204 |
205 | )}
206 |
210 |
224 |
225 |
226 |
230 |
244 |
245 |
246 |
247 |
248 |
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 |
.userImage})
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 |
354 | )}
355 |
356 | );
357 | };
358 | export default Post;
359 |
--------------------------------------------------------------------------------