├── public ├── 404.webp ├── favicon.ico ├── robots.txt └── vercel.svg ├── postcss.config.js ├── renovate.json ├── src ├── utils │ ├── urlNormalize.ts │ ├── usernameGenerator.ts │ ├── copyLink.ts │ ├── share.ts │ ├── handleTabChanges.ts │ └── postDate.ts ├── types │ ├── DataMessage.d.ts │ ├── Chats.d.ts │ ├── next-auth.d.ts │ ├── user.d.ts │ ├── post.d.ts │ └── State.d.ts ├── pages │ ├── _document.tsx │ ├── api │ │ ├── search-user.ts │ │ └── auth │ │ │ └── [...nextauth].ts │ ├── 404.tsx │ ├── 500.tsx │ ├── create.tsx │ ├── _app.tsx │ ├── post │ │ └── [id] │ │ │ └── edit.tsx │ ├── auth │ │ └── signin.tsx │ └── index.tsx ├── components │ ├── Notifications │ │ ├── Empty.tsx │ │ └── NotificationUser.tsx │ ├── Logo │ │ └── Logo.tsx │ ├── Comments │ │ ├── Empty.tsx │ │ ├── Comment.tsx │ │ └── Forms.tsx │ ├── Post │ │ ├── Image.tsx │ │ ├── Likes.tsx │ │ ├── Form.tsx │ │ ├── PostInfo.tsx │ │ ├── CreatedTime.tsx │ │ ├── Author.tsx │ │ ├── index.tsx │ │ ├── PreviewLargeScreen.tsx │ │ └── ActionButton.tsx │ ├── User │ │ ├── Statistic │ │ │ ├── Desktop.tsx │ │ │ ├── Mobile.tsx │ │ │ └── Statistic.tsx │ │ ├── Info │ │ │ └── Info.tsx │ │ └── Tab │ │ │ └── Tab.tsx │ ├── Header │ │ ├── PostHeader.tsx │ │ ├── NavHeader.tsx │ │ └── MainHeader.tsx │ ├── Footer │ │ └── index.tsx │ ├── Messages │ │ ├── EmptyMessages │ │ │ └── EmptyMessages.tsx │ │ ├── Chats │ │ │ ├── Header │ │ │ │ └── Header.tsx │ │ │ └── Chats.tsx │ │ ├── Users │ │ │ ├── Header │ │ │ │ └── UserHeader.tsx │ │ │ └── UsersChat.tsx │ │ └── Form │ │ │ └── ChatForm.tsx │ ├── Navigation │ │ ├── ExtraMenuBtn.tsx │ │ ├── NavLink.tsx │ │ ├── NavItem.tsx │ │ ├── Sidebar.tsx │ │ └── ExtraMenus.tsx │ ├── Suggestions │ │ ├── User.tsx │ │ ├── SuggestionMobile.tsx │ │ └── Suggestions.tsx │ ├── Captions │ │ ├── TextArea.tsx │ │ └── Captions.tsx │ ├── ImageCropper │ │ └── ImageCropper.tsx │ ├── Modal │ │ ├── Drawer │ │ │ ├── Search │ │ │ │ ├── index.tsx │ │ │ │ ├── Form.tsx │ │ │ │ └── Results.tsx │ │ │ ├── Notifications │ │ │ │ └── NotificationsDrawer.tsx │ │ │ └── Comments.tsx │ │ ├── Menu │ │ │ └── Lists.tsx │ │ ├── Feed.tsx │ │ ├── Users │ │ │ └── Recommendations.tsx │ │ ├── Notifications │ │ │ └── Notifications.tsx │ │ ├── Cropper │ │ │ └── Cropper.tsx │ │ └── Report │ │ │ └── Report.tsx │ ├── FileUpload │ │ └── FileUpload.tsx │ ├── Loader │ │ └── Post.tsx │ └── Layout │ │ └── Layout.tsx ├── hooks │ ├── useClickoutside.ts │ ├── useTheme.tsx │ ├── useUser.tsx │ ├── useBlurhash.tsx │ ├── usePost.tsx │ └── useWindowResize.tsx ├── data │ └── footerLists.json ├── styles │ └── globals.css ├── config │ └── firebase.ts ├── helper │ ├── updatePost.ts │ ├── like.ts │ ├── savePost.ts │ ├── startNewMessage.ts │ ├── deletePost.ts │ ├── postActions.ts │ ├── getUser.ts │ ├── reportPost.ts │ ├── comments.ts │ ├── getMessage.ts │ ├── makePost.ts │ ├── follow.ts │ ├── imageUpload.ts │ └── getPosts.ts ├── stores │ ├── Global │ │ └── StateContext.tsx │ ├── reducerFunctions │ │ ├── reducer.ts │ │ ├── drawer.ts │ │ ├── users.ts │ │ └── Modal.ts │ ├── Drawer │ │ └── DrawerStates.tsx │ └── Modal │ │ └── ModalStatesContext.tsx └── middleware.ts ├── prettier.config.cjs ├── .eslintrc.json ├── .gitignore ├── tsconfig.json ├── .github └── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md ├── README.md ├── package.json ├── CONTRIBUTING.md ├── next.config.js └── tailwind.config.js /public/404.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wirayuda299/Instafam/HEAD/public/404.webp -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wirayuda299/Instafam/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:base" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /src/utils/urlNormalize.ts: -------------------------------------------------------------------------------- 1 | import { normalize } from "path"; 2 | export const urlNormalize = (url: string) => { 3 | return normalize(url); 4 | }; 5 | -------------------------------------------------------------------------------- /src/types/DataMessage.d.ts: -------------------------------------------------------------------------------- 1 | type DataMessage = { 2 | message: any; 3 | id: string; 4 | image: string; 5 | name: string; 6 | docId: string; 7 | }; 8 | -------------------------------------------------------------------------------- /prettier.config.cjs: -------------------------------------------------------------------------------- 1 | /** @type {import("prettier").Config} */ 2 | const config = { 3 | plugins: [require.resolve("prettier-plugin-tailwindcss")], 4 | }; 5 | 6 | module.exports = config; 7 | -------------------------------------------------------------------------------- /src/types/Chats.d.ts: -------------------------------------------------------------------------------- 1 | export type Chats = { 2 | createdAt: number; 3 | message: string; 4 | sender: { 5 | id: string; 6 | image: string; 7 | name: string; 8 | }; 9 | }; 10 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals", 3 | "rules": { 4 | "no-console": "off", 5 | "dependency-cruiser/no-orphans": "off", 6 | "dependency-cruiser/no-deprecated-core": "off", 7 | "react-hooks/exhaustive-deps": "off" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/pages/_document.tsx: -------------------------------------------------------------------------------- 1 | import { Html, Head, Main, NextScript } from "next/document"; 2 | 3 | export default function Document() { 4 | return ( 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /src/utils/usernameGenerator.ts: -------------------------------------------------------------------------------- 1 | export function getUsernameFromEmail(email: string) { 2 | try { 3 | const atIndex = email.indexOf("@"); 4 | if (atIndex !== -1) { 5 | return email.slice(0, atIndex); 6 | } else { 7 | return email; 8 | } 9 | } catch (error: any) { 10 | console.log(error.message); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/types/next-auth.d.ts: -------------------------------------------------------------------------------- 1 | import NextAuth, { DefaultSession, DefaultUser } from "next-auth"; 2 | 3 | declare module "next-auth" { 4 | /** 5 | * Returned by `useSession`, `getSession` and received as a prop on the `SessionProvider` React Context 6 | */ 7 | interface Session { 8 | user: { 9 | username: string; 10 | uid: string; 11 | } & DefaultSession["user"] & 12 | DefaultUser; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/components/Notifications/Empty.tsx: -------------------------------------------------------------------------------- 1 | import { RiNotificationOffLine } from "react-icons/ri"; 2 | 3 | const Empty = () => { 4 | return ( 5 |
6 | 7 |

8 | No notifications 9 |

10 |
11 | ); 12 | }; 13 | export default Empty; 14 | -------------------------------------------------------------------------------- /src/types/user.d.ts: -------------------------------------------------------------------------------- 1 | type Following = { 2 | userId: string; 3 | }; 4 | 5 | type Followers = { 6 | followedBy: string; 7 | followedByName: string; 8 | followedImage: string; 9 | }; 10 | 11 | type Base = { 12 | image: string; 13 | createdAt: string; 14 | email: string; 15 | uid: string; 16 | username: string; 17 | name: string; 18 | }; 19 | interface IUser extends Base { 20 | following: Following[]; 21 | followers: Followers[]; 22 | savedPosts: IUserPostProps[]; 23 | } 24 | -------------------------------------------------------------------------------- /src/utils/copyLink.ts: -------------------------------------------------------------------------------- 1 | import { toast } from "react-hot-toast"; 2 | 3 | type CopyLink = (url: string) => void; 4 | 5 | export const copyLink: CopyLink = (url) => { 6 | try { 7 | navigator.clipboard 8 | .writeText(url) 9 | .then(() => { 10 | toast.success("Link copied to clipboard"); 11 | }) 12 | .catch((err) => { 13 | console.error(`Error copying ${url} to clipboard: ${err}`); 14 | }); 15 | } catch (error: any) { 16 | console.log(error.message); 17 | } 18 | }; 19 | -------------------------------------------------------------------------------- /src/components/Logo/Logo.tsx: -------------------------------------------------------------------------------- 1 | import { AiOutlineInstagram } from "react-icons/ai"; 2 | 3 | const Logo = () => { 4 | return ( 5 | <> 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | ); 15 | }; 16 | export default Logo; 17 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: */search/:*, */search/?, /search/?, /auth/login, /auth/logout, /auth/register, /auth/forgot-password, /auth/reset-password, /auth/confirm-email, /auth/connect, /auth/unlink, /auth/confirm, /auth/confirm-email, /auth/confirm-email/:*, /_next/*, /_next/static/*, /_next/static/chunks/*, /_next/static/chunks/pages/*, /_next/static/chunks/pages/_app.js, /_next/static/chunks/pages/_error.js, /_next/static/chunks/pages/_document.js, /_next/static/chunks/pages/_app.js, /_next/static/chunks/pages/_error.js, /_next/static/chunks/pages/_document.js -------------------------------------------------------------------------------- /src/components/Comments/Empty.tsx: -------------------------------------------------------------------------------- 1 | import { AiOutlineComment } from "react-icons/ai"; 2 | 3 | export default function Empty() { 4 | return ( 5 |
6 |
7 | 8 |

There's no comment yet

9 |

10 | Be the first person to comment on this post 11 |

12 |
13 |
14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /src/hooks/useClickoutside.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, RefObject } from "react"; 2 | 3 | export default function useClickOutSide(ref:RefObject, callback:() => void) { 4 | useEffect(() => { 5 | function handleClickOutside(event: MouseEvent) { 6 | // @ts-ignore 7 | if (ref.current && !ref.current.contains(event.target)) { 8 | callback() 9 | } 10 | } 11 | document.addEventListener("mousedown", handleClickOutside); 12 | return () => { 13 | document.removeEventListener("mousedown", handleClickOutside); 14 | }; 15 | }, [ref]); 16 | } 17 | 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 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | .pnpm-debug.log* 27 | 28 | # local env files 29 | .env*.local 30 | .env 31 | 32 | # vercel 33 | .vercel 34 | 35 | # typescript 36 | *.tsbuildinfo 37 | next-env.d.ts 38 | 39 | # Sentry Auth Token 40 | .sentryclirc 41 | -------------------------------------------------------------------------------- /src/hooks/useTheme.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from "react"; 2 | 3 | export default function useTheme() { 4 | const [theme, setTheme] = useState(""); 5 | 6 | useEffect(() => { 7 | const prefersDarkMode = window.matchMedia( 8 | "(prefers-color-scheme: dark)" 9 | ).matches; 10 | 11 | const selectedTheme = 12 | theme || localStorage.theme || (prefersDarkMode ? "dark" : "light"); 13 | 14 | document.documentElement.classList.toggle("dark", selectedTheme === "dark"); 15 | localStorage.theme = selectedTheme; 16 | }, [theme]); 17 | return {theme, setTheme} 18 | }; -------------------------------------------------------------------------------- /src/data/footerLists.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "About", 4 | "link": "/about" 5 | }, 6 | { 7 | "name": "Help", 8 | "link": "/help" 9 | }, 10 | { 11 | "name": "Api", 12 | "link": "/api" 13 | }, 14 | { 15 | "name": "Privacy", 16 | "link": "/privacy" 17 | }, 18 | { 19 | "name": "Terms", 20 | "link": "/terms" 21 | }, 22 | { 23 | "name": "Locations", 24 | "link": "/locations" 25 | }, 26 | { 27 | "name": "Language", 28 | "link": "/language" 29 | }, 30 | { 31 | "name": "Instafam Verified", 32 | "link": "/verfied-instafam" 33 | } 34 | ] 35 | -------------------------------------------------------------------------------- /src/types/post.d.ts: -------------------------------------------------------------------------------- 1 | type PostComments = { 2 | commentByUid: string; 3 | comment: string; 4 | commentByName: string; 5 | commentByPhoto: string; 6 | createdAt: string | number; 7 | }; 8 | interface IUserPostProps { 9 | likes: number; 10 | captions: string; 11 | image: string; 12 | postedById: string; 13 | author: string; 14 | postedByPhotoUrl: string; 15 | storageRef: string; 16 | likedBy: string[]; 17 | createdAt: string | number; 18 | comments: PostComments[]; 19 | hashtags: string[]; 20 | postId: string; 21 | tagged: []; 22 | savedBy: string[]; 23 | blurDataUrl: string; 24 | } 25 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/styles/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @supports (font: -apple-system-body) and (-webkit-appearance: none) { 6 | body { 7 | font-size: 16px !important; 8 | } 9 | } 10 | body { 11 | overflow: hidden !important; 12 | font-size: 16px !important; 13 | } 14 | .wrapper::after { 15 | display: block; 16 | content: ""; 17 | padding-bottom: 100%; 18 | } 19 | #nprogress .bar { 20 | background: linear-gradient(90deg, #db2777 0%, #ea580c 100%) !important; 21 | height: 4px !important; 22 | border-radius: 50% !important; 23 | } 24 | .dark{ 25 | @apply bg-black text-white 26 | } 27 | -------------------------------------------------------------------------------- /src/config/firebase.ts: -------------------------------------------------------------------------------- 1 | import { initializeApp } from "firebase/app"; 2 | import { getFirestore } from "firebase/firestore"; 3 | import { getStorage } from "firebase/storage"; 4 | 5 | const firebaseConfig = { 6 | apiKey: process.env.FIREBASE_API_KEY, 7 | authDomain: process.env.FIREBASE_AUTH_DOMAIN, 8 | projectId: process.env.FIREBASE_PROJECT_ID, 9 | storageBucket: process.env.FIREBASE_STORAGE_BUCKET, 10 | messagingSenderId: process.env.FIREBASE_MESSAGING_SENDER_ID, 11 | appId: process.env.FIREBASE_APP_ID, 12 | }; 13 | 14 | const app = initializeApp(firebaseConfig); 15 | export const db = getFirestore(app); 16 | export const storage = getStorage(app); 17 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true, 17 | "paths": { 18 | "@/*": ["./src/*"] 19 | } 20 | }, 21 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], 22 | "exclude": ["node_modules"] 23 | } 24 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /src/utils/share.ts: -------------------------------------------------------------------------------- 1 | import toast from "react-hot-toast"; 2 | 3 | type Share = (post: IUserPostProps, url: string) => void; 4 | 5 | export const share: Share = (post, url) => { 6 | try { 7 | if (navigator.share) { 8 | navigator 9 | .share({ 10 | title: post.captions, 11 | text: post.captions, 12 | url: url, 13 | }) 14 | .then(() => { 15 | console.log("Successfully shared"); 16 | }) 17 | .catch((err) => { 18 | console.error("Error sharing:", err); 19 | }); 20 | } else { 21 | toast.error("Share not supported"); 22 | } 23 | } catch (error: any) { 24 | toast.error(error.message); 25 | } 26 | }; 27 | -------------------------------------------------------------------------------- /src/helper/updatePost.ts: -------------------------------------------------------------------------------- 1 | export async function updatePost(updated: string, post: IUserPostProps) { 2 | const { toast } = await import("react-hot-toast"); 3 | try { 4 | const { db } = await import("@/config/firebase"); 5 | const { doc, updateDoc } = await import("firebase/firestore"); 6 | const q = doc(db, "posts", `post-${post.postId}`); 7 | await updateDoc(q, { 8 | captions: updated.match(/^[^#]*/), 9 | hashtags: 10 | updated 11 | .match(/#(?!\n)(.+)/g) 12 | ?.join(" ") 13 | .split(" ") || [], 14 | }); 15 | toast.success("Post updated!"); 16 | } catch (error) { 17 | if (error instanceof Error) { 18 | toast.error(error.message) as Error["message"]; 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/components/Post/Image.tsx: -------------------------------------------------------------------------------- 1 | import Image from "next/image"; 2 | import useBlurhash from "@/hooks/useBlurhash"; 3 | import type { FC, HTMLAttributes } from "react"; 4 | 5 | type Props = { 6 | post: IUserPostProps; 7 | } & HTMLAttributes; 8 | 9 | const PostImage: FC = ({ post }) => { 10 | const { blurHash } = useBlurhash(post); 11 | return ( 12 | {post?.author 23 | ); 24 | }; 25 | export default PostImage; 26 | -------------------------------------------------------------------------------- /src/hooks/useUser.tsx: -------------------------------------------------------------------------------- 1 | import { db } from "@/config/firebase"; 2 | import { useStateContext } from "@/stores/Global/StateContext"; 3 | import { doc, onSnapshot } from "firebase/firestore"; 4 | import { useEffect, useState } from "react"; 5 | 6 | export default function useUser(uid: string) { 7 | const [user, setUser] = useState(null); 8 | const { 9 | state: { selectedPost }, 10 | } = useStateContext(); 11 | 12 | useEffect(() => { 13 | const unsub = onSnapshot( 14 | doc(db, "users", `${uid ? uid : selectedPost?.postedById}`), 15 | (docs) => { 16 | if (docs.exists()) { 17 | setUser(docs.data() as IUser); 18 | } 19 | } 20 | ); 21 | return () => unsub(); 22 | }, [db, selectedPost, uid]); 23 | 24 | return { user }; 25 | } 26 | -------------------------------------------------------------------------------- /src/types/State.d.ts: -------------------------------------------------------------------------------- 1 | type GlobalStates = { 2 | selectedPost: IUserPostProps | null; 3 | result: IUser[]; 4 | blurhash: string; 5 | previewUrl: string; 6 | croppedImage: string; 7 | chatRoomSelected: IUser | null; 8 | selectedChat: DataMessage | null; 9 | }; 10 | 11 | type ModalStates = { 12 | menuModal: boolean; 13 | postPreviewModal: boolean; 14 | feedModal: boolean; 15 | postModal: boolean; 16 | postCommentModal: boolean; 17 | notificationModal: boolean; 18 | postCreateModal: boolean; 19 | postReportModal: boolean; 20 | messageModal: boolean; 21 | showAllUserModal: boolean; 22 | showReportModal: boolean; 23 | }; 24 | 25 | type DrawerStatesTypes = { 26 | isSearchDrawerOpen: boolean; 27 | resultDrawer: boolean; 28 | notificationDrawer: boolean; 29 | receiverDrawer: boolean; 30 | }; 31 | -------------------------------------------------------------------------------- /src/components/User/Statistic/Desktop.tsx: -------------------------------------------------------------------------------- 1 | import { type FC, memo } from "react"; 2 | import { StatisticProps } from "./Mobile"; 3 | 4 | const DesktopStatistic: FC = ({ data }) => { 5 | return ( 6 |
    10 | {data.map((item) => ( 11 |
  • 16 |
    17 | {item.value} 18 | {item.title} 19 |
    20 |
  • 21 | ))} 22 |
23 | ); 24 | }; 25 | export default memo(DesktopStatistic); 26 | -------------------------------------------------------------------------------- /src/helper/like.ts: -------------------------------------------------------------------------------- 1 | type LikesProps = { 2 | post: IUserPostProps; 3 | uid: string; 4 | likes: string[]; 5 | }; 6 | 7 | export const handleLikes = async (params: T) => { 8 | if (typeof window === "undefined") return; 9 | try { 10 | const { post, uid, likes } = params; 11 | if (!uid) return; 12 | 13 | const { doc, updateDoc, arrayRemove, arrayUnion } = await import( 14 | "firebase/firestore" 15 | ); 16 | const { db } = await import("@/config/firebase"); 17 | const postRef = doc(db, "posts", `post-${post.postId}`); 18 | 19 | if (likes.includes(uid)) { 20 | await updateDoc(postRef, { likedBy: arrayRemove(uid) }); 21 | } else { 22 | await updateDoc(postRef, { likedBy: arrayUnion(uid) }); 23 | } 24 | } catch (error: any) { 25 | console.error(error.message); 26 | } 27 | }; 28 | -------------------------------------------------------------------------------- /src/hooks/useBlurhash.tsx: -------------------------------------------------------------------------------- 1 | import { decode } from "blurhash"; 2 | import { useEffect, useState } from "react"; 3 | 4 | export default function useBlurhash(post: IUserPostProps) { 5 | const [blurHash, setBlurHash] = useState(undefined); 6 | 7 | useEffect(() => { 8 | if (post?.blurDataUrl) { 9 | const canvas = document.createElement("canvas"); 10 | const blurHash = post.blurDataUrl; 11 | const pixels = decode(blurHash, 32, 32, 1); 12 | const imageData = new ImageData(pixels, 32, 32); 13 | canvas.width = 32; 14 | canvas.height = 32; 15 | const ctx = canvas.getContext("2d"); 16 | if (!ctx) return; 17 | ctx.putImageData(imageData, 0, 0); 18 | const dataUrl = canvas.toDataURL(); 19 | setBlurHash(dataUrl); 20 | } 21 | }, [post?.blurDataUrl]); 22 | 23 | return { blurHash, setBlurHash }; 24 | } 25 | -------------------------------------------------------------------------------- /src/components/Header/PostHeader.tsx: -------------------------------------------------------------------------------- 1 | import Image from "next/image"; 2 | 3 | import CreatedTime from "@/components/Post/CreatedTime"; 4 | import { type FC, memo } from "react"; 5 | 6 | type PostHeaderProps = { 7 | post: IUserPostProps; 8 | children?: React.ReactNode; 9 | }; 10 | 11 | const PostHeader: FC = ({ post, children }) => { 12 | 13 | return ( 14 |
15 | {post?.author 23 | 24 | {children} 25 |
26 | ); 27 | }; 28 | export default memo(PostHeader); 29 | -------------------------------------------------------------------------------- /src/components/User/Statistic/Mobile.tsx: -------------------------------------------------------------------------------- 1 | import { FC, memo } from "react"; 2 | 3 | export type StatisticProps = { 4 | data: { 5 | id: number; 6 | title: string; 7 | value: number | undefined; 8 | }[]; 9 | }; 10 | const StatisticMobile: FC = ({ data }) => { 11 | return ( 12 |
    16 | {data.map((item) => ( 17 |
  • 22 |
    23 | {item.value} 24 | {item.title} 25 |
    26 |
  • 27 | ))} 28 |
29 | ); 30 | }; 31 | export default memo(StatisticMobile); 32 | -------------------------------------------------------------------------------- /src/components/Header/NavHeader.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | import { Playfair_Display } from "next/font/google"; 3 | 4 | import { useDrawerContext } from "@/stores/Drawer/DrawerStates"; 5 | 6 | const playfair = Playfair_Display({ 7 | fallback: ["sans-serif"], 8 | subsets: ["latin"], 9 | preload: true, 10 | weight: "700", 11 | display: "swap", 12 | }); 13 | 14 | const NavHeader = () => { 15 | const { 16 | drawerStates: { notificationDrawer, isSearchDrawerOpen }, 17 | } = useDrawerContext(); 18 | 19 | return ( 20 |
23 | 27 | {!isSearchDrawerOpen || !notificationDrawer && ( 28 |

Instafams

29 | 30 | )} 31 | 32 |
33 | ); 34 | }; 35 | export default NavHeader; 36 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /src/helper/savePost.ts: -------------------------------------------------------------------------------- 1 | type SavedPostProps = { 2 | post: IUserPostProps; 3 | uid: string; 4 | }; 5 | 6 | export async function savePost(params: SavedPostProps) { 7 | const { post, uid } = params; 8 | try { 9 | if (typeof window === "undefined") return; 10 | const { doc, updateDoc, arrayRemove, arrayUnion, getDoc } = await import( 11 | "firebase/firestore" 12 | ); 13 | const { db } = await import("@/config/firebase"); 14 | const q = doc(db, "posts", `post-${post.postId}`); 15 | const res = await getDoc(q); 16 | if (res.exists()) { 17 | const savedBy = res.data()?.savedBy; 18 | const hasSavedByUsers = savedBy?.find((save: string) => save === uid); 19 | if (hasSavedByUsers) { 20 | await updateDoc(q, { savedBy: arrayRemove(uid) }); 21 | } else { 22 | await updateDoc(q, { savedBy: arrayUnion(uid) }); 23 | } 24 | } 25 | } catch (error) { 26 | if (error instanceof Error) { 27 | return error.message as Error["message"]; 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/components/Post/Likes.tsx: -------------------------------------------------------------------------------- 1 | import { type FC, memo } from "react"; 2 | 3 | type LikesProps = { 4 | likesCount: string[]; 5 | uid: string; 6 | }; 7 | const Likes: FC = ({ likesCount, uid }) => { 8 | return ( 9 | <> 10 | {likesCount && likesCount.length > 0 ? ( 11 |
12 | {likesCount.includes(uid) ? ( 13 |

14 | {likesCount.length > 1 ? "You " : "liked by You "} 15 | 18 | and {likesCount.length - 1} others 19 | 20 |

21 | ) : ( 22 | 23 | {likesCount.length} {likesCount.length > 1 ? "likes" : "like"} 24 | 25 | )} 26 |
27 | ) : null} 28 | 29 | ); 30 | }; 31 | export default memo(Likes); 32 | -------------------------------------------------------------------------------- /src/components/Footer/index.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | import type { FC, ReactNode } from "react"; 3 | 4 | import footerlists from "@/data/footerLists.json"; 5 | 6 | type FooterProps = { 7 | children: ReactNode; 8 | classNames: string; 9 | }; 10 | 11 | const Footer: FC> = ({ children, classNames }) => { 12 | return ( 13 |
14 |
    15 | {footerlists.map((list) => ( 16 |
  • 21 | 27 | {list.name} 28 | 29 |
  • 30 | ))} 31 |
32 | {children} 33 |
34 | ); 35 | }; 36 | 37 | export default Footer; 38 | -------------------------------------------------------------------------------- /src/utils/handleTabChanges.ts: -------------------------------------------------------------------------------- 1 | import { ActionsTypeUsersPage } from "@/types/ActionsTypes"; 2 | import type { SetStateAction, TransitionStartFunction } from "react"; 3 | 4 | type ParamsTypes = { 5 | startTransition: TransitionStartFunction; 6 | setActiveTab: (value: SetStateAction) => void; 7 | dispatch: (value: ActionsTypeUsersPage) => void; 8 | tabId: number; 9 | }; 10 | 11 | type HandleTabChanges = (params: ParamsTypes) => void; 12 | 13 | export const handleTabClick: HandleTabChanges = ({ 14 | dispatch, 15 | setActiveTab, 16 | startTransition, 17 | tabId, 18 | }) => { 19 | startTransition(() => { 20 | setActiveTab(tabId); 21 | }); 22 | switch (tabId) { 23 | case 1: 24 | dispatch({ 25 | type: "SET_POST_TAB", 26 | }); 27 | break; 28 | case 2: 29 | dispatch({ 30 | type: "SET_SAVED_POST_TAB", 31 | }); 32 | break; 33 | case 3: 34 | dispatch({ 35 | type: "SET_Tagged_POST_TAB", 36 | }); 37 | break; 38 | default: 39 | break; 40 | } 41 | }; 42 | -------------------------------------------------------------------------------- /src/components/Messages/EmptyMessages/EmptyMessages.tsx: -------------------------------------------------------------------------------- 1 | import { useModalContext } from "@/stores/Modal/ModalStatesContext"; 2 | import { RiMessengerLine } from "react-icons/ri"; 3 | 4 | const EmptyMessages = () => { 5 | const { modalDispatch } = useModalContext(); 6 | return ( 7 |
8 | 11 |

Your Message

12 |

13 | Send private photos and messages to a friend or group 14 |

15 | 28 |
29 | ); 30 | }; 31 | 32 | export default EmptyMessages; 33 | -------------------------------------------------------------------------------- /src/pages/api/search-user.ts: -------------------------------------------------------------------------------- 1 | import { db } from "@/config/firebase"; 2 | import { query, collection, getDocs } from "firebase/firestore"; 3 | import type { NextApiRequest, NextApiResponse } from "next"; 4 | import { getServerSession } from "next-auth"; 5 | import { authOptions } from "./auth/[...nextauth]"; 6 | 7 | export default async function handler( 8 | req: NextApiRequest, 9 | res: NextApiResponse 10 | ) { 11 | const { search } = req.query; 12 | const session = await getServerSession(req, res, authOptions); 13 | const isString = typeof search === "string"; 14 | const isGetmethod = req.method === "GET"; 15 | 16 | if (!session || !isString || !isGetmethod) { 17 | return res.status(401).end("Unauthorized"); 18 | } 19 | 20 | if (req.cookies && session) { 21 | const q = query(collection(db, "users")); 22 | const response = await getDocs(q); 23 | const result = response.docs.map((doc) => doc.data()); 24 | const regex = new RegExp(`${search}`, "gi"); 25 | const filtered = result.filter( 26 | (user) => regex.test(user.name) || regex.test(user.username) 27 | ); 28 | return res.status(200).json(filtered ?? []); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/helper/startNewMessage.ts: -------------------------------------------------------------------------------- 1 | import { db } from "@/config/firebase"; 2 | import type { Session } from "next-auth"; 3 | import { toast } from "react-hot-toast"; 4 | 5 | export async function startNewMessage( 6 | session: Session | null, 7 | chatRoomSelected: IUser | null 8 | ) { 9 | try { 10 | if (!session) return; 11 | 12 | const { addDoc, collection } = await import("firebase/firestore"); 13 | if (chatRoomSelected?.uid === session?.user.uid) { 14 | throw new Error("Can not start message with your self"); 15 | } 16 | if (chatRoomSelected?.uid === undefined) return; 17 | await addDoc(collection(db, "messages"), { 18 | room: { 19 | id: [session?.user.uid, chatRoomSelected?.uid], 20 | receiver: { 21 | id: chatRoomSelected?.uid, 22 | image: chatRoomSelected?.image, 23 | name: chatRoomSelected?.username, 24 | }, 25 | sender: { 26 | id: session?.user.uid, 27 | image: session?.user.image, 28 | name: session?.user.username, 29 | }, 30 | chats: [], 31 | }, 32 | }); 33 | } catch (error: any) { 34 | toast.error(error.message); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Instafam - Connect with people around the world 2 | 3 | Welcome to Instafam!, This is my first nextjs project. Please if you have suggesstion for this project don't hesitate to let me know> And about this project,This is a social media app that allows you to connect with people around the world. You can post pictures, like and comment on other people's posts, and follow other users. You can also search for users by their username. 4 | 5 | ## Features 6 | 7 | - Login/Log out 8 | - Like and comment on posts 9 | - Follow and unFollow other users 10 | - Search for users by username or name 11 | - View your profile 12 | - Crop photo before upload 13 | - View other users' profiles 14 | - Save posts to your profile 15 | - View your saved posts 16 | - Create, read, update delete and report a post 17 | - Simple detection of nudity in images while create a post 18 | - Navigate to spesific post, 19 | - Copy post link 20 | - Share post 21 | - Responsive design 22 | - Dark mode switch 23 | - Send messages to other users 24 | 25 | Other features is comming soon. 26 | 27 | ## Technologies Used 28 | 29 | - React 30 | - Nextjs 13 (pages directory) 31 | - Tailwind CSS 32 | - Firebase 33 | - NextAuth 34 | 35 | -------------------------------------------------------------------------------- /src/stores/Global/StateContext.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | type Dispatch, 3 | createContext, 4 | useContext, 5 | useMemo, 6 | useReducer, 7 | ReactNode, 8 | } from "react"; 9 | 10 | import { reducer } from "../reducerFunctions/reducer"; 11 | import { ActionsType } from "@/types/ActionsTypes"; 12 | 13 | const initialState: GlobalStates = { 14 | selectedPost: null, 15 | result: [], 16 | blurhash: "", 17 | previewUrl: "", 18 | croppedImage: "", 19 | chatRoomSelected: null, 20 | selectedChat: null, 21 | }; 22 | 23 | type StateProviderProps = { 24 | Dispatch: Dispatch; 25 | state: GlobalStates; 26 | }; 27 | 28 | const StateContext = createContext({ 29 | Dispatch: () => {}, 30 | state: initialState, 31 | }); 32 | 33 | export function StateProvider({ children }: { children: ReactNode }) { 34 | const [state, Dispatch] = useReducer(reducer, initialState); 35 | 36 | const values = useMemo(() => { 37 | return { 38 | state, 39 | Dispatch, 40 | }; 41 | }, [state]); 42 | 43 | return ( 44 | {children} 45 | ); 46 | } 47 | 48 | export const useStateContext = () => useContext(StateContext); 49 | -------------------------------------------------------------------------------- /src/stores/reducerFunctions/reducer.ts: -------------------------------------------------------------------------------- 1 | import { ActionsType } from "@/types/ActionsTypes"; 2 | 3 | export function reducer(state: GlobalStates, action: ActionsType) { 4 | switch (action.type) { 5 | case "SET_SELECTED_CHAT": 6 | return { 7 | ...state, 8 | selectedChat: action.payload.selectedChat, 9 | }; 10 | case "SET_BLUR_HASH": 11 | return { 12 | ...state, 13 | blurhash: action.payload.blurhash, 14 | }; 15 | case "SET_CHAT_ROOM_SELECTED": 16 | return { 17 | ...state, 18 | chatRoomSelected: action.payload.chatRoomSelected, 19 | }; 20 | case "SET_CROPPED_IMAGE": 21 | return { 22 | ...state, 23 | croppedImage: action.payload.croppedImage, 24 | }; 25 | case "SET_PREVIEW_URL": 26 | return { 27 | ...state, 28 | previewUrl: action.payload.previewUrl, 29 | }; 30 | case "SELECT_POST": 31 | return { 32 | ...state, 33 | selectedPost: action.payload.post, 34 | }; 35 | case "SET_RESULT": 36 | return { 37 | ...state, 38 | result: action.payload.result, 39 | }; 40 | default: 41 | return state; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/stores/Drawer/DrawerStates.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | type Dispatch, 3 | type ReactNode, 4 | createContext, 5 | useContext, 6 | useMemo, 7 | useReducer, 8 | } from "react"; 9 | import { DrawerActionsType, drawerreducer } from "../reducerFunctions/drawer"; 10 | 11 | type DrawerStatesproviderTypes = { 12 | drawerStates: DrawerStatesTypes; 13 | drawerDispatch: Dispatch; 14 | }; 15 | export const initialStates = { 16 | isSearchDrawerOpen: false, 17 | resultDrawer: false, 18 | notificationDrawer: false, 19 | receiverDrawer: false, 20 | }; 21 | export const DrawerStates = createContext({ 22 | drawerStates: initialStates, 23 | drawerDispatch: () => {}, 24 | }); 25 | 26 | export const DrawerStatesprovider = ({ children }: { children: ReactNode }) => { 27 | const [drawerStates, drawerDispatch] = useReducer( 28 | drawerreducer, 29 | initialStates 30 | ); 31 | 32 | const values = useMemo(() => { 33 | return { 34 | drawerStates, 35 | drawerDispatch, 36 | }; 37 | }, [drawerStates]); 38 | 39 | return ( 40 | {children} 41 | ); 42 | }; 43 | export const useDrawerContext = () => useContext(DrawerStates); 44 | -------------------------------------------------------------------------------- /src/helper/deletePost.ts: -------------------------------------------------------------------------------- 1 | import { storage, db } from "@/config/firebase"; 2 | import type { Session } from "next-auth"; 3 | import toast from "react-hot-toast"; 4 | 5 | type DeletePostProps = { 6 | post: IUserPostProps | null; 7 | session: Session | null; 8 | }; 9 | 10 | export const deletePost = async (props: T) => { 11 | if (typeof window === "undefined") return; 12 | const { post, session } = props; 13 | 14 | if (!session || !session.user) throw new Error("Please login to delete post"); 15 | const uidNotMatch = session.user.uid !== post?.postedById; 16 | 17 | if (uidNotMatch) throw new Error("You can't delete this post"); 18 | 19 | try { 20 | const { deleteDoc, doc } = await import("firebase/firestore"); 21 | const { deleteObject, ref } = await import("firebase/storage"); 22 | 23 | const postRef = ref(storage, post?.storageRef); 24 | const deleteFromFirestore = await deleteDoc( 25 | doc(db, "posts", `post-${post?.postId}`) 26 | ); 27 | const deleteFromStorage = await deleteObject(postRef); 28 | 29 | await Promise.all([deleteFromFirestore, deleteFromStorage]); 30 | } catch (error) { 31 | if (error instanceof Error) { 32 | toast.error(error.message); 33 | } 34 | } 35 | }; 36 | -------------------------------------------------------------------------------- /src/components/Post/Form.tsx: -------------------------------------------------------------------------------- 1 | import type { FC } from "react"; 2 | import type { UseFormHandleSubmit, FieldValues } from "react-hook-form"; 3 | 4 | type PostFormProps = { 5 | defaultValues: { 6 | captions: string; 7 | }; 8 | updatePost: (data: any) => void; 9 | register: any; 10 | handleSubmit: UseFormHandleSubmit; 11 | }; 12 | 13 | const PostForm: FC = (props) => { 14 | const { defaultValues, updatePost, register, handleSubmit } = props; 15 | return ( 16 |
17 | 26 | 35 |
36 | ); 37 | }; 38 | export default PostForm; 39 | -------------------------------------------------------------------------------- /src/helper/postActions.ts: -------------------------------------------------------------------------------- 1 | import type { Session } from "next-auth"; 2 | 3 | type PostActions = ( 4 | selectedPost: IUserPostProps, 5 | session: Session | null, 6 | refreshData: () => void, 7 | setMenuModal: (menuModal: boolean) => void 8 | ) => Promise; 9 | 10 | export const postActions: PostActions = async ( 11 | selectedPost, 12 | session, 13 | refreshData, 14 | setMenuModal 15 | ) => { 16 | const { toast } = await import("react-hot-toast"); 17 | if (selectedPost?.postedById === session?.user.uid) { 18 | const { deletePost } = await import("@/helper/deletePost"); 19 | const deletePostsArgs = { 20 | post: selectedPost, 21 | refreshData, 22 | session, 23 | }; 24 | deletePost(deletePostsArgs).then(() => { 25 | setMenuModal(false); 26 | refreshData(); 27 | toast.success("Post deleted successfully."); 28 | }); 29 | } else { 30 | const { handleFollow } = await import("@/helper/follow"); 31 | const followArgs = { 32 | id: selectedPost?.postedById as string, 33 | uid: session?.user.uid as string, 34 | followedByName: session?.user.username as string, 35 | followedDate: Date.now(), 36 | followedImage: session?.user.image as string, 37 | }; 38 | await handleFollow(followArgs); 39 | } 40 | }; 41 | -------------------------------------------------------------------------------- /src/hooks/usePost.tsx: -------------------------------------------------------------------------------- 1 | import { db } from "@/config/firebase"; 2 | import { useStateContext } from "@/stores/Global/StateContext"; 3 | import { onSnapshot, doc } from "firebase/firestore"; 4 | import { useState, useEffect, useMemo } from "react"; 5 | type IComment = Pick; 6 | 7 | export default function usePost(post: IUserPostProps | null) { 8 | const [likesCount, setLikesCount] = useState([]); 9 | const [comment, setComment] = useState([]); 10 | const { 11 | state: { selectedPost }, 12 | } = useStateContext(); 13 | 14 | const [savedBy, setSavedBy] = useState([]); 15 | useEffect(() => { 16 | const unsub = onSnapshot( 17 | doc(db, "posts", `post-${post ? post.postId : selectedPost?.postId}`), 18 | (doc) => { 19 | if (doc.exists()) { 20 | setLikesCount(doc.data().likedBy); 21 | setComment(doc?.data().comments); 22 | setSavedBy(doc?.data().savedBy); 23 | } 24 | } 25 | ); 26 | return () => unsub(); 27 | }, [db, selectedPost]); 28 | 29 | const likes = useMemo(() => { 30 | return likesCount; 31 | }, [likesCount]); 32 | 33 | const comments = useMemo(() => { 34 | return comment; 35 | }, [comment]); 36 | 37 | return { likes, comments, savedBy }; 38 | } 39 | -------------------------------------------------------------------------------- /src/stores/reducerFunctions/drawer.ts: -------------------------------------------------------------------------------- 1 | import { initialStates } from "../Drawer/DrawerStates"; 2 | 3 | type PayloadType = 4 | | { type: "TOGGLE_NOTIFICATION_DRAWER"; payload: { notificationDrawer: boolean } } 5 | | { type: "TOGGLE_RECEIVER_DRAWER"; payload: { receiverDrawer: boolean } } 6 | | { type: "TOGGLE_RESULT_DRAWER"; payload: { resultDrawer: boolean } } 7 | | { type: "TOGGLE_SEARCH_DRAWER"; payload: { searchDrawer: boolean } }; 8 | 9 | 10 | export type DrawerActionsType = PayloadType; 11 | 12 | export function drawerreducer( 13 | state: typeof initialStates, 14 | action: DrawerActionsType 15 | ) { 16 | switch (action.type) { 17 | case "TOGGLE_RECEIVER_DRAWER": 18 | return { 19 | ...state, 20 | receiverDrawer: action.payload.receiverDrawer, 21 | }; 22 | case "TOGGLE_SEARCH_DRAWER": 23 | return { 24 | ...state, 25 | isSearchDrawerOpen: action.payload.searchDrawer, 26 | }; 27 | case "TOGGLE_RESULT_DRAWER": 28 | return { 29 | ...state, 30 | resultDrawer: action.payload.resultDrawer, 31 | }; 32 | case "TOGGLE_NOTIFICATION_DRAWER": 33 | return { 34 | ...state, 35 | notificationDrawer: action.payload.notificationDrawer, 36 | }; 37 | default: 38 | return state; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/components/Navigation/ExtraMenuBtn.tsx: -------------------------------------------------------------------------------- 1 | import type { Dispatch, FC, SetStateAction } from "react"; 2 | import { AiOutlineClose } from "react-icons/ai"; 3 | import { RxHamburgerMenu } from "react-icons/rx"; 4 | 5 | type ExtraMenuProps = { 6 | extraList: boolean; 7 | setIsOpen: Dispatch>; 8 | drawer: boolean; 9 | isOpen:boolean 10 | notificationdrawer: boolean; 11 | }; 12 | 13 | const ExtraMenuBtn: FC = (props) => { 14 | const { extraList, setIsOpen, drawer, notificationdrawer, isOpen } = props; 15 | 16 | 17 | return ( 18 | 38 | ); 39 | }; 40 | export default ExtraMenuBtn; 41 | -------------------------------------------------------------------------------- /src/pages/404.tsx: -------------------------------------------------------------------------------- 1 | import Head from "next/head"; 2 | import Image from "next/image"; 3 | export default function NotFound() { 4 | return ( 5 | <> 6 | 7 | 404 • Instafam 8 | 9 |
10 | 404 22 |
23 |
24 |
25 |

26 | You are all alone here 27 |

28 |

29 | 404 30 |

31 |
32 |
33 |
34 | 35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /src/helper/getUser.ts: -------------------------------------------------------------------------------- 1 | import { db } from "@/config/firebase"; 2 | import { collection, getDocs, limit, query, where } from "firebase/firestore"; 3 | 4 | export async function getUserRecommendation(uid: string) { 5 | try { 6 | const q = query(collection(db, "users"), where("uid", "!=", uid)); 7 | const getUsers = await getDocs(q); 8 | return getUsers.docs.map((doc) => doc.data()) as IUser[]; 9 | } catch (error) { 10 | if (error instanceof Error) { 11 | return error.message as Error["message"]; 12 | } 13 | } 14 | } 15 | export async function getUserRecommendationLimit(uid: string) { 16 | try { 17 | const q = query(collection(db, "users"), where("uid", "!=", uid), limit(5)); 18 | const getUsers = await getDocs(q); 19 | return getUsers.docs.map((doc) => doc.data()) as IUser[]; 20 | } catch (error) { 21 | if (error instanceof Error) { 22 | return error.message as Error["message"]; 23 | } 24 | } 25 | } 26 | 27 | export async function getCurrentUserData(username: string = "") { 28 | try { 29 | if (!username) throw new Error("Please add username"); 30 | 31 | const q = query(collection(db, "users"), where("username", "==", username)); 32 | const res = await getDocs(q); 33 | return res.docs.map((doc) => doc.data()) as IUser[]; 34 | } catch (error) { 35 | if (error instanceof Error) { 36 | return error.message as Error["message"]; 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/stores/reducerFunctions/users.ts: -------------------------------------------------------------------------------- 1 | import { ActionsTypeUsersPage } from "@/types/ActionsTypes"; 2 | 3 | export type StatesTypes = { 4 | postTab: boolean; 5 | savedPostTab: boolean; 6 | users: IUser[]; 7 | loadingSavedPosts: boolean; 8 | loadingUsers: boolean; 9 | savedPosts: IUserPostProps[]; 10 | showUsers: boolean; 11 | }; 12 | 13 | export function reducer(state: StatesTypes, action: ActionsTypeUsersPage) { 14 | switch (action.type) { 15 | case "SET_POST_TAB": 16 | return { 17 | ...state, 18 | postTab: true, 19 | savedPostTab: false, 20 | }; 21 | case "SET_SHOW_USERS": 22 | return { 23 | ...state, 24 | showUsers: action.payload.showUsers, 25 | }; 26 | case "SET_Tagged_POST_TAB": 27 | return { 28 | ...state, 29 | postTab: false, 30 | savedPostTab: false, 31 | }; 32 | case "SET_SAVED_POST_TAB": 33 | return { 34 | ...state, 35 | postTab: false, 36 | savedPostTab: true, 37 | }; 38 | case "SET_SAVED_POSTS": 39 | return { 40 | ...state, 41 | savedPosts: action.payload.savedposts, 42 | loadingSavedPosts: false, 43 | }; 44 | 45 | case "SET_USERS": 46 | return { 47 | ...state, 48 | users: action.payload.users, 49 | loadingUsers: false, 50 | }; 51 | default: 52 | return state; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/components/Suggestions/User.tsx: -------------------------------------------------------------------------------- 1 | import Image from "next/image"; 2 | import Link from "next/link"; 3 | import type { FC } from "react"; 4 | 5 | const UserRecommendations: FC<{ reccomend: IUser[] }> = ({ reccomend }) => { 6 | return ( 7 |
8 | {reccomend?.map((user: any) => ( 9 |
13 |
14 | {user?.name 23 |
24 |
{user.username}
25 |

{user.name}

26 |
27 |
28 | 33 | View 34 | 35 |
36 | ))} 37 |
38 | ); 39 | }; 40 | export default UserRecommendations; 41 | -------------------------------------------------------------------------------- /src/stores/Modal/ModalStatesContext.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Dispatch, 3 | ReactNode, 4 | createContext, 5 | useContext, 6 | useMemo, 7 | useReducer, 8 | } from "react"; 9 | 10 | import { modalReducer } from "../reducerFunctions/Modal"; 11 | import { ActionsModalTypes } from "@/types/ActionsTypes"; 12 | 13 | type ModalContextProvidersProps = { 14 | modalStates: ModalStates; 15 | modalDispatch: Dispatch; 16 | }; 17 | 18 | const initialStates: ModalStates = { 19 | showReportModal: false, 20 | showAllUserModal: false, 21 | menuModal: false, 22 | postPreviewModal: false, 23 | feedModal: false, 24 | postModal: false, 25 | postCommentModal: false, 26 | notificationModal: false, 27 | postCreateModal: false, 28 | postReportModal: false, 29 | messageModal: false, 30 | }; 31 | 32 | export const ModalContext = createContext({ 33 | modalDispatch: () => {}, 34 | modalStates: initialStates, 35 | }); 36 | 37 | export const ModalContextProviders = ({ 38 | children, 39 | }: { 40 | children: ReactNode; 41 | }) => { 42 | const [modalStates, modalDispatch] = useReducer(modalReducer, initialStates); 43 | 44 | const values = useMemo(() => { 45 | return { 46 | modalStates, 47 | modalDispatch, 48 | }; 49 | }, [modalStates]); 50 | 51 | return ( 52 | {children} 53 | ); 54 | }; 55 | 56 | export const useModalContext = () => useContext(ModalContext); 57 | -------------------------------------------------------------------------------- /src/helper/reportPost.ts: -------------------------------------------------------------------------------- 1 | import type { FieldValues, UseFormResetField } from "react-hook-form"; 2 | import { getCsrfToken } from "next-auth/react"; 3 | import { db } from "@/config/firebase"; 4 | import { doc, setDoc } from "firebase/firestore"; 5 | import type { Session } from "next-auth"; 6 | import toast from "react-hot-toast"; 7 | 8 | type handleReportFunc = ( 9 | e: FieldValues, 10 | selectedPost: IUserPostProps | null, 11 | session: Session | null, 12 | resetField: UseFormResetField 13 | ) => Promise; 14 | 15 | export const handleReport: handleReportFunc = async ( 16 | e, 17 | selectedPost, 18 | session, 19 | resetField 20 | ) => { 21 | try { 22 | const token = await getCsrfToken(); 23 | if (!session || !token) throw new Error("No Session or token found"); 24 | 25 | const reportRef = doc(db, "reports", `${selectedPost?.postId}`); 26 | const reportData = { 27 | postId: selectedPost?.postId, 28 | reportedBy: session?.user.uid, 29 | reportedAt: Date.now(), 30 | reportedPost: selectedPost?.image, 31 | reportedPostAuthor: selectedPost?.author, 32 | reportedPostAuthorId: selectedPost?.postedById, 33 | reportedPostAuthorImage: selectedPost?.postedByPhotoUrl, 34 | reason: e.reason, 35 | }; 36 | await setDoc(reportRef, reportData).then(() => { 37 | resetField("reason"); 38 | toast.success("Reported Successfully"); 39 | }); 40 | } catch (error) { 41 | toast.error("Something went wrong"); 42 | } 43 | }; 44 | -------------------------------------------------------------------------------- /src/helper/comments.ts: -------------------------------------------------------------------------------- 1 | import type { Session } from "next-auth"; 2 | import type { FieldValues, UseFormResetField } from "react-hook-form"; 3 | 4 | type Props = { 5 | e: FieldValues; 6 | post: IUserPostProps; 7 | session: Session | null; 8 | resetField: UseFormResetField; 9 | }; 10 | 11 | export const postComments = async (args: Props) => { 12 | const { e, post, session, resetField } = args; 13 | const { toast } = await import("react-hot-toast"); 14 | 15 | if (e.comments === "") return toast.error("Please enter a comment"); 16 | 17 | const { getCsrfToken } = await import("next-auth/react"); 18 | const { db } = await import("@/config/firebase"); 19 | const { doc, updateDoc, arrayUnion } = await import("firebase/firestore"); 20 | 21 | try { 22 | if (!session || !session.user) { 23 | throw new Error("You must be logged in to comment"); 24 | } 25 | const token = await getCsrfToken(); 26 | if (!token) { 27 | throw new Error("No CSRF token found"); 28 | } 29 | 30 | const postRef = doc(db, "posts", `post-${post.postId}`); 31 | await updateDoc(postRef, { 32 | comments: arrayUnion({ 33 | commentByUid: session?.user.uid, 34 | comment: e.comments, 35 | commentByName: session?.user.username, 36 | commentByPhoto: session?.user.image, 37 | createdAt: Date.now(), 38 | }), 39 | }).then(() => { 40 | resetField("comments"); 41 | }); 42 | } catch (error: any) { 43 | toast.error(error.message); 44 | } 45 | }; 46 | -------------------------------------------------------------------------------- /src/components/Captions/TextArea.tsx: -------------------------------------------------------------------------------- 1 | import type { Dispatch, FC, SetStateAction } from "react"; 2 | 3 | type TextAreaProps = { 4 | captions: string; 5 | setCaptions: Dispatch>; 6 | loading: boolean; 7 | handlePost: () => Promise; 8 | }; 9 | const TextArea: FC = (props) => { 10 | const { captions, setCaptions, loading, handlePost } = props; 11 | 12 | return ( 13 |
14 | 24 | 40 |
41 | ); 42 | }; 43 | export default TextArea; 44 | -------------------------------------------------------------------------------- /src/components/Post/PostInfo.tsx: -------------------------------------------------------------------------------- 1 | import type { FC } from "react"; 2 | import { AiFillHeart, AiTwotoneMessage } from "react-icons/ai"; 3 | 4 | const PostInfo: FC<{ post: IUserPostProps }> = ({ post }) => { 5 | return ( 6 |
7 |
8 | 19 |
20 | 33 |
34 | ); 35 | }; 36 | export default PostInfo; 37 | -------------------------------------------------------------------------------- /src/components/Comments/Comment.tsx: -------------------------------------------------------------------------------- 1 | import Image from "next/image"; 2 | import Link from "next/link"; 3 | import type { FC } from "react"; 4 | 5 | import { getCreatedDate } from "@/utils/postDate"; 6 | 7 | type CommentsProps = { 8 | comments: Pick["comments"]; 9 | }; 10 | 11 | const Comments: FC = ({ comments }) => { 12 | return ( 13 | <> 14 | {comments?.map((comment) => ( 15 |
16 |
17 | {comment?.commentByName 25 | 29 | {comment?.commentByName} 30 | 31 | {getCreatedDate(comment.createdAt)} 32 | 33 | 34 |
35 |
36 |

37 | {comment?.comment} 38 |

39 |
40 |
41 | ))} 42 | 43 | ); 44 | }; 45 | 46 | export default Comments; 47 | -------------------------------------------------------------------------------- /src/utils/postDate.ts: -------------------------------------------------------------------------------- 1 | export const getCreatedDate = (createdAt: number | string) => { 2 | try { 3 | const now = Date.now(); 4 | const diff = now - Number(createdAt); 5 | 6 | const diffInSeconds = diff / 1000; 7 | const diffInMinutes = diffInSeconds / 60; 8 | const diffInHours = diffInMinutes / 60; 9 | const diffInDays = diffInHours / 24; 10 | const diffInWeeks = diffInDays / 7; 11 | const diffInMonths = diffInDays / 30; 12 | const diffInYears = diffInDays / 365; 13 | 14 | if (diffInSeconds < 60) { 15 | return "just now"; 16 | } else if (diffInMinutes < 60) { 17 | return `${Math.floor(diffInMinutes)} ${ 18 | Math.floor(diffInMinutes) > 1 ? "minutes" : "minute" 19 | } ago`; 20 | } else if (diffInHours < 24) { 21 | return `${Math.floor(diffInHours)} ${ 22 | Math.floor(diffInHours) > 1 ? "hours" : "hour" 23 | } ago`; 24 | } else if (diffInDays < 7) { 25 | return `${Math.floor(diffInDays)} ${ 26 | Math.floor(diffInDays) > 1 ? "days" : "day" 27 | } ago`; 28 | } else if (diffInWeeks < 4) { 29 | return `${Math.floor(diffInWeeks)} ${ 30 | Math.floor(diffInWeeks) > 1 ? "weeks" : "week" 31 | } ago`; 32 | } else if (diffInMonths < 12) { 33 | return `${Math.floor(diffInMonths)} ${ 34 | Math.floor(diffInMonths) > 1 ? "months" : "month" 35 | } ago`; 36 | } else { 37 | return `${Math.floor(diffInYears)} ${ 38 | Math.floor(diffInYears) > 1 ? "years" : "year" 39 | } ago`; 40 | } 41 | } catch (error: any) { 42 | console.log(error.message); 43 | } 44 | }; 45 | -------------------------------------------------------------------------------- /src/components/Post/CreatedTime.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | import type { FC } from "react"; 3 | 4 | import { useModalContext } from "@/stores/Modal/ModalStatesContext"; 5 | import { getCreatedDate } from "@/utils/postDate"; 6 | 7 | type Props = { 8 | author: string; 9 | createdAt: string | number; 10 | }; 11 | 12 | const CreatedTime: FC = ({ createdAt, author }) => { 13 | const createdAtTime = getCreatedDate(createdAt); 14 | const { 15 | modalStates: { postModal }, 16 | modalDispatch, 17 | } = useModalContext(); 18 | return ( 19 |
20 |
21 |
22 |

23 | 25 | postModal 26 | ? modalDispatch({ 27 | type: "TOGGLE_POST_MODAL", 28 | payload: { 29 | postModal: false, 30 | }, 31 | }) 32 | : null 33 | } 34 | href={`/profile/${author}`} 35 | prefetch={false} 36 | className="block text-sm font-semibold leading-tight antialiased" 37 | > 38 | {author} 39 | 40 |

41 | 44 | {createdAtTime} 45 | 46 |
47 |
48 |
49 | ); 50 | }; 51 | 52 | export default CreatedTime; 53 | -------------------------------------------------------------------------------- /src/components/User/Info/Info.tsx: -------------------------------------------------------------------------------- 1 | import type { Session } from "next-auth"; 2 | import type { FC } from "react"; 3 | 4 | type InfoProps = { 5 | users: IUser | null; 6 | session: Session | null; 7 | refreshData: () => void; 8 | }; 9 | const Info: FC = ({ users, session, refreshData }) => { 10 | return ( 11 |
12 |

13 | {users ? users?.username : ""} 14 |

15 | {session?.user.uid !== users?.uid ? ( 16 | 38 | ) : null} 39 |
40 | ); 41 | }; 42 | export default Info; 43 | -------------------------------------------------------------------------------- /src/middleware.ts: -------------------------------------------------------------------------------- 1 | import { withAuth } from "next-auth/middleware"; 2 | import { NextResponse } from "next/server"; 3 | 4 | export default withAuth( 5 | function middleware(req) { 6 | const { pathname } = req.nextUrl; 7 | const regex = new RegExp( 8 | /^\/([a-zA-Z0-9-_+]+\/)*[a-zA-Z0-9-_+]+\.[a-zA-Z0-9]+$/ 9 | ); 10 | const isStatic = regex.test(pathname); 11 | const hasToken = 12 | req.nextauth.token !== null || req.nextauth.token !== undefined; 13 | if (pathname === "/latest/meta-data") { 14 | if (hasToken && isStatic) { 15 | return NextResponse.rewrite(new URL("/", req.url)); 16 | } else { 17 | return NextResponse.rewrite(new URL("/auth/signin", req.url)); 18 | } 19 | } else if (hasToken && isStatic) { 20 | return NextResponse.rewrite(new URL(pathname, req.url)); 21 | } else { 22 | if (isStatic && !hasToken) { 23 | return NextResponse.rewrite(new URL("/auth/signin", req.url)); 24 | } 25 | } 26 | }, 27 | { 28 | callbacks: { 29 | authorized: ({ token }) => token !== null || token !== undefined, 30 | }, 31 | } 32 | ); 33 | export const config = { 34 | matcher: [ 35 | "/", 36 | "/trending", 37 | "/notifications", 38 | "/messages", 39 | "/create", 40 | "/profile", 41 | "/profile/:path*", 42 | "/post/:path*", 43 | "/post/:path*/:path*", 44 | "/api/:path*", 45 | "/auth/:path*", 46 | "/auth/:path*/:path*", 47 | "/auth/signin/:path*/:path*", 48 | "/auth/signin/:path*", 49 | "/((?!api|_next/static|_next/image|favicon.ico).*)", 50 | "/latest", 51 | "/latest/meta-data", 52 | ], 53 | }; 54 | -------------------------------------------------------------------------------- /src/components/Messages/Chats/Header/Header.tsx: -------------------------------------------------------------------------------- 1 | import Image from "next/image"; 2 | import { AiOutlineClose, AiOutlineMenu } from "react-icons/ai"; 3 | import type { FC } from "react"; 4 | import { useDrawerContext } from "@/stores/Drawer/DrawerStates"; 5 | 6 | const ChatHeader: FC<{ selectedChat: DataMessage | null }> = ({ 7 | selectedChat, 8 | }) => { 9 | const { 10 | drawerStates: { receiverDrawer }, 11 | drawerDispatch, 12 | } = useDrawerContext(); 13 | 14 | return ( 15 |
16 |
17 | {selectedChat ? ( 18 | <> 19 | {selectedChat?.name 27 |

{selectedChat?.name}

28 | 29 | ) : null} 30 |
31 | 46 |
47 | ); 48 | }; 49 | export default ChatHeader; 50 | -------------------------------------------------------------------------------- /src/components/ImageCropper/ImageCropper.tsx: -------------------------------------------------------------------------------- 1 | import { Cropper } from "react-cropper-custom"; 2 | import "react-cropper-custom/dist/index.css"; 3 | import type { Dispatch, FC, SetStateAction } from "react"; 4 | 5 | export type Area = { 6 | width: number; 7 | height: number; 8 | x: number; 9 | y: number; 10 | }; 11 | 12 | type ImageCropperProps = { 13 | img: string; 14 | zoom: number; 15 | setZoom: Dispatch>; 16 | onCropComplete: (area: Area) => void; 17 | handleClick: () => void; 18 | }; 19 | const ImageCropper: FC = (props) => { 20 | const { img, zoom, setZoom, onCropComplete, handleClick } = props; 21 | 22 | return ( 23 |
24 |
25 |
26 |
27 | 34 |
35 | 44 |
45 |
46 |
47 | ); 48 | }; 49 | export default ImageCropper; 50 | -------------------------------------------------------------------------------- /src/components/Modal/Drawer/Search/index.tsx: -------------------------------------------------------------------------------- 1 | import dynamic from "next/dynamic"; 2 | import { AiOutlineSearch } from "react-icons/ai"; 3 | import { memo, useRef } from "react"; 4 | 5 | import useClickOutSide from "@/hooks/useClickoutside"; 6 | import { useDrawerContext } from "@/stores/Drawer/DrawerStates"; 7 | const Form = dynamic(() => import("./Form"), { ssr: false }); 8 | 9 | const SearchDrawer = () => { 10 | const { 11 | drawerStates: { isSearchDrawerOpen },drawerDispatch 12 | } = useDrawerContext(); 13 | const searchRef = useRef(null) 14 | 15 | useClickOutSide(searchRef, () => { 16 | drawerDispatch({ 17 | type: 'TOGGLE_SEARCH_DRAWER', 18 | payload: { 19 | searchDrawer:false 20 | } 21 | }) 22 | }) 23 | 24 | if (!isSearchDrawerOpen) return null; 25 | 26 | 27 | return ( 28 | 48 | ); 49 | }; 50 | export default memo(SearchDrawer); 51 | -------------------------------------------------------------------------------- /src/components/Captions/Captions.tsx: -------------------------------------------------------------------------------- 1 | import Image from "next/image"; 2 | import dynamic from "next/dynamic"; 3 | import type { Dispatch, FC, SetStateAction } from "react"; 4 | import type { Session } from "next-auth"; 5 | 6 | const TextArea = dynamic(() => import("./TextArea")); 7 | 8 | type CaptionsProps = { 9 | handlePost: () => Promise; 10 | loading: boolean; 11 | session: Session | null; 12 | img: string; 13 | setCaptions: Dispatch>; 14 | captions: string; 15 | }; 16 | 17 | const Captions: FC = (props) => { 18 | const { handlePost, loading, session, img, setCaptions, captions } = props; 19 | 20 | if (!img) return null; 21 | 22 | return ( 23 |
27 |
28 | {session?.user?.username 36 |

37 | {session?.user?.username} 38 |

39 |
40 |