├── src ├── react-app-env.d.ts ├── models │ ├── identity.tsx │ ├── index.js │ ├── profile.tsx │ └── post.tsx ├── components │ ├── index.js │ ├── sidebar │ │ ├── index.js │ │ ├── user.js │ │ ├── suggestions.js │ │ └── suggested-profile.js │ ├── post │ │ ├── post-attachment.js │ │ ├── detail │ │ │ └── post-detail.js │ │ ├── footer │ │ │ ├── index.js │ │ │ ├── add-comment │ │ │ │ └── add-comment.js │ │ │ └── comment │ │ │ │ └── comments.js │ │ ├── post-action │ │ │ └── index.js │ │ ├── header.js │ │ └── index.js │ ├── expended-text.tsx │ ├── user-avatar.js │ ├── model │ │ ├── model.js │ │ └── menu-action.component.tsx │ ├── loader.js │ ├── profile │ │ ├── index.js │ │ ├── photos.js │ │ └── profile-header.js │ ├── timeline │ │ └── index.js │ ├── header.js │ └── create-post │ │ └── create-post-model-component.js ├── style │ └── index.scss ├── pages │ ├── no-page-found.page.js │ ├── explore │ │ ├── explore.page.js │ │ └── people.page.js │ ├── dashboard.js │ ├── settings │ │ ├── settings-sidebar.component.js │ │ ├── account-settings.page.js │ │ └── edit-profile.page.js │ ├── profile.page.js │ ├── login.page.js │ └── signup.page.js ├── constants │ └── routes.js ├── context │ ├── firebase.js │ ├── feed.js │ └── session.js ├── lib │ └── firebase.js ├── index.js ├── hook │ ├── use-auth-listener.js │ ├── use-scroll.tsx │ ├── use-post.tsx │ ├── use-user.tsx │ ├── use-people.tsx │ └── use-feed.tsx ├── helper │ └── routes.helper.js ├── App.js ├── services │ ├── firebase-storage.tsx │ ├── profile.tsx │ ├── auth.tsx │ └── feed.tsx └── seed.tsx ├── .vscode └── settings.json ├── .firebaserc ├── public ├── favicon.ico ├── images │ ├── logo.png │ ├── users │ │ ├── logo.png │ │ └── raphael │ │ │ ├── 1.jpg │ │ │ ├── 2.jpg │ │ │ ├── 3.jpg │ │ │ ├── 4.jpg │ │ │ └── 5.jpg │ ├── avatars │ │ ├── dali.jpg │ │ ├── karl.jpg │ │ ├── orwell.jpg │ │ ├── steve.jpg │ │ ├── default.png │ │ └── raphael.jpg │ └── iphone-with-profile.jpg └── index.html ├── firebase.json ├── workerbox-config.js ├── .gitignore ├── tsconfig.json ├── .github └── workflows │ ├── firebase-hosting-merge.yml │ └── firebase-hosting-pull-request.yml ├── .eslintrc.json ├── package.json ├── README.md └── .firebase └── hosting.YnVpbGQ.cache /src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.words": [ 3 | "Signup" 4 | ] 5 | } -------------------------------------------------------------------------------- /.firebaserc: -------------------------------------------------------------------------------- 1 | { 2 | "projects": { 3 | "default": "react-instagram-9284e" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheAlphamerc/react-instagram/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheAlphamerc/react-instagram/HEAD/public/images/logo.png -------------------------------------------------------------------------------- /public/images/users/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheAlphamerc/react-instagram/HEAD/public/images/users/logo.png -------------------------------------------------------------------------------- /public/images/avatars/dali.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheAlphamerc/react-instagram/HEAD/public/images/avatars/dali.jpg -------------------------------------------------------------------------------- /public/images/avatars/karl.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheAlphamerc/react-instagram/HEAD/public/images/avatars/karl.jpg -------------------------------------------------------------------------------- /public/images/avatars/orwell.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheAlphamerc/react-instagram/HEAD/public/images/avatars/orwell.jpg -------------------------------------------------------------------------------- /public/images/avatars/steve.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheAlphamerc/react-instagram/HEAD/public/images/avatars/steve.jpg -------------------------------------------------------------------------------- /public/images/avatars/default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheAlphamerc/react-instagram/HEAD/public/images/avatars/default.png -------------------------------------------------------------------------------- /public/images/avatars/raphael.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheAlphamerc/react-instagram/HEAD/public/images/avatars/raphael.jpg -------------------------------------------------------------------------------- /public/images/users/raphael/1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheAlphamerc/react-instagram/HEAD/public/images/users/raphael/1.jpg -------------------------------------------------------------------------------- /public/images/users/raphael/2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheAlphamerc/react-instagram/HEAD/public/images/users/raphael/2.jpg -------------------------------------------------------------------------------- /public/images/users/raphael/3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheAlphamerc/react-instagram/HEAD/public/images/users/raphael/3.jpg -------------------------------------------------------------------------------- /public/images/users/raphael/4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheAlphamerc/react-instagram/HEAD/public/images/users/raphael/4.jpg -------------------------------------------------------------------------------- /public/images/users/raphael/5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheAlphamerc/react-instagram/HEAD/public/images/users/raphael/5.jpg -------------------------------------------------------------------------------- /public/images/iphone-with-profile.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheAlphamerc/react-instagram/HEAD/public/images/iphone-with-profile.jpg -------------------------------------------------------------------------------- /src/models/identity.tsx: -------------------------------------------------------------------------------- 1 | export function identity(arg: Type | undefined): Type | null { 2 | if (arg === undefined) { 3 | return null; 4 | } 5 | return arg; 6 | } 7 | -------------------------------------------------------------------------------- /firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "hosting": { 3 | "public": "build", 4 | "ignore": [ 5 | "firebase.json", 6 | "**/.*", 7 | "**/node_modules/**" 8 | ] 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/components/index.js: -------------------------------------------------------------------------------- 1 | import Header from './header'; 2 | import Sidebar from './sidebar'; 3 | import Timeline from './timeline'; 4 | import UserAvatar from './user-avatar'; 5 | 6 | export {Timeline,Sidebar,Header,UserAvatar}; -------------------------------------------------------------------------------- /workerbox-config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | globDirectory: 'build/', 3 | globPatterns: [ 4 | '**/*.{json,ico,jpg,png,html,css,js,txt}' 5 | ], 6 | swDest: 'build/sw.js', 7 | ignoreURLParametersMatching: [ 8 | /^utm_/, 9 | /^fbclid$/ 10 | ] 11 | }; -------------------------------------------------------------------------------- /src/models/index.js: -------------------------------------------------------------------------------- 1 | import {Profile,ProfileConverter} from "./profile"; 2 | // import {CommentModel,CommentConverter} from "./comment"; 3 | import { PostModel,PostConverter } from "./post"; 4 | 5 | export {PostModel as CommentModel,PostConverter as CommentConverter}; 6 | export {Profile, ProfileConverter} 7 | export {PostModel,PostConverter}; 8 | -------------------------------------------------------------------------------- /src/components/sidebar/index.js: -------------------------------------------------------------------------------- 1 | import User from "./user"; 2 | import Suggestions from "./suggestions"; 3 | 4 | 5 | function Sidebar({user}) { 6 | 7 | return ( 8 |
9 |
10 | 11 |
12 | 13 |
14 | ); 15 | } 16 | 17 | export default Sidebar; 18 | -------------------------------------------------------------------------------- /.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 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | .firebase/hosting.YnVpbGQ.cache 25 | .firebase/hosting.YnVpbGQ.cache 26 | -------------------------------------------------------------------------------- /src/style/index.scss: -------------------------------------------------------------------------------- 1 | body { 2 | 3 | .Modal { 4 | display: none; 5 | &.active { 6 | position: fixed; 7 | top: 0px; 8 | bottom: 0px; 9 | left: 0px; 10 | right: 0px; 11 | overflow: auto; 12 | z-index: 300; 13 | background-color: #3338; 14 | display: block; 15 | } 16 | .Card { 17 | margin: 10rem auto; 18 | } 19 | } 20 | .spinner { 21 | animation: spinner 0.75s ease-in-out infinite; 22 | } 23 | } 24 | Html{ 25 | background-color: #f3f4f6 26 | } -------------------------------------------------------------------------------- /src/pages/no-page-found.page.js: -------------------------------------------------------------------------------- 1 | import { useEffect } from "react"; 2 | import Header from "../components/header"; 3 | import { withSession } from "../context/session"; 4 | function NoPageFound({ user }) { 5 | useEffect(() => { 6 | document.title = "Page not found - instagram"; 7 | }); 8 | return ( 9 |
10 |
11 |
12 |

No page found

13 |
14 |
15 | ); 16 | } 17 | 18 | export default withSession(NoPageFound); 19 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "noFallthroughCasesInSwitch": true, 16 | "module": "esnext", 17 | "moduleResolution": "node", 18 | "resolveJsonModule": true, 19 | "isolatedModules": true, 20 | "noEmit": true, 21 | "jsx": "react-jsx" 22 | }, 23 | "include": [ 24 | "src" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /src/constants/routes.js: -------------------------------------------------------------------------------- 1 | export const LOGIN_ROUTE = '/login'; 2 | export const NO_PAGE_FOUND_ROUTE = '*'; 3 | export const SIGNUP_ROUTE = '/signup'; 4 | export const DASHBOARD = '/'; 5 | export const PROFILE = '/p/:username'; 6 | export const POST_ROUTE = 'post'; 7 | export const POST_DETAIL_ROUTE = 'post-detail'; 8 | export const ACCOUNT_SETTINGS_ROUTE = '/account'; 9 | export const PROFILE_EDIT_ROUTE = `edit`; 10 | export const EXPLORE_ROUTE = '/explore'; 11 | export const PEOPLE_ROUTE = 'people'; 12 | 13 | // Return the complete route for edit profile 14 | export function getProfileEditAccount(){ 15 | return `${ACCOUNT_SETTINGS_ROUTE}/${PROFILE_EDIT_ROUTE}`; 16 | } -------------------------------------------------------------------------------- /src/context/firebase.js: -------------------------------------------------------------------------------- 1 | import { createContext } from "react"; 2 | 3 | import { firebase,db } from "../lib/firebase"; 4 | 5 | const FirebaseContext = createContext(null); 6 | FirebaseContext.displayName = "FirebaseContext"; 7 | 8 | export { FirebaseContext }; 9 | 10 | export default ({ children }) => { 11 | return ( 12 | 13 | {children} 14 | 15 | ); 16 | }; 17 | 18 | export const withFirebase = (Component) => (props) => 19 | ( 20 | 21 | {(firebase, db) => } 22 | 23 | ); 24 | -------------------------------------------------------------------------------- /.github/workflows/firebase-hosting-merge.yml: -------------------------------------------------------------------------------- 1 | # This file was auto-generated by the Firebase CLI 2 | # https://github.com/firebase/firebase-tools 3 | 4 | name: Deploy to Firebase Hosting on merge 5 | 'on': 6 | push: 7 | branches: 8 | - master 9 | jobs: 10 | build_and_deploy: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v2 14 | - run: npm run build 15 | - uses: FirebaseExtended/action-hosting-deploy@v0 16 | with: 17 | repoToken: '${{ secrets.GITHUB_TOKEN }}' 18 | firebaseServiceAccount: '${{ secrets.FIREBASE_SERVICE_ACCOUNT_REACT_INSTAGRAM_9284E }}' 19 | channelId: live 20 | projectId: react-instagram-9284e 21 | -------------------------------------------------------------------------------- /src/components/post/post-attachment.js: -------------------------------------------------------------------------------- 1 | import PropTypes from "prop-types"; 2 | function PostAttachmentComponent({ attachments, setActive = e=>{} }) { 3 | if (!attachments) return null; 4 | return ( 5 |
{ 7 | setActive(true); 8 | }} 9 | className="flex place-content-center items-center w-full bg-gray-200" 10 | > 11 | {attachments.map((attachment, index) => { 12 | return ; 13 | })} 14 |
15 | ); 16 | } 17 | 18 | PostAttachmentComponent.prototype = { 19 | attachments: PropTypes.array.isRequired, 20 | }; 21 | export default PostAttachmentComponent; 22 | -------------------------------------------------------------------------------- /src/context/feed.js: -------------------------------------------------------------------------------- 1 | import { createContext } from "react"; 2 | import UserFeeds from "../hook/use-feed"; 3 | 4 | const FeedContext = createContext(null); 5 | FeedContext.displayName = "FeedContext"; 6 | 7 | export { FeedContext }; 8 | 9 | export const withFeedProvider = (Component) => (props) => { 10 | const data = UserFeeds(); 11 | return ( 12 | 13 | 14 | 15 | ); 16 | }; 17 | 18 | export const withFeed = (Component) => (props) => 19 | ( 20 | 21 | {(data) => ( 22 | 23 | )} 24 | 25 | ); 26 | -------------------------------------------------------------------------------- /.github/workflows/firebase-hosting-pull-request.yml: -------------------------------------------------------------------------------- 1 | # This file was auto-generated by the Firebase CLI 2 | # https://github.com/firebase/firebase-tools 3 | 4 | name: Deploy to Firebase Hosting on PR 5 | 'on': pull_request 6 | jobs: 7 | build_and_preview: 8 | if: '${{ github.event.pull_request.head.repo.full_name == github.repository }}' 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v2 12 | - run: npm run build 13 | - uses: FirebaseExtended/action-hosting-deploy@v0 14 | with: 15 | repoToken: '${{ secrets.GITHUB_TOKEN }}' 16 | firebaseServiceAccount: '${{ secrets.FIREBASE_SERVICE_ACCOUNT_REACT_INSTAGRAM_9284E }}' 17 | projectId: react-instagram-9284e 18 | -------------------------------------------------------------------------------- /src/context/session.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from "react"; 2 | import UseAuthListener from "../hook/use-auth-listener"; 3 | import UseUser from "../hook/use-user"; 4 | 5 | export const SessionContext = React.createContext(null); 6 | SessionContext.displayName = "SessionContext"; 7 | 8 | export const withSessionProvider = (Component) => (props) => { 9 | const user = UseUser(); 10 | return ( 11 | 12 | 13 | 14 | ); 15 | }; 16 | 17 | export const withSession = (Component) => (props) => { 18 | return ( 19 | 20 | {(user) => } 21 | 22 | ); 23 | }; 24 | -------------------------------------------------------------------------------- /src/pages/explore/explore.page.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import { Header } from '../../components'; 3 | import { withSession } from '../../context/session'; 4 | import { People } from '../../helper/routes.helper'; 5 | 6 | function ExploreComponent({ user }) { 7 | useEffect(() => { 8 | document.title = "Instagram"; 9 | }, []); 10 | 11 | return ( 12 |
13 |
14 |
15 |
16 | 17 |
18 |
19 |
20 | ); 21 | } 22 | const ExplorePage = withSession(ExploreComponent); 23 | export default ExplorePage; 24 | -------------------------------------------------------------------------------- /src/lib/firebase.js: -------------------------------------------------------------------------------- 1 | import { initializeApp } from "firebase/app"; 2 | import { 3 | getFirestore, 4 | collection, 5 | addDoc, 6 | getDocs, 7 | getDoc, 8 | query, 9 | doc, 10 | setDoc, 11 | where, 12 | limit, 13 | updateDoc, 14 | } from "firebase/firestore"; 15 | 16 | const config = { 17 | apiKey: "AIzaSyDAv_F_c6Wg-LsVhbL38Z9RHMyW3A5bTwA", 18 | authDomain: "react-instagram-9284e.firebaseapp.com", 19 | projectId: "react-instagram-9284e", 20 | storageBucket: "react-instagram-9284e.appspot.com", 21 | messagingSenderId: "718982163479", 22 | appId: "1:718982163479:web:046bbd54684ad0bd6e0052", 23 | measurementId: "G-NRW3V5G962", 24 | }; 25 | 26 | const firebase = initializeApp(config); 27 | 28 | const db = getFirestore(firebase); 29 | 30 | export { firebase, db,collection,setDoc,doc }; 31 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 13 | Instagraam 14 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import App from "./App"; 4 | import "./style/index.scss"; 5 | import { FirebaseContext } from "./context/firebase"; 6 | import { firebase,db } from "./lib/firebase"; 7 | 8 | ReactDOM.render( 9 | 10 | 11 | 12 | 13 | , 14 | document.getElementById("root") 15 | ); 16 | 17 | // Client side rendered app: react 18 | // -> database which is firebase 19 | // -> react-loading-skeleton 20 | // -> tailwind 21 | 22 | // Folder Structure 23 | // -> src 24 | // -> component 25 | // -> constant 26 | // -> context 27 | // -> helper 28 | // -> hooks 29 | // -> models 30 | // -> lib (fireabse is going to live here) 31 | // -> service (firebase functions in here) 32 | // -> styles (tailwind's folder (app/tailwind)) 33 | -------------------------------------------------------------------------------- /src/components/expended-text.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | 3 | interface Props { 4 | text: string; 5 | className?: string; 6 | } 7 | function ExpandedText({ text, className }: Props) { 8 | const [longText, setLongText] = useState(text.length > 100); 9 | 10 | return ( 11 | 12 | {longText ? text.substring(0, 100) : text} 13 | {longText && ( 14 | setLongText(!longText)} 16 | className="text-blue-500 cursor-pointer" 17 | > 18 |  more.. 19 | 20 | )} 21 | {text.length > 100 && !longText && ( 22 | setLongText(!longText)} 24 | className="text-blue-500 cursor-pointer" 25 | > 26 |  less 27 | 28 | )} 29 | 30 | ); 31 | } 32 | 33 | export default ExpandedText; 34 | -------------------------------------------------------------------------------- /src/components/user-avatar.js: -------------------------------------------------------------------------------- 1 | import cx from "classnames"; 2 | 3 | function UserAvatar({ avatar, fullname, className }) { 4 | return ( 5 |
10 | {avatar != null ? ( 11 | A 12 | ) : fullname ? ( 13 | getInitials(fullname) 14 | ) : ( 15 | "" 16 | )} 17 |
18 | ); 19 | } 20 | 21 | // Return Initials of name 22 | function getInitials(name) { 23 | if (!name) { 24 | return ""; 25 | } 26 | const names = name.split(" "); 27 | if (names.length === 1) { 28 | return names[0].substring(0, 2).toUpperCase(); 29 | } 30 | return `${names[0].substring(0, 1).toUpperCase()}${names[1] 31 | .substring(0, 1) 32 | .toUpperCase()}`; 33 | } 34 | 35 | export default UserAvatar; 36 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | // ... 4 | "react-hooks", 5 | "import" 6 | ], 7 | "rules": { 8 | // ... 9 | "react-hooks/rules-of-hooks": "error", // Checks rules of Hooks 10 | "react-hooks/exhaustive-deps": "warn" ,// Checks effect dependencies 11 | "react/react-in-jsx-scope": "off",// suppress errors for missing 'import React' in files 12 | "react/jsx-filename-extension": [1, { "extensions": [".js", ".jsx"] }], //should add ".ts" if typescript project 13 | "react/prop-types": 0, 14 | "no-unused-vars": 0, 15 | "react/display-name": 0, 16 | "react/no-unescaped-entities": 0 17 | }, 18 | "extends": [ 19 | "eslint:recommended", 20 | "plugin:react/recommended" 21 | ], 22 | "parserOptions": { 23 | "sourceType": "module", 24 | "ecmaVersion": 2020 25 | }, 26 | "env": { 27 | "browser": true, 28 | "node": true 29 | } 30 | } -------------------------------------------------------------------------------- /src/hook/use-auth-listener.js: -------------------------------------------------------------------------------- 1 | import { useState, useEffect, useContext } from "react"; 2 | import { FirebaseContext } from "../context/firebase"; 3 | import { getAuth, onAuthStateChanged } from "firebase/auth"; 4 | 5 | function UseAuthListener() { 6 | const [user, setUser] = useState( 7 | JSON.parse(localStorage.getItem("authUser")) 8 | ); 9 | const { firebase } = useContext(FirebaseContext); 10 | 11 | useEffect(() => { 12 | const auth = getAuth(); 13 | const listener = onAuthStateChanged(auth, (user) => { 14 | if (user) { 15 | // User is signed in 16 | localStorage.setItem("authUser", JSON.stringify(user)); 17 | setUser(user); 18 | // ... 19 | } else { 20 | // User is signed out 21 | localStorage.removeItem("authUser"); 22 | setUser(null); 23 | console.log("User Signed Out"); 24 | } 25 | }); 26 | return () => listener(); 27 | }, [firebase]); 28 | 29 | return user; 30 | } 31 | 32 | export default UseAuthListener; 33 | -------------------------------------------------------------------------------- /src/components/sidebar/user.js: -------------------------------------------------------------------------------- 1 | import { memo } from "react"; 2 | 3 | import { Link } from "react-router-dom"; 4 | import Skeleton from "react-loading-skeleton"; 5 | import "react-loading-skeleton/dist/skeleton.css"; 6 | import { UserAvatar } from ".."; 7 | 8 | function User({ user }) { 9 | if (!user) { 10 | return ; 11 | } else { 12 | return ( 13 | 14 |
15 |
16 | 21 |
22 |
23 |
24 |
{user.fullname}
25 |
{user.username}
26 |
27 | 28 | ); 29 | } 30 | } 31 | 32 | export default memo(User); 33 | -------------------------------------------------------------------------------- /src/pages/dashboard.js: -------------------------------------------------------------------------------- 1 | // import { seedDatabase } from "../seed"; 2 | // import {db,collection,setDoc,doc} from "../lib/firebase"; 3 | import * as Components from "../components/index"; 4 | 5 | import { useEffect, useRef } from "react"; 6 | 7 | import { withSession } from "../context/session"; 8 | 9 | function Dashboard({ user }) { 10 | const createPostRef = useRef(); 11 | 12 | useEffect(() => { 13 | try { 14 | // seedDatabase(db,collection,doc,setDoc); 15 | } catch (error) { 16 | console.error(error); 17 | } 18 | document.title = "Instagram"; 19 | }, [user]); 20 | 21 | return ( 22 |
23 | 24 |
25 | 26 | 27 |
28 |
29 | ); 30 | } 31 | 32 | export default withSession(Dashboard); 33 | -------------------------------------------------------------------------------- /src/pages/settings/settings-sidebar.component.js: -------------------------------------------------------------------------------- 1 | import cx from "classnames"; 2 | import React from "react"; 3 | import { NavLink } from "react-router-dom"; 4 | function SettingsSidebarComponent({ className }) { 5 | // const [, set] = useState(); 6 | return ( 7 | 13 | ); 14 | } 15 | 16 | function LinkButton({ label, to, isActive = false }) { 17 | return ( 18 |
23 | {label} 24 |
25 | ); 26 | function QueryNavLink({ to, ...props }) { 27 | return ; 28 | } 29 | } 30 | export default SettingsSidebarComponent; 31 | -------------------------------------------------------------------------------- /src/pages/settings/account-settings.page.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from "react"; 2 | import { Outlet } from "react-router-dom"; 3 | import Header from "../../components/header"; 4 | import { withSession } from "../../context/session"; 5 | import SettingsSidebarComponent from "./settings-sidebar.component"; 6 | import EditProfilePage from "./edit-profile.page"; 7 | 8 | function AccountSettingsComponent({ user }) { 9 | useEffect(() => { 10 | document.title = "Account Settings - Instagram"; 11 | }, []); 12 | 13 | return ( 14 |
15 |
16 |
17 |
18 | 19 |
20 | 21 | {/* */} 22 |
23 |
24 |
25 |
26 | ); 27 | } 28 | const AccountSettingsPage = withSession(AccountSettingsComponent); 29 | export default AccountSettingsPage; 30 | -------------------------------------------------------------------------------- /src/hook/use-scroll.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from "react"; 2 | 3 | /** 4 | * Get suggested users list 5 | * @param {boolean} hasMoreData 6 | * @param {boolean} loadMore 7 | * @param {boolean} isLoading 8 | * @param {Function} setLoadMore 9 | * @returns 10 | */ 11 | 12 | function useScroll( 13 | hasMoreData: boolean, 14 | loadMore: boolean, 15 | isLoading: boolean, 16 | setLoadMore: (e: boolean) => void 17 | ) { 18 | /// Trigger load more effect 19 | useEffect(() => { 20 | // create callback 21 | const callBack = () => { 22 | if ( 23 | window.innerHeight + window.scrollY + 100 >= 24 | document.body.offsetHeight 25 | ) { 26 | // Fetch more post feed when user scrolls to bottom 27 | if (hasMoreData && !isLoading && !loadMore) { 28 | console.log("Load more"); 29 | setLoadMore(true); 30 | } 31 | } 32 | }; 33 | console.log("Scroll listener attached:"); 34 | window.addEventListener("scroll", callBack); 35 | return () => { 36 | console.log("Scroll listener Removed:"); 37 | window.removeEventListener("scroll", callBack); 38 | }; 39 | }, []); 40 | } 41 | 42 | export default useScroll; 43 | -------------------------------------------------------------------------------- /src/helper/routes.helper.js: -------------------------------------------------------------------------------- 1 | import * as ROUTES from "../constants/routes"; 2 | 3 | import { Navigate } from "react-router-dom"; 4 | import { lazy } from "react"; 5 | const Login = lazy(() => import("../pages/login.page")); 6 | const Profile = lazy(() => import("../pages/profile.page")); 7 | const Signup = lazy(() => import("../pages/signup.page")); 8 | const Dashboard = lazy(() => import("../pages/dashboard")); 9 | const NoPageFound = lazy(() => import("../pages/no-page-found.page")); 10 | const AccountSettings = lazy(() => import("../pages/settings/account-settings.page")) 11 | const EditProfile = lazy(() => import("../pages/settings/edit-profile.page")); 12 | const Explore = lazy(() => import("../pages/explore/explore.page")); 13 | const People = lazy(() => import("../pages/explore/people.page")); 14 | 15 | function getProtectedRoute(user, element) { 16 | if (!(user && Object.keys(user).length === 0)) { 17 | return element; 18 | } 19 | return ; 20 | } 21 | function getLoggedInRoute(user, element) { 22 | if (!(user && Object.keys(user).length === 0)) { 23 | return ; 24 | } 25 | return element; 26 | } 27 | export { Profile, Login, Signup, Dashboard, NoPageFound ,AccountSettings,EditProfile,Explore,People}; 28 | export { getProtectedRoute, getLoggedInRoute ,ROUTES as RouteHelper}; 29 | -------------------------------------------------------------------------------- /src/pages/explore/people.page.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import usePeople from "././../../hook/use-people"; 3 | import Skeleton from "react-loading-skeleton"; 4 | import "react-loading-skeleton/dist/skeleton.css"; 5 | import SuggestedProfile from "../../components/sidebar/suggested-profile"; 6 | import { withSession } from "../../context/session"; 7 | 8 | function PeoplePageComponent({ user }) { 9 | const people = usePeople(user.userId, 20); 10 | 11 | return ( 12 |
13 |

Suggestions

14 |
15 | {!people ? ( 16 | 17 | ) : people.length > 0 ? ( 18 | <> 19 |
20 | {people.map((people, index) => ( 21 | 26 | ))} 27 |
28 | 29 | ) : ( 30 |
No suggestion Available
31 | )} 32 |
33 |
34 | ); 35 | } 36 | export default withSession(PeoplePageComponent); 37 | -------------------------------------------------------------------------------- /src/hook/use-post.tsx: -------------------------------------------------------------------------------- 1 | import { useContext, useEffect, useState } from "react"; 2 | import { 3 | onSnapshot, 4 | collection, 5 | query, 6 | where, 7 | QuerySnapshot, 8 | } from "firebase/firestore"; 9 | import { PostConverter, PostModel } from "../models/post"; 10 | import { FirebaseContext } from "../context/firebase"; 11 | import { db } from "../lib/firebase"; 12 | import { SessionContext } from "../context/session"; 13 | 14 | function UsePost(): QuerySnapshot | undefined { 15 | const [changePost, setChangePost] = useState< 16 | QuerySnapshot | undefined 17 | >(undefined); 18 | const user = useContext(SessionContext); 19 | useEffect(() => { 20 | console.log("Listen for post change"); 21 | try { 22 | const listener = onSnapshot( 23 | query( 24 | collection(db, "posts").withConverter(PostConverter), 25 | where("createdBy.userId", "==", user.userId) 26 | ), 27 | (querySnapshot) => { 28 | const list = querySnapshot.docs.map((doc) => doc.data()); 29 | setChangePost(querySnapshot); 30 | }, 31 | (error) => { 32 | console.log("Listener inner", error); 33 | } 34 | ); 35 | 36 | return () => listener(); 37 | } catch (error) { 38 | console.log("Listener ", error); 39 | } 40 | }, [db]); 41 | 42 | return changePost; 43 | } 44 | 45 | export default UsePost; 46 | -------------------------------------------------------------------------------- /src/components/post/detail/post-detail.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useMemo, useState } from "react"; 2 | import HeaderComponent from "../header"; 3 | import PostAttachmentComponent from "../post-attachment"; 4 | import { withSession } from "../../../context/session"; 5 | import PostFooterComponent from "../footer"; 6 | import CommentsComponent from "../footer/comment/comments"; 7 | import ExpandedText from "../../expended-text"; 8 | 9 | export default function PostDetailComponent({ 10 | user, 11 | post, 12 | onAction = (actionType) => {}, 13 | }) { 14 | return ( 15 |
16 |
17 | 18 |
19 |
20 | 21 |
22 |
23 | 28 |
29 |
30 | 31 |
32 |
33 |
34 |
35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /src/components/model/model.js: -------------------------------------------------------------------------------- 1 | import cx from "classnames"; 2 | 3 | export function Modal({ 4 | children, 5 | className = "", 6 | width = 540, 7 | padding = true, 8 | active = false, 9 | setActive = (e) => {}, 10 | ...props 11 | }) { 12 | return ( 13 |
{ 15 | if (active) { 16 | setActive(false); 17 | } else { 18 | setActive(true); 19 | } 20 | }} 21 | onKeyUp={(e) => { 22 | if (e.key === "Escape") { 23 | setActive(false); 24 | } 25 | }} 26 | className={cx(`Modal`, { 27 | active: active, 28 | })} 29 | {...props} 30 | > 31 | { 33 | // stop the card being closed when we click on inner divs 34 | if (active) { 35 | e.stopPropagation(); 36 | } 37 | }} 38 | padding={padding} 39 | className={`overflow-hidden ` } 40 | style={{ maxWidth: width }} 41 | > 42 | {children} 43 | 44 |
45 | ); 46 | } 47 | 48 | function Card({ 49 | children, 50 | padding = true, 51 | className = "", 52 | style = {}, 53 | onClick = (e) => {}, 54 | }) { 55 | return ( 56 |
63 | {children} 64 |
65 | ); 66 | } 67 | -------------------------------------------------------------------------------- /src/components/sidebar/suggestions.js: -------------------------------------------------------------------------------- 1 | import Skeleton from "react-loading-skeleton"; 2 | import "react-loading-skeleton/dist/skeleton.css"; 3 | import { useEffect, useState } from "react"; 4 | import SuggestedProfile from "./suggested-profile"; 5 | import ProfileService from "../../services/profile"; 6 | import usePeople from "../../hook/use-people"; 7 | import { Link } from "react-router-dom"; 8 | import { RouteHelper } from "../../helper/routes.helper"; 9 | 10 | function Suggestions({ userId }) { 11 | const suggestedUsers = usePeople(userId, 10, 10); 12 | 13 | return ( 14 |
15 | {!suggestedUsers ? ( 16 | 17 | ) : suggestedUsers.length > 0 ? ( 18 | <> 19 |

20 | Suggestions 21 | 22 | See all 23 | 24 |

25 |
26 | {suggestedUsers.map((user, index) => ( 27 | 32 | ))} 33 |
34 | 35 | ) : null} 36 |
37 | ); 38 | } 39 | 40 | export default Suggestions; 41 | -------------------------------------------------------------------------------- /src/pages/profile.page.js: -------------------------------------------------------------------------------- 1 | import { useParams, useNavigate } from "react-router-dom"; 2 | import { useState, useEffect } from "react"; 3 | import ProfileService from "../services/profile"; 4 | import { withSession } from "../context/session"; 5 | import { RouteHelper } from "../helper/routes.helper"; 6 | import Loader from "../components/loader"; 7 | import Header from "../components/header"; 8 | import ProfileComponent from "../components/profile/"; 9 | import Skeleton from "react-loading-skeleton"; 10 | 11 | function ProfilePage({ user }) { 12 | const { username } = useParams(); 13 | const [profile, setProfile] = useState(); 14 | 15 | const history = useNavigate(); 16 | useEffect(() => { 17 | async function getProfile() { 18 | try { 19 | const data = await ProfileService.getProfileByUsername(username); 20 | if (data) { 21 | setProfile(data); 22 | } else { 23 | history(RouteHelper.NO_PAGE_FOUND); 24 | } 25 | } catch (error) { 26 | console.log("Error", error); 27 | } 28 | } 29 | if (username != null && username !== undefined) { 30 | getProfile(); 31 | } 32 | }, [username]); 33 | 34 | if (!profile) { 35 | return ; 36 | } 37 | return ( 38 |
39 |
40 | 41 |
42 | ); 43 | } 44 | 45 | export default withSession(ProfilePage); 46 | -------------------------------------------------------------------------------- /src/components/post/footer/index.js: -------------------------------------------------------------------------------- 1 | import { faHeart } from "@fortawesome/free-solid-svg-icons"; 2 | import PostActionComponent from "../post-action"; 3 | import { 4 | faHeart as emptyHeart, 5 | faCommentDots, 6 | } from "@fortawesome/free-regular-svg-icons"; 7 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; 8 | import { useRef, useState } from "react"; 9 | import cx from "classnames"; 10 | import FeedService from "../../../services/feed"; 11 | import AddCommentComponent from "./add-comment/add-comment.js"; 12 | import CommentsComponent from "./comment/comments"; 13 | import ExpandedText from "../../expended-text"; 14 | import { PostAction } from ".."; 15 | 16 | function PostFooterComponent({ user, post, onAction = (actionType) => {} }) { 17 | const [comments, setComments] = useState(post.comments); 18 | const commentInput = useRef(null); 19 | const handleFocus = () => commentInput.current.focus(); 20 | 21 | return ( 22 |
23 | 29 | {post.createdBy.fullname}  30 | 31 | 34 | onAction(PostAction.addComment, newComment) 35 | } 36 | commentInput={commentInput} 37 | /> 38 |
39 | ); 40 | } 41 | 42 | export default PostFooterComponent; 43 | -------------------------------------------------------------------------------- /src/components/loader.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const loaderStyleOuter = { 4 | position: "fixed", 5 | zIndex: 100, 6 | left: 0, 7 | right: 0, 8 | bottom: 0, 9 | display: "flex", 10 | justifyContent: "center", 11 | alignItems: "center", 12 | top: 0, 13 | }; 14 | 15 | const loaderStyleInner = { 16 | content: " ", 17 | display: "block", 18 | background: 0, 19 | borderRadius: "50%", 20 | width: "48px", 21 | height: "48px", 22 | margin: 0, 23 | boxSizing: "border-box", 24 | border: "2px solid #fff", 25 | borderColor: "#373d49 transparent #373d49 transparent", 26 | }; 27 | 28 | export default function Loader() { 29 | return ( 30 |
31 |
32 |
33 | ); 34 | } 35 | 36 | 37 | const spinnerStyle = (white) => ({ 38 | content: " ", 39 | display: "block", 40 | background: 0, 41 | borderRadius: "50%", 42 | width: "24px", 43 | height: "24px", 44 | margin: 0, 45 | boxSizing: "border-box", 46 | border: "2px solid #fff", 47 | borderColor: !white 48 | ? "#3B82F6 transparent #3B82F6 transparent" 49 | : "white transparent white transparent", 50 | }); 51 | 52 | function Spinner({ white = false , className }) { 53 | return ( 54 |
58 |
59 |
60 | ); 61 | } 62 | 63 | export {Spinner}; -------------------------------------------------------------------------------- /src/components/profile/index.js: -------------------------------------------------------------------------------- 1 | import { dispatch, useDispatch, useReducer } from "react"; 2 | import { useEffect } from "react"; 3 | import ProfileService from "../../services/profile"; 4 | import FeedService from "../../services/feed"; 5 | import Photos from "./photos"; 6 | import ProfileHeader from "./profile-header"; 7 | 8 | function ProfileComponent({ profile, loggedInUser }) { 9 | const reducer = (state, newState) => ({ ...state, ...newState }); 10 | 11 | const initialState = { 12 | postCollection: {}, 13 | followerCount: profile.followers?.length ?? 0, 14 | }; 15 | 16 | const [{ postCollection, followerCount }, dispatch] = useReducer( 17 | reducer, 18 | initialState 19 | ); 20 | useEffect(() => { 21 | document.title = "instagram - Profile"; 22 | async function getProfile() { 23 | const postCollection = await FeedService.getUserPostsByUsername( 24 | profile.username 25 | ); 26 | dispatch({ 27 | postCollection: postCollection, 28 | followerCount: profile.followers?.length ?? 0, 29 | }); 30 | } 31 | if (profile) { 32 | getProfile(); 33 | } 34 | }, [profile.username]); 35 | 36 | return ( 37 |
38 | 45 | 46 | 47 |
48 | ); 49 | } 50 | 51 | export default ProfileComponent; 52 | -------------------------------------------------------------------------------- /src/components/timeline/index.js: -------------------------------------------------------------------------------- 1 | import "react-loading-skeleton/dist/skeleton.css"; 2 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; 3 | import PostComponent from "../post"; 4 | import Skeleton from "react-loading-skeleton"; 5 | import UserFeeds from "../../hook/use-feed"; 6 | import { faCamera } from "@fortawesome/free-solid-svg-icons"; 7 | import { withFeed, withFeedProvider } from "../../context/feed"; 8 | 9 | function Timeline({ createPostRef, feed, isLoading }) { 10 | return ( 11 |
12 | {isLoading ? ( 13 | 14 | ) : feed.length > 0 ? ( 15 |
16 |
17 | {feed.map((post, index) => ( 18 | 19 | ))} 20 |
21 |
22 | ) : null} 23 | {!feed || } 24 |
25 | ); 26 | } 27 | 28 | function NoPostComponent({ feed, createPostRef }) { 29 | return ( 30 | feed.length === 0 && ( 31 |
32 | { 37 | createPostRef.current.click(); 38 | }} 39 | /> 40 |

No Posts Yet

41 |
42 | ) 43 | ); 44 | } 45 | export default withFeedProvider(withFeed(Timeline)); 46 | -------------------------------------------------------------------------------- /src/components/post/footer/add-comment/add-comment.js: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import { PostModel, Profile } from "../../../../models/index"; 3 | 4 | function AddCommentComponent({ 5 | user, 6 | commentInput, 7 | onNewCommentAdd = (e) => {}, 8 | }) { 9 | const [caption, setCaption] = useState(""); 10 | 11 | const handleSubmit = (e) => { 12 | e.preventDefault(); 13 | 14 | if (caption.length >= 1) { 15 | const comment = new PostModel({ 16 | id: "", 17 | caption: caption, 18 | createdBy: Profile.postUser( 19 | user.userId, 20 | user.fullname, 21 | user.username, 22 | user.avatar ?? "" 23 | ), 24 | createdAt: Date.now(), 25 | }); 26 | onNewCommentAdd(comment); 27 | setCaption(""); 28 | } 29 | }; 30 | return ( 31 |
32 |
37 | setCaption(e.target.value)} 44 | ref={commentInput} 45 | /> 46 | 53 |
54 |
55 | ); 56 | } 57 | 58 | export default AddCommentComponent; 59 | -------------------------------------------------------------------------------- /src/hook/use-user.tsx: -------------------------------------------------------------------------------- 1 | import { collection, onSnapshot, query, where } from "firebase/firestore"; 2 | import { useEffect, useState } from "react"; 3 | import { db } from "../lib/firebase"; 4 | import { Profile, ProfileConverter } from "../models/index"; 5 | import { getUserByUserId } from "../services/auth"; 6 | import UseAuthListener from "./use-auth-listener"; 7 | 8 | function UseUser(): Profile { 9 | const [activeUser, setActiveUser] = useState({}); 10 | const user = UseAuthListener(); 11 | useEffect(() => { 12 | async function getUserObjectByUserId() { 13 | const userObject = await getUserByUserId(user.uid); 14 | setActiveUser(userObject); 15 | } 16 | 17 | if (user?.uid) { 18 | getUserObjectByUserId(); 19 | } else { 20 | setActiveUser({}); 21 | } 22 | }, [user]); 23 | 24 | useEffect(() => { 25 | /// Listen for user data changes 26 | try { 27 | const listener = onSnapshot( 28 | query( 29 | collection(db, "users").withConverter(ProfileConverter), 30 | where("userId", "==", user.uid) 31 | ), 32 | (querySnapshot) => { 33 | const list = querySnapshot.docs.map((doc) => doc.data()); 34 | querySnapshot.docChanges().forEach((change) => { 35 | if (change.type === "added") { 36 | setActiveUser(list[0]); 37 | } 38 | if (change.type === "modified") { 39 | setActiveUser(list[0]); 40 | } 41 | if (change.type === "removed") { 42 | console.log("User Removed: ", list); 43 | } 44 | }); 45 | } 46 | ); 47 | 48 | return () => listener(); 49 | } catch (error) { 50 | console.log(error); 51 | } 52 | },[user]); 53 | 54 | return activeUser; 55 | } 56 | 57 | export default UseUser; 58 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-instagram", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@fortawesome/fontawesome-svg-core": "^1.2.36", 7 | "@fortawesome/free-regular-svg-icons": "^5.15.4", 8 | "@fortawesome/free-solid-svg-icons": "^5.15.4", 9 | "@fortawesome/react-fontawesome": "^0.1.16", 10 | "@testing-library/jest-dom": "^5.16.1", 11 | "@testing-library/react": "^11.2.7", 12 | "@testing-library/user-event": "^12.8.3", 13 | "classnames": "^2.3.1", 14 | "date-fns": "^2.27.0", 15 | "eslint": "^7.32.0", 16 | "firebase": "^9.6.1", 17 | "npm": "^8.3.0", 18 | "npm-run-all": "^4.1.5", 19 | "prop-types": "^15.7.2", 20 | "react": "^17.0.2", 21 | "react-dom": "^17.0.2", 22 | "react-loading-skeleton": "^3.0.1", 23 | "react-router-dom": "^6.1.1", 24 | "react-scripts": "4.0.3", 25 | "start": "^5.1.0", 26 | "web-vitals": "^1.1.2" 27 | }, 28 | "scripts": { 29 | "start": "react-scripts start", 30 | "build": "react-scripts build", 31 | "test": "react-scripts test", 32 | "eject": "react-scripts eject", 33 | "predeploy": "npm run build", 34 | "deploy": "gh-pages -d build", 35 | "lint": "eslint src/**/*.js src/**/*.jsx" 36 | }, 37 | "eslintConfig": { 38 | "extends": [ 39 | "react-app", 40 | "react-app/jest" 41 | ] 42 | }, 43 | "browserslist": { 44 | "production": [ 45 | ">0.2%", 46 | "not dead", 47 | "not op_mini all" 48 | ], 49 | "development": [ 50 | "last 1 chrome version", 51 | "last 1 firefox version", 52 | "last 1 safari version" 53 | ] 54 | }, 55 | "devDependencies": { 56 | "@types/react": "^17.0.37", 57 | "autoprefixer": "^10.4.0", 58 | "eslint-plugin-react": "^7.27.1", 59 | "eslint-plugin-react-hooks": "^4.3.0", 60 | "gh-pages": "^3.2.3", 61 | "sass": "^1.45.1" 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/components/post/footer/comment/comments.js: -------------------------------------------------------------------------------- 1 | import { Link } from "react-router-dom"; 2 | import { useState } from "react"; 3 | import { formatDistanceToNow } from "date-fns"; 4 | import ExpandedText from "../../../expended-text"; 5 | 6 | function CommentsComponent({ allComments, postedAt, displayAllComments }) { 7 | const [showAllComments, setShowAllComments] = useState( 8 | displayAllComments ?? false 9 | ); 10 | 11 | if (Array.isArray(allComments)) { 12 | allComments.sort(function (a, b) { 13 | return new Date(b.createdAt) - new Date(a.createdAt); 14 | }); 15 | } 16 | 17 | return ( 18 |
19 |
20 | {!showAllComments && allComments.length >= 3 && ( 21 |

setShowAllComments(!showAllComments)} 23 | className="text-sm text-gray-400 mb-1 select-none cursor-pointer" 24 | > 25 | View all {allComments.length} Comments 26 |

27 | )} 28 | {allComments 29 | .slice(0, showAllComments ? allComments.length : 3) 30 | .map((comment, index) => ( 31 |

32 | 33 | 34 | {" "} 35 | {comment.createdBy.fullname} 36 | 37 | 38 | 42 | {/* {comment.caption} */} 43 |

44 | ))} 45 |

46 | posted {formatDistanceToNow(postedAt)} ago 47 |

48 |
49 |
50 | ); 51 | } 52 | 53 | export default CommentsComponent; 54 | -------------------------------------------------------------------------------- /src/components/post/post-action/index.js: -------------------------------------------------------------------------------- 1 | import { faHeart } from "@fortawesome/free-solid-svg-icons"; 2 | import { 3 | faHeart as emptyHeart, 4 | faCommentDots, 5 | } from "@fortawesome/free-regular-svg-icons"; 6 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; 7 | import { useState } from "react"; 8 | import cx from "classnames"; 9 | import { PostAction } from ".."; 10 | 11 | function PostActionComponent({ 12 | user, 13 | post, 14 | onAction = (actionType) => {}, 15 | handleFocus, 16 | }) { 17 | const [isLiked, setIsLiked] = useState(post.likes.includes(user.userId)); 18 | 19 | const [likes, setLike] = useState(post.likes.length); 20 | return ( 21 |
22 |
23 | 38 | 39 |
handleFocus()} 41 | onKeyDown={(event) => { 42 | if (event.key === "Enter") { 43 | handleFocus(); 44 | } 45 | }} 46 | > 47 | 52 |
53 |
54 |

55 | {likes}  56 | {likes > 1 ? `likes` : `like`} 57 |

58 |
59 | ); 60 | } 61 | 62 | export default PostActionComponent; 63 | -------------------------------------------------------------------------------- /src/components/model/menu-action.component.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import { Modal } from "./model"; 3 | import cx from "classnames"; 4 | 5 | interface Prop { 6 | active: boolean; 7 | setActive: (_: boolean) => {}; 8 | actions: Array; 9 | } 10 | export const ActionLevel = Object.freeze({ default: 1, primary: 2, alert: 3 }); 11 | export class Action { 12 | label: String; 13 | action: () => {}; 14 | actionLevel: number; 15 | 16 | constructor( 17 | label: String, 18 | action: () => {}, 19 | actionLevel: number = ActionLevel.default 20 | ) { 21 | this.label = label; 22 | this.action = action; 23 | this.actionLevel = actionLevel; 24 | } 25 | } 26 | 27 | function PostMenuComponent({ active, setActive, actions }: Prop) { 28 | return ( 29 |
30 | { 34 | setActive(e); 35 | }} 36 | > 37 |
38 | {actions.map((action, index) => ( 39 | 40 | ))} 41 |
42 |
43 |
44 | ); 45 | } 46 | 47 | function ActionButton({ 48 | action, 49 | setActive, 50 | }: { 51 | action: Action; 52 | setActive: (_: boolean) => {}; 53 | }) { 54 | if(action == null || action === undefined){ 55 | return null; 56 | } 57 | return ( 58 |
{ 60 | action.action(); 61 | setActive(false); 62 | }} 63 | className={cx("p-2 rounded hover:bg-gray-100", { 64 | "text-gray-600": action.actionLevel === ActionLevel.default, 65 | "text-blue-500": action.actionLevel === ActionLevel.primary, 66 | "text-red-500": action.actionLevel === ActionLevel.alert, 67 | })} 68 | > 69 | {action.label} 70 |
71 | ); 72 | } 73 | 74 | export default PostMenuComponent; 75 | -------------------------------------------------------------------------------- /src/components/sidebar/suggested-profile.js: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import { Link } from "react-router-dom"; 3 | import { UserAvatar } from ".."; 4 | import ProfileService from "../../services/profile"; 5 | 6 | function SuggestedProfile({ user, loggedInUserId }) { 7 | const [followed, setFollowed] = useState( 8 | user.followers?.includes(loggedInUserId) ?? false 9 | ); 10 | 11 | async function handleFollowUser() { 12 | setFollowed(!followed); 13 | 14 | await ProfileService.updateMyFollowingUser( 15 | loggedInUserId, 16 | user.userId, 17 | followed 18 | ); 19 | } 20 | // if(followed){ 21 | // return null; 22 | // } 23 | 24 | return ( 25 |
26 |
27 | {/* */} 28 |
29 | {user.avatar != null ? ( 30 | A 31 | ) : user.fullname ? ( 32 | // user.fullname.substring(0, 2).toUpperCase() 33 | 38 | ) : ( 39 | "" 40 | )} 41 |
42 | 43 | 44 |

{user.fullname}

45 | 46 |
47 | 48 |
49 | 55 |
56 |
57 | ); 58 | } 59 | 60 | export default SuggestedProfile; 61 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import * as ROUTES from "./constants/routes"; 2 | 3 | import { BrowserRouter, Route, Routes } from "react-router-dom"; 4 | import { 5 | Dashboard, 6 | Login, 7 | NoPageFound, 8 | Profile, 9 | Signup, 10 | AccountSettings, 11 | EditProfile, 12 | getLoggedInRoute, 13 | getProtectedRoute, 14 | Explore, 15 | People, 16 | } from "./helper/routes.helper"; 17 | 18 | import Loader from "./components/loader"; 19 | import { Suspense } from "react"; 20 | import { withFirebase } from "./context/firebase"; 21 | import { withSession } from "./context/session"; 22 | import { withSessionProvider } from "./context/session"; 23 | 24 | function App({ user }) { 25 | return ( 26 | 27 | }> 28 | 29 | )} 32 | /> 33 | )} 36 | /> 37 | )} 40 | /> 41 | )} 44 | /> 45 | )} 48 | > 49 | } /> 50 | 51 | )} 54 | > 55 | } /> 56 | 57 | 58 | } /> 59 | 60 | 61 | 62 | ); 63 | } 64 | 65 | export default withFirebase(withSessionProvider(withSession(App))); 66 | -------------------------------------------------------------------------------- /src/services/firebase-storage.tsx: -------------------------------------------------------------------------------- 1 | import { db } from "../lib/firebase"; 2 | 3 | import { getStorage, ref, uploadBytes, getDownloadURL, uploadBytesResumable } from "firebase/storage"; 4 | 5 | 6 | 7 | class FirebaseStorageService { 8 | /** 9 | * Uplopad file to firebase storage 10 | * @param {File} file 11 | * @param {string} pathname 12 | * @param {function} onFileupload 13 | * @param {function} onUploadFail 14 | * @returns 15 | */ 16 | static async uploadFile(file: File, pathname: string, onFileupload = (e: string) => { }, onUploadFail = (e: string) => { },): Promise { 17 | try { 18 | const storage = getStorage(); 19 | const reference = ref(storage, `${pathname}/${file.name}`); 20 | const uploadTask = uploadBytesResumable(reference, file); 21 | uploadTask.on('state_changed', 22 | (snapshot) => { 23 | // Observe state change events such as progress, pause, and resume 24 | // Get task progress, including the number of bytes uploaded and the total number of bytes to be uploaded 25 | const progress = (snapshot.bytesTransferred / snapshot.totalBytes) * 100; 26 | console.log('Upload is ' + progress + '% done'); 27 | switch (snapshot.state) { 28 | case 'paused': 29 | console.log('Upload is paused'); 30 | break; 31 | case 'running': 32 | console.log('Upload is running'); 33 | break; 34 | } 35 | }, 36 | (error) => { 37 | // Handle unsuccessful uploads 38 | onUploadFail(error.message); 39 | }, 40 | async () => { 41 | const downloadURL = await getDownloadURL(uploadTask.snapshot.ref); 42 | console.log('File available at', downloadURL); 43 | onFileupload(downloadURL); 44 | }); 45 | return ""; 46 | } catch (e) { 47 | console.log(e); 48 | throw (e); 49 | } 50 | } 51 | 52 | } 53 | 54 | export default FirebaseStorageService; -------------------------------------------------------------------------------- /src/hook/use-people.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import { Profile } from "../models/profile"; 3 | import ProfileService from "../services/profile"; 4 | import useScroll from "./use-scroll"; 5 | 6 | /** 7 | * Get suggested users list 8 | * @param {string} userId Get suggested users list 9 | * @param {number} pageLimit set page limit 10 | * @param {number} maxLimit set max limit 11 | * @returns 12 | */ 13 | function usePeople(userId: string, pageLimit: number, maxLimit: number) { 14 | const [people, setpeople] = useState | undefined>(); 15 | const [lastId, setLastId] = useState(""); 16 | const [loadMore, setLoadMore] = useState(false); 17 | const [isLoading, setIsLoading] = useState(false); 18 | const [hasMoreData, setHasMoreData] = useState(true); 19 | 20 | useScroll(hasMoreData, loadMore, isLoading, setLoadMore); 21 | 22 | /// Restrict Effect to get people more then asked limit 23 | useEffect(() => { 24 | if (people && maxLimit && people.length == maxLimit) { 25 | setHasMoreData(false); 26 | setLoadMore(false); 27 | } 28 | }, [people]); 29 | 30 | /// Get people list from firebase 31 | useEffect(() => { 32 | async function fetchSuggestedProfiles() { 33 | setIsLoading(true); 34 | const list = await ProfileService.getSuggestedProfiles( 35 | userId, 36 | lastId, 37 | pageLimit 38 | ); 39 | if (list && list.length > 0) { 40 | setLastId(list.reverse()[0].userId); 41 | list.sort((a, b) => b.createdAt - a.createdAt); 42 | 43 | /// Check and set if all data is loaded 44 | setHasMoreData(list.length == pageLimit); 45 | 46 | /// Set people list for first time 47 | if (people === undefined) { 48 | setpeople(list); 49 | } else { 50 | /// Set people list for next time 51 | var newList = people.concat(list); 52 | setpeople(newList); 53 | } 54 | } else { 55 | setpeople([]); 56 | setHasMoreData(false); 57 | } 58 | setLoadMore(false); 59 | setIsLoading(false); 60 | } 61 | if (userId && !isLoading) { 62 | if (hasMoreData) { 63 | fetchSuggestedProfiles(); 64 | } else { 65 | console.log("No more data available"); 66 | } 67 | } 68 | }, [userId, loadMore]); 69 | 70 | return people; 71 | } 72 | 73 | export default usePeople; 74 | -------------------------------------------------------------------------------- /src/components/post/header.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import { Link } from "react-router-dom"; 3 | import { UserAvatar } from ".."; 4 | import { faHeart } from "@fortawesome/free-solid-svg-icons"; 5 | import { faEllipsisH } from "@fortawesome/free-solid-svg-icons"; 6 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; 7 | import PostMenuComponent, { 8 | Action, 9 | ActionLevel, 10 | } from "../model/menu-action.component"; 11 | import FeedService from "../../services/feed"; 12 | import ProfileService from "../../services/profile"; 13 | import { PostAction } from "."; 14 | 15 | export default function HeaderComponent({ 16 | user, 17 | post, 18 | onAction = (actionType) => {}, 19 | }) { 20 | const [active, setActive] = useState(false); 21 | const isMyPost = post.createdBy.userId === user.userId ?? false; 22 | 23 | return ( 24 |
25 | 29 | 34 |
35 |

{post.createdBy.username}

36 |

{post.location}

37 |
38 | 39 | 78 |
79 | ); 80 | } 81 | -------------------------------------------------------------------------------- /src/components/profile/photos.js: -------------------------------------------------------------------------------- 1 | import { 2 | faHeart as emptyHeart, 3 | faCommentDots, 4 | } from "@fortawesome/free-regular-svg-icons"; 5 | 6 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; 7 | import Skeleton from "react-loading-skeleton"; 8 | import cx from "classnames"; 9 | import { faCamera } from "@fortawesome/free-solid-svg-icons"; 10 | import { faHeart } from "@fortawesome/free-solid-svg-icons"; 11 | import { useState } from "react"; 12 | 13 | function Photos({ photos, user }) { 14 | return ( 15 |
16 |
17 | {!photos ? ( 18 | 19 | ) : photos.length > 0 ? ( 20 | photos.map((post, index) => ( 21 | 22 | )) 23 | ) : null} 24 |
25 | {!photos || 26 | (photos.length === 0 && ( 27 |
28 | 33 |

No Posts Yet

34 |
35 | ))} 36 |
37 | ); 38 | } 39 | 40 | function Photo({ post, user }) { 41 | const [isLiked, setIsLiked] = useState(post.likes.includes(user.userId)); 42 | return ( 43 |
44 |
45 | {'Image 50 |
51 |
54 |
55 | 62 |

63 | {post.likes.length} 64 |

65 |
66 |
67 | 71 |

72 | {post.likes.length} 73 |

74 |
75 |
76 |
77 | ); 78 | } 79 | export default Photos; 80 | -------------------------------------------------------------------------------- /src/components/post/index.js: -------------------------------------------------------------------------------- 1 | import HeaderComponent from "./header"; 2 | import PostAttachmentComponent from "./post-attachment"; 3 | import { withSession } from "../../context/session"; 4 | import PostFooterComponent from "./footer"; 5 | import { Modal } from "../model/model"; 6 | import PostDetail from "./detail/post-detail"; 7 | import { useEffect, useState } from "react"; 8 | import ExpandedText from "../expended-text"; 9 | import CommentsComponent from "./footer/comment/comments"; 10 | import FeedService from "../../services/feed"; 11 | import ProfileService from "../../services/profile"; 12 | 13 | export const PostAction = Object.freeze({ 14 | like: 1, 15 | addComment: 2, 16 | share: 3, 17 | delete: 4, 18 | report: 5, 19 | follow: 6, 20 | unfollow: 7, 21 | }); 22 | 23 | function PostComponent({ user, post }) { 24 | const [active, setActive] = useState(); 25 | 26 | const onAction = async (actionType, data) => { 27 | try { 28 | switch (actionType) { 29 | case PostAction.like: 30 | await FeedService.togglePostLike(post, user.userId); 31 | break; 32 | case PostAction.addComment: 33 | FeedService.addComment(data, post.id); 34 | break; 35 | case PostAction.delete: 36 | await FeedService.deletePost(post, user.userId); 37 | break; 38 | case PostAction.report: 39 | await FeedService.reportPost(post, user); 40 | console.log("Reported"); 41 | break; 42 | case PostAction.follow: 43 | await ProfileService.updateMyFollowingUser( 44 | user.userId, 45 | post.createdBy.userId, 46 | false 47 | ); 48 | console.log("user followed"); 49 | break; 50 | case PostAction.unfollow: 51 | await ProfileService.updateMyFollowingUser( 52 | user.userId, 53 | post.createdBy.userId, 54 | true 55 | ); 56 | console.log("user unfollowed"); 57 | break; 58 | 59 | default: 60 | break; 61 | } 62 | } catch (error) { 63 | console.error(error); 64 | } 65 | }; 66 | 67 | return ( 68 |
69 | 70 | 74 |
75 | 76 | 77 | 81 | 82 |
83 | 84 | 85 | 86 |
87 | ); 88 | } 89 | 90 | export default withSession(PostComponent); 91 | -------------------------------------------------------------------------------- /src/seed.tsx: -------------------------------------------------------------------------------- 1 | import { CommentModel, Profile, ProfileConverter } from "./models"; 2 | import { PostConverter, PostModel } from "./models/post"; 3 | 4 | // NOTE: replace 'bEIPMd9MPpX6nDrN29Qi0vIqYg123' with your Firebase auth user id (can be taken from Firebase) 5 | 6 | export async function seedDatabase( 7 | db: any, 8 | collection: any, 9 | doc: any, 10 | setDoc: any 11 | ) { 12 | const users = [ 13 | new Profile({ 14 | userId: "bEIPMd9MPpX6nDrN29Qi0vIqYg123", 15 | fullname: "John Doe", 16 | username: "johndoe", 17 | email: "jon.doe@gmail.com", 18 | avatar: "https://i.pravatar.cc/300", 19 | followers: ["bEIPMd9MPpX6nDrN2cdi0vIqYgabc"], 20 | createdAt: Date.now().toString(), 21 | }), 22 | new Profile({ 23 | userId: "bEIPMd9MPpX6nDrN2cdi0vIqYgabc", 24 | fullname: "Karl Sanzio", 25 | username: "karlsanzio", 26 | email: "karl.sanzio@gmail.com", 27 | avatar: "https://i.pravatar.cc/300", 28 | following: [ 29 | "bEIPMd9MPpX6nDrN29Qi0vIqYg123", 30 | "bEdvMd9MPpX6nDrN29Qi0vIqYgxyz", 31 | ], 32 | createdAt: Date.now().toString(), 33 | }), 34 | new Profile({ 35 | userId: "bEdvMd9MPpX6nDrN29Qi0vIqYgxyz", 36 | fullname: "Rachel Green", 37 | username: "rachelgreen", 38 | email: "rachel.green@gmail.com", 39 | avatar: "https://i.pravatar.cc/300", 40 | followers: ["bEIPMd9MPpX6nDrN2cdi0vIqYgabc"], 41 | createdAt: Date.now().toString(), 42 | }), 43 | new Profile({ 44 | userId: "bEIPMd9MPpX6nDrN29Qi0vIqYgpqr", 45 | fullname: "Raynolds", 46 | username: "raynolds", 47 | email: "raynolds@gmail.com", 48 | avatar: "https://i.pravatar.cc/300", 49 | createdAt: Date.now().toString(), 50 | }), 51 | ]; 52 | 53 | // eslint-disable-next-line prefer-const 54 | for (let k = 0; k < users.length; k++) { 55 | const ref = doc(collection(db, "users"), users[k].userId).withConverter( 56 | ProfileConverter 57 | ); 58 | 59 | await setDoc(ref, users[k]); 60 | } 61 | 62 | // const post = new PostModel({ 63 | // id: "bEIPMd9MPpX6nDrN29Qi0vIqYg123", 64 | // caption: "Saint George and the Dragon", 65 | // attachments: ["https://picsum.photos/500/500"], 66 | // likes: ["1", "2", "3"], 67 | // comments: [ 68 | // new CommentModel( 69 | // "bEIPMd9MPpX6nDr", 70 | // "This is ammazing", 71 | // ["1", "2", "3"], 72 | // // User who created the comment 73 | // Profile.postUser( 74 | // "bEIPMd9MPpX6nDrN29Qi0vIqYg123", 75 | // "John Doe", 76 | // "johndoe", 77 | // "https://i.pravatar.cc/300" 78 | // ), 79 | // // Date of the comment 80 | // Date.now() 81 | // ), 82 | // ], 83 | // createdBy: Profile.postUser( 84 | // "bEIPMd9MPpX6nDrN29Qi0vIqYg123", 85 | // "John Doe", 86 | // "johndoe", 87 | // "https://i.pravatar.cc/300" 88 | // ), 89 | // createdAt: Date.now(), 90 | // }); 91 | // for (let i = 1; i <= 5; ++i) { 92 | // const ref = doc(collection(db, "posts"), i.toString()).withConverter( 93 | // PostConverter 94 | // ); 95 | 96 | // await setDoc(ref, post); 97 | // } 98 | } 99 | -------------------------------------------------------------------------------- /src/models/profile.tsx: -------------------------------------------------------------------------------- 1 | import { identity } from "./identity"; 2 | 3 | interface Prop { 4 | userId: string; 5 | fullname: string; 6 | username: string; 7 | email?: string | null ; 8 | avatar?: string | null; 9 | bio?: string | null; 10 | website?: string | null; 11 | following?: String[] | null; 12 | followers?: String[] | null; 13 | createdAt?: any | null; 14 | } 15 | class Profile { 16 | userId: string; 17 | fullname: string; 18 | username: string; 19 | email: String | null | undefined; 20 | bio: string | null | undefined; 21 | website: string | null | undefined; 22 | following: String[] | null | undefined; 23 | followers: String[] | null | undefined; 24 | createdAt: any | null | undefined; 25 | avatar: string | null | undefined; 26 | 27 | constructor({ 28 | userId, 29 | fullname, 30 | username, 31 | email, 32 | website, 33 | bio, 34 | avatar, 35 | following, 36 | followers, 37 | createdAt, 38 | }: Prop) { 39 | this.userId = userId; 40 | this.fullname = fullname; 41 | this.username = username; 42 | this.email = email; 43 | this.website = website; 44 | this.bio = bio; 45 | this.following = following; 46 | this.followers = followers; 47 | this.createdAt = createdAt; 48 | this.avatar = avatar; 49 | } 50 | 51 | toString() { 52 | return this.fullname + ", " + this.username + ", " + this.email; 53 | } 54 | 55 | static postUser( 56 | userId: string, 57 | fullname: string, 58 | username: string, 59 | avatar: string 60 | ): Profile { 61 | return new Profile({ 62 | userId: userId, 63 | fullname: fullname, 64 | username: username, 65 | avatar: avatar, 66 | }); 67 | } 68 | } 69 | 70 | // Firestore data converter 71 | const ProfileConverter = { 72 | toFirestore: (user: any) => { 73 | return { 74 | userId: user.userId, 75 | avatar: identity(user.avatar), 76 | fullname: identity(user.fullname), 77 | username: identity(user.username), 78 | email: identity(user.email), 79 | website: identity(user.website), 80 | bio: identity(user.bio), 81 | following: identity(user.following), 82 | followers: identity(user.followers), 83 | createdAt: identity(user.createdAt), 84 | }; 85 | }, 86 | fromFirestore: (snapshot: any, options: any) => { 87 | const data = snapshot.data(options); 88 | return new Profile({ 89 | userId: snapshot.id, 90 | avatar: data.avatar, 91 | fullname: data.fullname, 92 | username: data.username, 93 | email: data.email, 94 | website: data.website, 95 | bio: data.bio, 96 | following: data.following, 97 | followers: data.followers, 98 | createdAt: data.createdAt, 99 | }); 100 | }, 101 | }; 102 | 103 | const PostProfileConverter = { 104 | toFirestore: (user: any) => { 105 | return { 106 | userId: user.userId, 107 | avatar: identity(user.avatar), 108 | fullname: identity(user.fullname), 109 | username: identity(user.username), 110 | }; 111 | }, 112 | fromFirestore: (data: any) => { 113 | return new Profile({ 114 | userId: data.userId, 115 | fullname: data.fullname, 116 | username: data.username, 117 | avatar: data.avatar, 118 | }); 119 | }, 120 | }; 121 | 122 | 123 | 124 | export { Profile, ProfileConverter, PostProfileConverter }; 125 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Instagram Clone 2 | 3 | Instagram clone built in [Reactjs](https://reactjs.org/) using Firebase Auth and Firestore db. 4 | 5 | [![Hits](https://hits.seeyoufarm.com/api/count/incr/badge.svg?url=https%3A%2F%2Fgithub.com%2FTheAlphamerc%2Freact-instagram&count_bg=%2379C83D&title_bg=%23555555&icon=&icon_color=%23E7E7E7&title=hits&edge_flat=false)](https://hits.seeyoufarm.com) [![Open Source Love](https://badges.frapsoft.com/os/v2/open-source.svg?v=103)](https://github.com/Thealphamerc/react-instagram) [![GitHub stars](https://img.shields.io/github/stars/Thealphamerc/react-instagram?style=social)](https://github.com/login?return_to=https://github.com/FTheAlphamerc/react-instagram) 6 | ![GitHub forks](https://img.shields.io/github/forks/TheAlphamerc/react-instagram?style=social) 7 | 8 | ## Live Demo 9 | https://react-instagram-9284e.web.app 10 | 11 | ### Steps to run 12 | 13 | `npm install` 14 | 15 | `npm run start` 16 | 17 | 18 | ## Web screnshots 19 | | | | 20 | | ------------- |:-------------:| 21 | | | | 22 | | | | 23 | | | | 24 | 25 | 26 | 27 | ## Tablet Screenshot 28 | | | | 29 | | ------------- |:-------------:| 30 | | | | 31 | | | | 32 | 33 | 34 | 35 | ## Mobile Screenshot 36 | | | | | | 37 | | ------------- |:-------------|:------------- |:-------------| 38 | | | | || 39 | 40 | 41 | 42 | 43 | 44 | ## Created & Maintained By 45 | 46 | [Sonu Sharma](https://github.com/TheAlphamerc) ([Twitter](https://www.twitter.com/TheAlphamerc)) ([Youtube](https://www.youtube.com/user/sonusharma045sonu/)) ([Insta](https://www.instagram.com/_sonu_sharma__)) ([Dev.to](https://dev.to/thealphamerc)) 47 | ![Twitter Follow](https://img.shields.io/twitter/follow/thealphamerc?style=social) 48 | 49 | > If you found this project helpful or you learned something from the source code and want to thank me, consider buying me a cup of :coffee: 50 | > 51 | 52 | > * 53 | > * [PayPal](https://www.paypal.me/TheAlphamerc/) 54 | 55 | > You can also nominate me for Github Star developer program 56 | > https://stars.github.com/nominate 57 | 58 | -------------------------------------------------------------------------------- /src/models/post.tsx: -------------------------------------------------------------------------------- 1 | // import { CommentModel, CommentConverter } from "."; 2 | import { ProfileConverter } from "."; 3 | import { identity } from "./identity"; 4 | import { PostProfileConverter, Profile } from "./profile"; 5 | 6 | interface Prop { 7 | id: string; 8 | caption?: string; 9 | attachments?: string[]; 10 | location?: string; 11 | likes?: string[]; 12 | comments?: PostModel[]; 13 | createdAt: any; 14 | createdBy: Profile; 15 | reportedBy?: Profile[]; 16 | } 17 | class PostModel { 18 | id: string; 19 | caption: string; 20 | attachments: string[] | undefined; 21 | location: string | undefined; 22 | likes: string[] | undefined; 23 | comments: PostModel[] | undefined; 24 | createdAt: any; 25 | createdBy: Profile; 26 | reportedBy: Profile[] | undefined; 27 | constructor({ 28 | id, 29 | caption = "", 30 | attachments, 31 | location, 32 | createdBy, 33 | createdAt, 34 | likes, 35 | comments, 36 | reportedBy, 37 | }: Prop) { 38 | this.id = id; 39 | this.caption = caption; 40 | this.attachments = attachments; 41 | this.location = location; 42 | this.comments = comments; 43 | this.likes = likes; 44 | this.createdAt = createdAt; 45 | this.createdBy = createdBy; 46 | this.reportedBy = reportedBy; 47 | } 48 | } 49 | 50 | // Firestore data converter 51 | const PostConverter = { 52 | toFirestore: (post: any) => { 53 | return { 54 | id: identity(post.id), 55 | caption: identity(post.caption), 56 | attachments: identity(post.attachments), 57 | likes: identity(post.likes), 58 | location: identity(post.location), 59 | comments: Array.isArray(post.comments) 60 | ? post.comments.map((com: any) => PostConverter.toFirestore(com)) 61 | : [], 62 | createdBy: PostProfileConverter.toFirestore(post.createdBy), 63 | createdAt: post.createdAt, 64 | reportedBy: Array.isArray(post.reportedBy) 65 | ? post.reportedBy.map((user: any) => ProfileConverter.toFirestore(user)) 66 | : [], 67 | }; 68 | }, 69 | fromFirestore: (snapshot: any, options: any) => { 70 | const data = snapshot.data(options); 71 | return new PostModel({ 72 | id: snapshot.id, 73 | caption: data.caption, 74 | attachments: data.attachments, 75 | location: data.location, 76 | likes: data.likes, 77 | comments: Array.isArray(data.comments) 78 | ? data.comments.map((com: any) => CommentConverter.fromFirestore(com)) 79 | : [], 80 | createdBy: PostProfileConverter.fromFirestore(data.createdBy), 81 | createdAt: data.createdAt, 82 | reportedBy: Array.isArray(data.comments) 83 | ? data.comments.map((com: any) => 84 | PostProfileConverter.fromFirestore(com) 85 | ) 86 | : [], 87 | }); 88 | }, 89 | }; 90 | 91 | const CommentConverter = { 92 | toFirestore: (post: any) => { 93 | return { 94 | id: identity(post.id), 95 | caption: identity(post.caption), 96 | likes: identity(post.likes), 97 | createdBy: PostProfileConverter.toFirestore(post.createdBy), 98 | createdAt: post.createdAt, 99 | }; 100 | }, 101 | fromFirestore: (comment: any) => { 102 | return new PostModel({ 103 | id: comment.id, 104 | caption: comment.caption, 105 | likes: comment.likes, 106 | createdBy: PostProfileConverter.fromFirestore(comment.createdBy), 107 | createdAt: comment.createdAt, 108 | }); 109 | }, 110 | }; 111 | 112 | export { PostModel, PostConverter }; 113 | -------------------------------------------------------------------------------- /src/hook/use-feed.tsx: -------------------------------------------------------------------------------- 1 | import { useContext, useEffect, useState } from "react"; 2 | import { SessionContext } from "../context/session"; 3 | import { PostModel } from "../models/post"; 4 | import FeedService from "../services/feed"; 5 | import UsePost from "./use-post"; 6 | import useScroll from "./use-scroll"; 7 | 8 | function UserFeeds(): { 9 | feed: PostModel[] | undefined; 10 | isLoading: boolean; 11 | } { 12 | const [feed, setFeeds] = useState>(); 13 | const querySnapshot = UsePost(); 14 | const [lastId, setLastId] = useState(""); 15 | const [loadMore, setLoadMore] = useState(false); 16 | const [isLoading, setIsLoading] = useState(true); 17 | const [hasMoreData, setHasMoreData] = useState(true); 18 | const pageLimit = 10; 19 | 20 | const user = useContext(SessionContext); 21 | 22 | useScroll(hasMoreData, loadMore, isLoading, setLoadMore); 23 | 24 | /// Fetch feeds 25 | useEffect(() => { 26 | async function getTimelineFeed() { 27 | setIsLoading(true); 28 | let following: string[] = user != undefined ? user.following ?? [] : []; 29 | const list = await FeedService.getTimeLineFeed( 30 | [user.userId, ...following], 31 | lastId, 32 | pageLimit 33 | ); 34 | if (list.length > 0) { 35 | setLastId(list.reverse()[0].createdAt); 36 | 37 | list.sort((a, b) => b.createdAt - a.createdAt); 38 | 39 | setHasMoreData(list && list.length == pageLimit); 40 | if (feed === undefined) { 41 | setFeeds(list); 42 | } else { 43 | var newList = feed.concat(list); 44 | setFeeds(newList); 45 | } 46 | } else { 47 | setFeeds([]); 48 | } 49 | setLoadMore(false); 50 | setIsLoading(false); 51 | } 52 | 53 | if (user != undefined && user.userId != undefined) { 54 | if (hasMoreData) { 55 | getTimelineFeed(); 56 | } else { 57 | console.log("No more data available"); 58 | } 59 | } else { 60 | // setFeeds(undefined); 61 | console.log("Something went wrong"); 62 | } 63 | }, [user, loadMore]); 64 | 65 | // Update feed if [post] is updated 66 | useEffect(() => { 67 | if (querySnapshot != undefined) { 68 | var list = feed; 69 | querySnapshot.docChanges().forEach((change) => { 70 | if (feed != undefined && list != undefined) { 71 | var isExist = feed.some((p) => p.id == change.doc.id); 72 | if (change.type === "added") { 73 | if (!isExist) { 74 | list?.unshift(change.doc.data()); 75 | console.log("Post added: ", change.doc.data()); 76 | setFeeds(list); 77 | } 78 | } 79 | if (change.type === "modified") { 80 | if (isExist) { 81 | let index = list?.findIndex((p) => p.id == change.doc.id); 82 | if (index != undefined) { 83 | list[index] = change.doc.data(); 84 | setFeeds(list); 85 | console.log("Post Modified: ", change.doc.data()); 86 | } 87 | } 88 | } 89 | if (change.type === "removed") { 90 | console.log("Post Removed: ", change.doc.data()); 91 | if (isExist) { 92 | list?.filter((item) => item.id != change.doc.id); 93 | setFeeds(list); 94 | } 95 | } 96 | } 97 | }); 98 | } 99 | }, [querySnapshot]); 100 | 101 | return { feed, isLoading: isLoading }; 102 | } 103 | 104 | export default UserFeeds; 105 | -------------------------------------------------------------------------------- /src/services/profile.tsx: -------------------------------------------------------------------------------- 1 | import { db } from "../lib/firebase"; 2 | import { 3 | collection, 4 | getDocs, 5 | limit, 6 | updateDoc, 7 | arrayUnion, 8 | arrayRemove, 9 | query, 10 | doc, 11 | where, 12 | orderBy, 13 | startAfter, 14 | } from "firebase/firestore"; 15 | 16 | import { Profile, ProfileConverter } from "../models/index"; 17 | 18 | class ProfileService { 19 | /** 20 | * Update followers list in logged in user 21 | * @param {string} loggedInUserId 22 | * @param {string} secondUserId 23 | * @param {bool} isFollow 24 | * @returns 25 | */ 26 | static async updateMyFollowingUser( 27 | loggedInUserId: string, 28 | secondUserId: string, 29 | isFollow: boolean 30 | ) { 31 | try { 32 | if (isFollow) { 33 | await updateDoc(doc(collection(db, "users"), loggedInUserId), { 34 | following: arrayRemove(secondUserId), 35 | }); 36 | await updateDoc(doc(collection(db, "users"), secondUserId), { 37 | followers: arrayRemove(loggedInUserId), 38 | }); 39 | } else { 40 | await updateDoc(doc(collection(db, "users"), loggedInUserId), { 41 | following: arrayUnion(secondUserId), 42 | }); 43 | await updateDoc(doc(collection(db, "users"), secondUserId), { 44 | followers: arrayUnion(loggedInUserId), 45 | }); 46 | } 47 | } catch (e) { 48 | console.log(e); 49 | throw e; 50 | } 51 | } 52 | 53 | /** 54 | * Get suggested users list 55 | * @param {string} userId 56 | * @param {string} after 57 | * @param {number} limit 58 | * @returns 59 | */ 60 | static async getSuggestedProfiles( 61 | userId: string, 62 | after: String, 63 | pageLimit: number 64 | ): Promise { 65 | try { 66 | const querySnapshot = query( 67 | collection(db, "users").withConverter(ProfileConverter), 68 | where("userId", "!=", userId), 69 | orderBy("userId"), 70 | // orderBy("createdAt"), 71 | startAfter(after), 72 | limit(pageLimit) 73 | ); 74 | const docs = await getDocs(querySnapshot); 75 | const list = docs.docs.map((doc) => doc.data()); 76 | return list; 77 | } catch (e) { 78 | console.log(e); 79 | throw e; 80 | } 81 | } 82 | 83 | /** 84 | * Get user profile using username 85 | * @param {string} username 86 | * @returns 87 | */ 88 | static async getProfileByUsername(username: string): Promise { 89 | if (username === "" || username === undefined || username === null) { 90 | throw "username is not valid"; 91 | } 92 | console.log("REading profile for", username); 93 | const querySnapshot = query( 94 | collection(db, "users").withConverter(ProfileConverter), 95 | where("username", "==", username) 96 | ); 97 | const docs = await getDocs(querySnapshot); 98 | const profile = docs.docs[0].data(); 99 | return profile; 100 | } 101 | 102 | /** 103 | * Update user profile 104 | * @param {string} userId 105 | * @param {string} fullname 106 | * @param {string} bio 107 | * @param {string} website 108 | * @param {string} profileImage 109 | * @returns 110 | */ 111 | static async updateProfile( 112 | userId: string, 113 | fullname: string, 114 | bio: string, 115 | website: string, 116 | profileImage: string 117 | ) { 118 | try { 119 | await updateDoc(doc(collection(db, "users"), userId), { 120 | fullname: fullname, 121 | bio: bio, 122 | website: website, 123 | // profileImage: profileImage, 124 | }); 125 | } catch (e) { 126 | console.log(e); 127 | throw e; 128 | } 129 | } 130 | } 131 | 132 | export default ProfileService; 133 | -------------------------------------------------------------------------------- /src/pages/login.page.js: -------------------------------------------------------------------------------- 1 | import * as Route from "../constants/routes"; 2 | 3 | import Loader, { Spinner } from "../components/loader"; 4 | import { Suspense, useEffect, useState } from "react"; 5 | 6 | import { Link } from "react-router-dom"; 7 | import cx from "classnames"; 8 | import { loginWithEmailPassword } from "../services/auth"; 9 | import { useNavigate } from "react-router-dom"; 10 | 11 | function Login() { 12 | const history = useNavigate(); 13 | 14 | const [email, setEmail] = useState(""); 15 | const [password, setPassword] = useState(""); 16 | const [error, setError] = useState(""); 17 | const [loading, setLoading] = useState(false); 18 | 19 | const handleLogin = async (e) => { 20 | e.preventDefault(); 21 | try { 22 | setLoading(true); 23 | await loginWithEmailPassword(email, password); 24 | history(Route.DASHBOARD); 25 | console.log("login success"); 26 | } catch (e) { 27 | setError(e); 28 | } finally { 29 | setLoading(false); 30 | } 31 | }; 32 | 33 | useEffect(() => { 34 | document.title = "Login - instagram"; 35 | }, []); 36 | 37 | return ( 38 | }> 39 |
40 |
41 | iPhone 46 |
47 |
48 |
49 |

50 | instagram 55 |

56 | {error && ( 57 |

{error}

58 | )} 59 | 60 |
61 | setEmail(e.target.value)} 68 | /> 69 | setPassword(e.target.value)} 76 | autoComplete="off" 77 | /> 78 | 90 |
91 |
92 |
93 |

94 | Don't have an account?{" "} 95 | 99 | Sign up 100 | 101 |

102 |
103 |
104 |
105 |
106 | ); 107 | } 108 | 109 | export default Login; 110 | -------------------------------------------------------------------------------- /src/components/header.js: -------------------------------------------------------------------------------- 1 | import * as ROUTES from "../constants/routes"; 2 | 3 | import { 4 | faHome, 5 | faPlus, 6 | faSignOutAlt, 7 | } from "@fortawesome/free-solid-svg-icons"; 8 | import { getAuth, signOut } from "firebase/auth"; 9 | 10 | import CreatePostModelComponent from "../components/create-post/create-post-model-component"; 11 | import { FontAwesomeIcon} from "@fortawesome/react-fontawesome"; 12 | import { Link } from "react-router-dom"; 13 | import { useState } from "react"; 14 | import { UserAvatar } from "."; 15 | 16 | function Header({ user, createPostRef }) { 17 | const isLogout = user && Object.keys(user).length === 0; 18 | const [createPostModel, setCreatePostModel] = useState(false); 19 | 20 | return ( 21 |
22 | 27 | 28 |
29 |
30 | {/* LOGO */} 31 |
32 | 33 | instagram 38 | 39 |
40 | {/* Home | Logout | User */} 41 |
42 | {!isLogout ? ( 43 | <> 44 | 48 | 49 | 50 |
{ 54 | setCreatePostModel(true); 55 | }} 56 | > 57 | 58 |
59 | 71 | {!user.fullname ? null : ( 72 |
73 | 74 | 79 | 80 |
81 | )} 82 | 83 | ) : ( 84 | <> 85 | 89 | 92 | 93 | 94 | 98 | 101 | 102 | 103 | )} 104 |
105 |
106 |
107 |
108 | ); 109 | } 110 | 111 | export default Header; 112 | -------------------------------------------------------------------------------- /src/services/auth.tsx: -------------------------------------------------------------------------------- 1 | import { db } from "../lib/firebase"; 2 | import { 3 | collection, 4 | getDocs, 5 | getDoc, 6 | query, 7 | doc, 8 | setDoc, 9 | where, 10 | } from "firebase/firestore"; 11 | import { 12 | updateProfile, 13 | getAuth, 14 | createUserWithEmailAndPassword, 15 | signInWithEmailAndPassword, 16 | UserCredential, 17 | } from "firebase/auth"; 18 | import { Profile, ProfileConverter } from "../models/index"; 19 | 20 | async function doesUsernameExist(username: string) { 21 | try { 22 | const querySnapshot = query( 23 | collection(db, "users"), 24 | where("username", "==", username) 25 | ); 26 | const docs = await getDocs(querySnapshot); 27 | return docs.docs.length > 0; 28 | } catch (e) { 29 | console.log(e); 30 | throw Error("Something went wrong"); 31 | } 32 | } 33 | 34 | async function loginWithEmailPassword( 35 | email: string, 36 | password: string 37 | ): Promise { 38 | const auth = getAuth(); 39 | try { 40 | let userCredential = await signInWithEmailAndPassword( 41 | auth, 42 | email, 43 | password 44 | ).catch((error) => { 45 | console.log(error.code); 46 | switch (error.code) { 47 | case "auth/invalid-email": 48 | error = "Email or password is invalid"; 49 | break; 50 | case "auth/user-not-found": 51 | error = "User not exists"; 52 | break; 53 | case "auth/wrong-password": 54 | error = "Incorrect password"; 55 | break; 56 | 57 | case "auth/too-many-requests": 58 | error = "You have tried too many times. Please try again later"; 59 | break; 60 | 61 | default: 62 | error = "Some thing went wrong. Please try again later"; 63 | break; 64 | } 65 | throw error; 66 | }); 67 | return userCredential; 68 | } catch (error) { 69 | console.log(error); 70 | throw error; 71 | } 72 | } 73 | 74 | async function createUser( 75 | username: string, 76 | fullname: string, 77 | email: string, 78 | password: string 79 | ): Promise { 80 | try { 81 | const auth = getAuth(); 82 | const userCredential = await createUserWithEmailAndPassword( 83 | auth, 84 | email, 85 | password 86 | ).catch((error) => { 87 | console.log(error); 88 | switch (error.code) { 89 | case "auth/invalid-email": 90 | error = "Email or password is invalid"; 91 | break; 92 | case "auth/weak-password": 93 | error = "Password is week"; 94 | break; 95 | case "auth/email-already-in-use": 96 | error = "Email already in use"; 97 | break; 98 | 99 | default: 100 | console.log(error.code); 101 | console.log(error.message); 102 | error = "Some thing went wrong. Please try again later"; 103 | break; 104 | } 105 | throw error; 106 | }); 107 | // authentication 108 | // -> email, password & username (displyname) 109 | const profile = new Profile({ 110 | userId: userCredential.user.uid, 111 | fullname: fullname, 112 | username: username.toLowerCase(), 113 | email: email, 114 | createdAt: Date.now(), 115 | followers: [], 116 | following: [], 117 | }); 118 | 119 | const ref = doc(collection(db, "users"), profile.userId).withConverter( 120 | ProfileConverter 121 | ); 122 | 123 | await setDoc(ref, profile); 124 | const currentUser = auth.currentUser; 125 | if (currentUser) { 126 | await updateProfile(currentUser, { displayName: profile.fullname }); 127 | } 128 | 129 | return profile; 130 | } catch (e) { 131 | console.log(e); 132 | throw e; 133 | } 134 | } 135 | 136 | async function getUserByUserId(userId: string): Promise { 137 | try { 138 | const ref = doc(collection(db, "users"), userId).withConverter( 139 | ProfileConverter 140 | ); 141 | const profileSnap = await getDoc(ref); 142 | if (profileSnap.exists()) { 143 | return profileSnap.data(); 144 | } else { 145 | throw new Error("User not found"); 146 | } 147 | } catch (e) { 148 | console.log(e); 149 | throw e; 150 | } 151 | } 152 | 153 | export { 154 | doesUsernameExist, 155 | createUser, 156 | loginWithEmailPassword, 157 | getUserByUserId, 158 | }; 159 | -------------------------------------------------------------------------------- /src/services/feed.tsx: -------------------------------------------------------------------------------- 1 | import { db } from "../lib/firebase"; 2 | import { Profile, ProfileConverter } from "../models/index"; 3 | import { 4 | collection, getDocs, limit, updateDoc, arrayUnion, arrayRemove, query, doc, where, deleteDoc,setDoc, orderBy, startAfter 5 | } from "firebase/firestore"; 6 | 7 | 8 | import { CommentConverter, CommentModel } from "../models/index"; 9 | import { PostConverter, PostModel } from "../models/post"; 10 | import { PostProfileConverter } from "../models/profile"; 11 | 12 | class FeedService { 13 | // Get user following posts 14 | static async getTimeLineFeed(following: string[],after:String, postLimit:number): Promise { 15 | try { 16 | var fol = following.slice(0, 9); 17 | const querySnapshot = query( 18 | collection(db, "posts").withConverter(PostConverter), 19 | where("createdBy.userId", "in", fol), 20 | orderBy("createdAt", "desc"), 21 | startAfter(after), 22 | limit(postLimit) 23 | ); 24 | const docs = await getDocs(querySnapshot) 25 | if (docs.docs.length === 0) { 26 | return []; 27 | } 28 | const list = docs.docs.map((doc) => doc.data()) 29 | return list; 30 | 31 | } catch (e) { 32 | console.log(e); 33 | throw (e); 34 | } 35 | } 36 | 37 | // Increase/Decrease the number of likes of a post 38 | static async togglePostLike(post: PostModel, userId: string): Promise { 39 | try { 40 | const likes = post.likes ?? []; 41 | const isLiked = likes.includes(userId); 42 | if (isLiked) { 43 | await updateDoc(doc(collection(db, "posts"), post.id), { 44 | likes: arrayRemove(userId) 45 | }); 46 | 47 | console.log(`Post unliked ${post.id}`); 48 | } else { 49 | await updateDoc(doc(collection(db, "posts"), post.id), { 50 | likes: arrayUnion(userId) 51 | }); 52 | console.log(`Post liked ${post.id}`); 53 | } 54 | } catch (e) { 55 | console.log(e); 56 | throw (e); 57 | } 58 | } 59 | 60 | // Add new comment in a post 61 | static async addComment(comment: CommentModel, postId: string): Promise { 62 | try { 63 | const map = CommentConverter.toFirestore(comment); 64 | await updateDoc(doc(collection(db, "posts").withConverter(PostConverter), postId), { 65 | comments: arrayUnion(map) 66 | }); 67 | } catch (e) { 68 | console.log(e); 69 | throw (e); 70 | } 71 | } 72 | 73 | // Delete a post if created by the logged in user 74 | static async deletePost(post: PostModel, userId: string): Promise { 75 | try { 76 | if (post.createdBy.userId === userId) { 77 | await deleteDoc(doc(collection(db, "posts"), post.id)); 78 | } 79 | } catch (e) { 80 | console.log(e); 81 | throw (e); 82 | } 83 | } 84 | 85 | // Get user's post 86 | static async getUserPostsByUsername(username: string): Promise { 87 | try { 88 | const querySnapshot = query( 89 | collection(db, "posts").withConverter(PostConverter), 90 | where("createdBy.username", "==", username), 91 | limit(10) 92 | ); 93 | const docs = await getDocs(querySnapshot) 94 | const list = docs.docs.map((doc) => doc.data()) 95 | return list; 96 | } catch (e) { 97 | console.log(e); 98 | throw (e); 99 | } 100 | } 101 | 102 | // Create post 103 | static async createPost(post: PostModel): Promise { 104 | try { 105 | console.log("Post saving start"); 106 | const ref = doc(collection(db, "posts").withConverter(PostConverter)); 107 | await setDoc(ref, post); 108 | } catch (e) { 109 | console.log(e); 110 | throw (e); 111 | } 112 | } 113 | 114 | // Report a post 115 | static async reportPost(post: PostModel, Profile: Profile): Promise { 116 | try { 117 | const map = PostProfileConverter.toFirestore(Profile); 118 | await updateDoc(doc(collection(db, "posts"), post.id).withConverter(PostConverter), { 119 | reportedBy: arrayUnion(map) 120 | }); 121 | } catch (e) { 122 | console.log(e); 123 | throw (e); 124 | } 125 | } 126 | } 127 | 128 | export default FeedService; -------------------------------------------------------------------------------- /.firebase/hosting.YnVpbGQ.cache: -------------------------------------------------------------------------------- 1 | asset-manifest.json,1640418010338,46ada1fcd9001b5cd70a23d014fb2545589e2681a26c0c6ace54bcae8e1ec511 2 | index.html,1640418010338,d9e7b5c7852e9652eb4f53937b693586379c503558a118ab5ffb046786fff819 3 | images/logo.png,1640418001171,e71b9a7bccb667171d051b9c690c666db19e8c2cdcf7e1b69d9c70156a6fbc77 4 | favicon.ico,1640418001166,cd1851c84e8e4196274084e1d7f8e87e8bd513be25e4e6bd97dad364283c7aa9 5 | images/users/logo.png,1640418001171,e71b9a7bccb667171d051b9c690c666db19e8c2cdcf7e1b69d9c70156a6fbc77 6 | images/avatars/default.png,1640418001168,3db4f8cd069ca8a97d6482493ff5d5aec1f5502c3d331be38b78186f1ec627b4 7 | static/css/7.83b5cdb7.chunk.css,1640418010330,3a42bee3883269cd0360bff1b180b6ce2e4ecf39dee6421aefcbbdf7c2f4bbdc 8 | static/css/7.83b5cdb7.chunk.css.map,1640418010338,bbf6ec030e066ce1996fc2c3c5fd47423e211f824d2c11904d2c4c9620de99d2 9 | static/js/0.28b1e12d.chunk.js.LICENSE.txt,1640418010330,e8826907045043afda5db9f94ceb4b5b72a5fa73fd5f19217c6000ddb657b54b 10 | static/js/1.6c39a24e.chunk.js,1640418010331,443ec7ddcade7651717544e4a8207de0fbd793963ea0dcc2c7de06beb37adbad 11 | static/js/1.6c39a24e.chunk.js.LICENSE.txt,1640418010330,2048ce3c8deded975f098493238139068382ead6a43efed9ab79114809e15b70 12 | static/js/10.d026e722.chunk.js,1640418010331,3d7742256f54fb515760c5592d2ce51541d1047144bddfca1e7663b2b7b41ae9 13 | static/js/10.d026e722.chunk.js.LICENSE.txt,1640418010330,5c92efd90d1294942abdce383f928741d25a69dbf4daa7a67d6fe6de9f75b24d 14 | static/js/10.d026e722.chunk.js.map,1640418010338,ac3b4adf7507ad9c37990e9d9f66eacefbc03276bdd86632e1d38c2a09f44396 15 | static/js/0.28b1e12d.chunk.js,1640418010330,ca9a7cf9516bd0a1a98bb7249287796705a0f0925e4c43ee9999bc3e3e30041e 16 | static/js/4.6f26904a.chunk.js.LICENSE.txt,1640418010338,a2481453fd1e1e9e14a565dd414239b668bb0dec7d77608f187dea0af6bd6a44 17 | static/js/5.9a1664d8.chunk.js,1640418010330,5b455f7124f1243872c7f96c5401f3a018cf2571f2a1793f4a306b184645bf4f 18 | static/js/6.54071d1a.chunk.js,1640418010330,94e5f2dbb65c1880b49b06a105f3d06b9ea3d39d63c565c21f661ceae3d88c83 19 | static/js/6.54071d1a.chunk.js.map,1640418010338,5e113bf12ad2bd4e88bc80d3d3bc2f41cdcd8c71e8e24484060cc09a0b3ef11c 20 | static/js/7.f0195616.chunk.js,1640418010330,beefc6e2168b9cffdf6290d921d9a4f99ed5c35093f48531de5cba88aa0d12f0 21 | static/js/5.9a1664d8.chunk.js.map,1640418010338,2a4eaccd12e7d1cabd943d0938890cf70ce5cc27ba2bbb99219b1d3237fb023b 22 | static/js/8.b29a53f0.chunk.js,1640418010330,f918407775d9c9597cd4226f008ed847b3c2a4aa4884a07140b155b9c0387a15 23 | static/js/9.7d61026c.chunk.js,1640418010331,59f61f83ff279d382dd30ceb517a0f6e3a3efce62c725acd1ce788b52fe9292e 24 | static/js/9.7d61026c.chunk.js.LICENSE.txt,1640418010338,5c92efd90d1294942abdce383f928741d25a69dbf4daa7a67d6fe6de9f75b24d 25 | static/js/8.b29a53f0.chunk.js.map,1640418010338,02a4a38717aade5ec735b974261a962828e8258895c3c94d69f3bf07820f27c5 26 | static/js/9.7d61026c.chunk.js.map,1640418010338,ad3f0096ea79a3db6b3a00b56e7093189633655b181d3b49e3878a24c402f2a0 27 | static/js/runtime-main.7268014c.js,1640418010330,f3051b758af9a8eaf8dd1dde5af47d31509c9dce86e8371224239e59df60ce9c 28 | static/js/main.cf5467d6.chunk.js,1640418010330,ae9ce28a399c18c436e92cc8cd0a1e971a783f4232684c49958274bf0fc96fdf 29 | static/js/main.cf5467d6.chunk.js.map,1640418010338,94d313f67af863375815d835347dd99af36ce10d715d83f9efd83f7ee55743e2 30 | static/js/runtime-main.7268014c.js.map,1640418010338,fcb6b2cfadc47413c5a248ca9184ef8ad9fee74edd37d32f1f96dd7a9be47582 31 | static/js/7.f0195616.chunk.js.map,1640418010338,8ae1cc8bb9af51b74d56f676392cd058128c73bb7641655803756207dc64b191 32 | images/avatars/raphael.jpg,1640418001169,20b9f8a56f37a004849e36f70f8ffe73282970cefc0419f244a4bf6a4f8ac4db 33 | images/avatars/orwell.jpg,1640418001169,0824a0915f842377e12d3a86160cd9d8fbe8807e20a347e3b80eed9e668a5c1e 34 | static/js/1.6c39a24e.chunk.js.map,1640418010338,b73a3bbfea6198a2a0c618003fd6e4be93669d8ff52b9b433430d211062cad90 35 | images/avatars/karl.jpg,1640418001168,a1385ee38832c73026963aa5fa6a7b8c30c5d3168ebbf6a21d2b075765dbfc03 36 | images/avatars/dali.jpg,1640418001167,fc15f174bd9214e62a879760204feb568df5b0f39d2f64c26883d89994a41caf 37 | images/iphone-with-profile.jpg,1640418001171,f5bd256b154671e01d68064e9172a5522670a5ee78e8954cc5c62a98a6dbf06b 38 | images/users/raphael/3.jpg,1640418001176,904ae2c51c8eb31752029a9193280da10c3643d4f873551ecff1dacbb93d7f4d 39 | images/avatars/steve.jpg,1640418001170,fc71e0df91bce0d004c76cb4d8646d198bf06f2ca5e1c758db6f1122ea9a291c 40 | images/users/raphael/4.jpg,1640418001178,ead6e5a29e9fada212d8277e2f5bcb646bbef554b7e29f27aa8b46ee8f527b7c 41 | images/users/raphael/2.jpg,1640418001175,8de5a8f9280b552c40e3e64eda618ae15e13da346f5f2932f27431b95cc01b1a 42 | images/users/raphael/5.jpg,1640418001179,b7c57d1aa9fa8e3af72dda5a45f6ec6aebad8879613660c0d0eb8a8d31931e5a 43 | static/js/4.6f26904a.chunk.js,1640418010330,949be76c0a68e244acdf2639dd5b919cf0ccd5896ddedf462d3a6f1c6d0bd95a 44 | images/users/raphael/1.jpg,1640418001173,d603cc0855e086e549012c1b6259c402054fa2d7d45cc475c8e50bd28f7583d9 45 | static/js/0.28b1e12d.chunk.js.map,1640418010338,5f6620a567d0629034d16d00944571b89630e4a5656cf3aa9e90d926f7bbddbd 46 | static/js/4.6f26904a.chunk.js.map,1640418010338,33fc92f042bca4fe7ed108dd38a0860722d4bb9a1cced1294eba69fe313041cf 47 | -------------------------------------------------------------------------------- /src/pages/signup.page.js: -------------------------------------------------------------------------------- 1 | import * as Route from "../constants/routes"; 2 | 3 | import Loader, { Spinner } from "../components/loader"; 4 | import { createUser, doesUsernameExist } from "../services/auth"; 5 | import { Suspense, useContext, useEffect, useState } from "react"; 6 | 7 | import { FirebaseContext } from "../context/firebase"; 8 | import { Link } from "react-router-dom"; 9 | import cx from "classnames"; 10 | import { useNavigate } from "react-router-dom"; 11 | 12 | function Signup() { 13 | const history = useNavigate(); 14 | const { getAuth } = useContext(FirebaseContext); 15 | 16 | const [email, setEmail] = useState(""); 17 | const [username, setUserName] = useState(""); 18 | const [fullname, setFullName] = useState(""); 19 | const [password, setPassword] = useState(""); 20 | const [error, setError] = useState(""); 21 | const [loading, setLoading] = useState(false); 22 | 23 | const handleSignup = async (e) => { 24 | e.preventDefault(); 25 | try { 26 | setLoading(true); 27 | const usernameExists = await doesUsernameExist(username); 28 | if (usernameExists) { 29 | setError("Username already taken, pleas/e try another one"); 30 | return; 31 | } else { 32 | setError(""); 33 | } 34 | 35 | await createUser(username, fullname, email, password); 36 | history(Route.DASHBOARD); 37 | } catch (e) { 38 | console.log(e); 39 | setError(e); 40 | } finally { 41 | setLoading(false); 42 | } 43 | }; 44 | 45 | useEffect(() => { 46 | document.title = "Signup - instagram"; 47 | }, []); 48 | 49 | return ( 50 | }> 51 |
52 |
53 | iPhone 58 |
59 |
60 |
61 |

62 | instagram 67 |

68 | {error && ( 69 |

{error}

70 | )} 71 | 72 |
73 | setUserName(e.target.value)} 80 | value={username || ""} 81 | /> 82 | setFullName(e.target.value)} 89 | value={fullname || ""} 90 | /> 91 | setEmail(e.target.value)} 98 | value={email || ""} 99 | /> 100 | setPassword(e.target.value)} 107 | autoComplete="off" 108 | value={password} 109 | /> 110 | 122 |
123 |
124 |
125 |

126 | Have an account?{" "} 127 | 131 | Sign in 132 | 133 |

134 |
135 |
136 |
137 |
138 | ); 139 | } 140 | 141 | export default Signup; 142 | 143 | //text-red-primary 144 | //text-gray-base 145 | // border-grey-primary 146 | // bg-blue-medium 147 | -------------------------------------------------------------------------------- /src/components/profile/profile-header.js: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import { Link } from "react-router-dom"; 3 | import { UserAvatar } from ".."; 4 | import { RouteHelper } from "../../helper/routes.helper"; 5 | import ProfileService from "../../services/profile"; 6 | 7 | function ProfileHeader({ 8 | profile, 9 | loggedInUser, 10 | photoCount, 11 | followerCount, 12 | setFollowCount, 13 | }) { 14 | const [isFollowingProfile, setIsFollowingProfile] = useState( 15 | profile.followers?.includes(loggedInUser.userId) 16 | ); 17 | const [isMyProfile, setIsMyProfile] = useState( 18 | profile.username === loggedInUser.username 19 | ); 20 | 21 | const handleFollowUser = async () => { 22 | setIsFollowingProfile(!isFollowingProfile); 23 | setFollowCount({ 24 | followerCount: isFollowingProfile ? followerCount - 1 : followerCount + 1, 25 | }); 26 | await ProfileService.updateMyFollowingUser( 27 | loggedInUser.userId, 28 | profile.userId, 29 | isFollowingProfile 30 | ); 31 | }; 32 | return ( 33 |
34 |
35 |
36 | 42 |
43 | 49 |
50 |
51 | 52 |
53 |
54 | 60 |
61 | 66 | 67 | 68 |
69 |
70 | 71 | 76 |
77 | ); 78 | } 79 | const FollowNEditProfileButton = ({ 80 | profile, 81 | isMyProfile, 82 | handleFollowUser, 83 | isFollowingProfile, 84 | }) => { 85 | const button = !isMyProfile ? ( 86 | 98 | ) : ( 99 | 103 | Edit Profile 104 | 105 | ); 106 | 107 | return ( 108 |
109 |
110 | 111 | {profile.username} 112 | 113 |
114 | {button} 115 |
116 | ); 117 | }; 118 | const BioNWebsite = ({ profile }) => { 119 | return ( 120 |
121 |

{profile.fullname}

122 |

{profile.bio}

123 | {profile.website && ( 124 |

125 | {profile.website} 126 |

127 | )} 128 |
129 | ); 130 | }; 131 | const ProfileStateSM = ({ profile, photoCount, followerCount }) => { 132 | return ( 133 |
134 |

135 | {photoCount ?? 0}{" "} 136 | {photoCount === 1 ? " photo" : " photos"} 137 |

138 |

139 | {followerCount}  140 | 141 | {followerCount === 1 ? " follower" : " followers"} 142 | 143 |

144 |

145 | {profile.following?.length ?? 0} 146 | 147 | {profile.following?.length === 1 ? " following" : " following"} 148 | 149 |

150 |
151 | ); 152 | }; 153 | const ProfileStates = ({ user, photoCount, followerCount }) => { 154 | return ( 155 |
156 | 184 |
185 | ); 186 | }; 187 | 188 | export default ProfileHeader; 189 | -------------------------------------------------------------------------------- /src/pages/settings/edit-profile.page.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState, useRef, useMemo } from "react"; 2 | import cx from "classnames"; 3 | import { withSession } from "../../context/session"; 4 | import { UserAvatar } from "../../components"; 5 | import { Spinner } from "../../components/loader"; 6 | import ProfileService from "../../services/profile"; 7 | 8 | function EditProfilePageComponent({ user }) { 9 | const email = user.email; 10 | const [name, setName] = useState(user.fullname); 11 | const [avatar, setAvatar] = useState(user.avatar); 12 | const [website, setWebsite] = useState(user.website ?? ""); 13 | const [bio, setBio] = useState(user.bio ?? ""); 14 | const [error, setError] = useState(""); 15 | const [loading, setLoading] = useState(false); 16 | const [success, setSuccess] = useState(false); 17 | 18 | const nameRef = useRef(); 19 | const websiteRef = useRef(); 20 | const bioRef = useRef(); 21 | 22 | // Save profile 23 | const handleSave = async (e) => { 24 | e.preventDefault(); 25 | try { 26 | setLoading(true); 27 | 28 | await ProfileService.updateProfile( 29 | user.userId, 30 | name, 31 | bio, 32 | website, 33 | avatar 34 | ); 35 | setSuccess(true); 36 | console.log("profile updated"); 37 | } catch (e) { 38 | setError(e); 39 | console.log(e); 40 | } finally { 41 | setLoading(false); 42 | } 43 | }; 44 | 45 | return ( 46 |
47 |
48 |
49 | 54 |
55 |
56 |

{user.username}

57 |

58 | Change profile photo 59 |

60 |
61 |
62 | 63 |
64 |
65 | 68 |
69 | { 79 | setName(e.target.value); 80 | }} 81 | onFocus={(e) => {}} 82 | /> 83 |
84 |
85 | 90 | 95 |
96 | 99 |
100 |