├── functions
├── tsconfig.dev.json
├── .gitignore
├── tsconfig.json
├── .eslintrc.js
├── package.json
└── src
│ └── index.ts
├── public
├── favicon.ico
├── images
│ ├── googlelogo.png
│ ├── recCommsArt.png
│ ├── redditlogo.png
│ ├── redditPersonalHome.png
│ ├── redditText.svg
│ └── redditFace.svg
└── vercel.svg
├── next.config.js
├── Dockerfile
├── src
├── atoms
│ ├── userAtom.ts
│ ├── authModalAtom.ts
│ ├── directoryMenuAtom.ts
│ ├── postsAtom.ts
│ └── communitiesAtom.ts
├── styles
│ └── globals.css
├── components
│ ├── Main
│ │ └── index.tsx
│ ├── Post
│ │ ├── PostsHome.tsx
│ │ ├── Loader.tsx
│ │ ├── PostForm
│ │ │ ├── TabItem.tsx
│ │ │ ├── TextInputs.tsx
│ │ │ ├── ImageUpload.tsx
│ │ │ └── NewPostForm.tsx
│ │ ├── Comments
│ │ │ ├── Input.tsx
│ │ │ ├── CommentItem.tsx
│ │ │ └── index.tsx
│ │ ├── PostItem
│ │ │ └── index.tsx
│ │ └── Posts.tsx
│ ├── Layout
│ │ ├── index.tsx
│ │ ├── InputField.tsx
│ │ ├── PageContent.tsx
│ │ └── InputItem.tsx
│ ├── Navbar
│ │ ├── RightContent
│ │ │ ├── ActionIcon.tsx
│ │ │ ├── index.tsx
│ │ │ ├── ProfileMenu
│ │ │ │ ├── NoUserList.tsx
│ │ │ │ ├── UserList.tsx
│ │ │ │ └── MenuWrapper.tsx
│ │ │ ├── AuthButtons.tsx
│ │ │ └── Icons.tsx
│ │ ├── Directory
│ │ │ ├── Moderating.tsx
│ │ │ ├── MenuListItem.tsx
│ │ │ ├── MyCommunities.tsx
│ │ │ ├── index.tsx
│ │ │ └── Communities.tsx
│ │ ├── SearchInput.tsx
│ │ └── index.tsx
│ ├── Community
│ │ ├── CommunityNotFound.tsx
│ │ ├── Premium.tsx
│ │ ├── PersonalHome.tsx
│ │ ├── CreatePostLink.tsx
│ │ ├── Temp.tsx
│ │ ├── Header.tsx
│ │ ├── Recommendations.tsx
│ │ └── About.tsx
│ └── Modal
│ │ ├── ModalWrapper.tsx
│ │ ├── Auth
│ │ ├── Inputs.tsx
│ │ ├── OAuthButtons.tsx
│ │ ├── SignUp.tsx
│ │ ├── Login.tsx
│ │ ├── index.tsx
│ │ └── ResetPassword.tsx
│ │ └── CreateCommunity
│ │ └── index.tsx
├── firebase
│ ├── errors.ts
│ ├── authFunctions.ts
│ └── clientApp.ts
├── pages
│ ├── api
│ │ └── hello.ts
│ ├── _document.tsx
│ ├── _app.tsx
│ ├── r
│ │ └── [community]
│ │ │ ├── submit.tsx
│ │ │ ├── index.tsx
│ │ │ └── comments
│ │ │ └── [pid].tsx
│ └── index.tsx
├── helpers
│ └── firestore.ts
├── chakra
│ ├── theme.ts
│ ├── input.ts
│ └── button.ts
└── hooks
│ ├── useSelectFile.ts
│ ├── useAuth.ts
│ ├── useDirectory.ts
│ ├── useCommunityData.ts
│ └── usePosts.ts
├── firebase.json
├── Eks-terraform
├── backend.tf
├── provider.tf
└── main.tf
├── next-env.d.ts
├── service.yml
├── deployment.yml
├── tsconfig.json
├── ingress.yml
├── package.json
├── README.md
└── Installation-script.sh
/functions/tsconfig.dev.json:
--------------------------------------------------------------------------------
1 | {
2 | "include": [
3 | ".eslintrc.js"
4 | ]
5 | }
6 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Aj7Ay/reddit-clone-k8s/HEAD/public/favicon.ico
--------------------------------------------------------------------------------
/public/images/googlelogo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Aj7Ay/reddit-clone-k8s/HEAD/public/images/googlelogo.png
--------------------------------------------------------------------------------
/public/images/recCommsArt.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Aj7Ay/reddit-clone-k8s/HEAD/public/images/recCommsArt.png
--------------------------------------------------------------------------------
/public/images/redditlogo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Aj7Ay/reddit-clone-k8s/HEAD/public/images/redditlogo.png
--------------------------------------------------------------------------------
/public/images/redditPersonalHome.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Aj7Ay/reddit-clone-k8s/HEAD/public/images/redditPersonalHome.png
--------------------------------------------------------------------------------
/next.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {
3 | reactStrictMode: true,
4 | }
5 |
6 | module.exports = nextConfig
7 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:19-alpine3.15
2 |
3 | WORKDIR /reddit-clone
4 |
5 | COPY . /reddit-clone
6 | RUN npm install
7 |
8 | EXPOSE 3000
9 | CMD ["npm","run","dev"]
10 |
--------------------------------------------------------------------------------
/src/atoms/userAtom.ts:
--------------------------------------------------------------------------------
1 | import { atom } from "recoil";
2 |
3 | const defaultUserState = {};
4 |
5 | export const userState = atom({
6 | key: "userState",
7 | default: null,
8 | });
9 |
--------------------------------------------------------------------------------
/firebase.json:
--------------------------------------------------------------------------------
1 | {
2 | "functions": {
3 | "predeploy": [
4 | "npm --prefix \"$RESOURCE_DIR\" run lint",
5 | "npm --prefix \"$RESOURCE_DIR\" run build"
6 | ]
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/functions/.gitignore:
--------------------------------------------------------------------------------
1 | # Compiled JavaScript files
2 | lib/**/*.js
3 | lib/**/*.js.map
4 |
5 | # TypeScript v1 declaration files
6 | typings/
7 |
8 | # Node.js dependency directory
9 | node_modules/
10 |
--------------------------------------------------------------------------------
/Eks-terraform/backend.tf:
--------------------------------------------------------------------------------
1 | terraform {
2 | backend "s3" {
3 | bucket = "ajay-mrcloudbook777" # Replace with your actual S3 bucket name
4 | key = "EKS/terraform.tfstate"
5 | region = "ap-south-1"
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/next-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 |
4 | // NOTE: This file should not be edited
5 | // see https://nextjs.org/docs/basic-features/typescript for more information.
6 |
--------------------------------------------------------------------------------
/src/styles/globals.css:
--------------------------------------------------------------------------------
1 | html,
2 | body {
3 | padding: 0;
4 | margin: 0;
5 | font-family: "Open Sans", sans-serif;
6 | }
7 |
8 | a {
9 | color: inherit;
10 | text-decoration: none;
11 | }
12 |
13 | * {
14 | box-sizing: border-box;
15 | }
16 |
--------------------------------------------------------------------------------
/service.yml:
--------------------------------------------------------------------------------
1 | apiVersion: v1
2 | kind: Service
3 | metadata:
4 | name: reddit-clone-service
5 | spec:
6 | selector:
7 | app: reddit-clone
8 | ports:
9 | - port: 80
10 | targetPort: 3000
11 | protocol: TCP
12 | type: LoadBalancer
13 |
14 |
--------------------------------------------------------------------------------
/Eks-terraform/provider.tf:
--------------------------------------------------------------------------------
1 | terraform {
2 | required_providers {
3 | aws = {
4 | source = "hashicorp/aws"
5 | version = "~> 5.0"
6 | }
7 | }
8 | }
9 |
10 | # Configure the AWS Provider
11 | provider "aws" {
12 | region = "ap-south-1"
13 | }
14 |
--------------------------------------------------------------------------------
/src/components/Main/index.tsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link";
2 | import React from "react";
3 |
4 | const Main: React.FC<{}> = () => {
5 | return (
6 |
7 | Here is main
8 | hehe
9 |
10 | );
11 | };
12 | export default Main;
13 |
--------------------------------------------------------------------------------
/src/firebase/errors.ts:
--------------------------------------------------------------------------------
1 | export const FIREBASE_ERRORS = {
2 | "Firebase: Error (auth/email-already-in-use).":
3 | "A user with that email already exists",
4 |
5 | "Firebase: Error (auth/user-not-found).": "Invalid email or password",
6 | "Firebase: Error (auth/wrong-password).": "Invalid email or password",
7 | };
8 |
--------------------------------------------------------------------------------
/functions/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "module": "commonjs",
4 | "noImplicitReturns": true,
5 | "noUnusedLocals": true,
6 | "outDir": "lib",
7 | "sourceMap": true,
8 | "strict": true,
9 | "target": "es2017"
10 | },
11 | "compileOnSave": true,
12 | "include": [
13 | "src"
14 | ]
15 | }
16 |
--------------------------------------------------------------------------------
/src/components/Post/PostsHome.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 |
3 | type PostsHomeProps = {};
4 |
5 | const PostsHome: React.FC = () => {
6 | const [loading, setLoading] = useState(false);
7 |
8 | // stuff related to home page only
9 | return Home Posts Wrapper
;
10 | };
11 | export default PostsHome;
12 |
--------------------------------------------------------------------------------
/src/pages/api/hello.ts:
--------------------------------------------------------------------------------
1 | // Next.js API route support: https://nextjs.org/docs/api-routes/introduction
2 | import type { NextApiRequest, NextApiResponse } from 'next'
3 |
4 | type Data = {
5 | name: string
6 | }
7 |
8 | export default function handler(
9 | req: NextApiRequest,
10 | res: NextApiResponse
11 | ) {
12 | res.status(200).json({ name: 'John Doe' })
13 | }
14 |
--------------------------------------------------------------------------------
/src/atoms/authModalAtom.ts:
--------------------------------------------------------------------------------
1 | import { atom } from "recoil";
2 |
3 | export interface AuthModalState {
4 | open: boolean;
5 | view: ModalView;
6 | }
7 |
8 | export type ModalView = "login" | "signup" | "resetPassword";
9 |
10 | const defaultModalState: AuthModalState = {
11 | open: false,
12 | view: "login",
13 | };
14 |
15 | export const authModalState = atom({
16 | key: "authModalState",
17 | default: defaultModalState,
18 | });
19 |
--------------------------------------------------------------------------------
/deployment.yml:
--------------------------------------------------------------------------------
1 | apiVersion: apps/v1
2 | kind: Deployment
3 | metadata:
4 | name: reddit-clone-deployment
5 | labels:
6 | app: reddit-clone
7 | spec:
8 | replicas: 2
9 | selector:
10 | matchLabels:
11 | app: reddit-clone
12 | template:
13 | metadata:
14 | labels:
15 | app: reddit-clone
16 | spec:
17 | containers:
18 | - name: reddit-clone
19 | image: sevenajay/reddit:latest
20 | ports:
21 | - containerPort: 3000
22 |
--------------------------------------------------------------------------------
/src/helpers/firestore.ts:
--------------------------------------------------------------------------------
1 | import { user } from "firebase-functions/v1/auth";
2 | import { query, collection, getDocs } from "firebase/firestore";
3 | import { firestore } from "../firebase/clientApp";
4 |
5 | export const getMySnippets = async (userId: string) => {
6 | const snippetQuery = query(
7 | collection(firestore, `users/${userId}/communitySnippets`)
8 | );
9 |
10 | const snippetDocs = await getDocs(snippetQuery);
11 | return snippetDocs.docs.map((doc) => ({ ...doc.data() }));
12 | };
13 |
--------------------------------------------------------------------------------
/src/components/Layout/index.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { useAuthState } from "react-firebase-hooks/auth";
3 | import { auth } from "../../firebase/clientApp";
4 | import useAuth from "../../hooks/useAuth";
5 | import Navbar from "../Navbar";
6 | import AuthModal from "../Modal/Auth";
7 |
8 | const Layout: React.FC = ({ children }) => {
9 | // useAuth(); // will implement later at end of tutorial
10 |
11 | return (
12 | <>
13 |
14 | {children}
15 | >
16 | );
17 | };
18 |
19 | export default Layout;
20 |
--------------------------------------------------------------------------------
/src/firebase/authFunctions.ts:
--------------------------------------------------------------------------------
1 | import { GoogleAuthProvider, signInWithPopup, signOut } from "firebase/auth";
2 |
3 | import { auth } from "./clientApp";
4 |
5 | export const signInWithGoogle: any = async () =>
6 | signInWithPopup(auth, new GoogleAuthProvider());
7 |
8 | export const signUpWithEmailAndPassword = async (
9 | email: string,
10 | password: string
11 | ) => {};
12 |
13 | export const loginWithEmaiAndPassword = async (
14 | email: string,
15 | password: string
16 | ) => {};
17 |
18 | export const logout = () => signOut(auth);
19 |
--------------------------------------------------------------------------------
/src/components/Navbar/RightContent/ActionIcon.tsx:
--------------------------------------------------------------------------------
1 | import { Flex, Icon } from "@chakra-ui/react";
2 | import React from "react";
3 |
4 | type ActionIcon = {
5 | icon: any;
6 | size: number;
7 | onClick?: any;
8 | };
9 |
10 | const ActionIcon: React.FC = ({ icon, size }) => {
11 | return (
12 |
19 |
20 |
21 | );
22 | };
23 | export default ActionIcon;
24 |
--------------------------------------------------------------------------------
/src/chakra/theme.ts:
--------------------------------------------------------------------------------
1 | import { extendTheme } from "@chakra-ui/react";
2 | import { Button } from "./button";
3 | import { Input } from "./input";
4 |
5 | export const theme = extendTheme({
6 | colors: {
7 | brand: {
8 | 100: "#FF3C00",
9 | },
10 | },
11 | fonts: {
12 | body: "Open Sans, sans-serif",
13 | },
14 | styles: {
15 | global: () => ({
16 | body: {
17 | bg: "gray.200",
18 | },
19 | }),
20 | },
21 | components: {
22 | Button,
23 | // Input, // not working for some reason - come back to this
24 | },
25 | });
26 |
--------------------------------------------------------------------------------
/src/components/Community/CommunityNotFound.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Flex, Button } from "@chakra-ui/react";
3 | import Link from "next/link";
4 |
5 | const CommunityNotFound: React.FC = () => {
6 | return (
7 |
13 | Sorry, that community does not exist or has been banned
14 |
15 |
16 |
17 |
18 | );
19 | };
20 | export default CommunityNotFound;
21 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": true,
6 | "skipLibCheck": true,
7 | "strict": true,
8 | "forceConsistentCasingInFileNames": true,
9 | "noEmit": true,
10 | "esModuleInterop": true,
11 | "module": "esnext",
12 | "moduleResolution": "node",
13 | "resolveJsonModule": true,
14 | "isolatedModules": true,
15 | "jsx": "preserve",
16 | "incremental": true
17 | },
18 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
19 | "exclude": ["node_modules"]
20 | }
21 |
--------------------------------------------------------------------------------
/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 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | );
19 | }
20 |
--------------------------------------------------------------------------------
/src/pages/_app.tsx:
--------------------------------------------------------------------------------
1 | import { ChakraProvider } from "@chakra-ui/react";
2 | import type { AppProps } from "next/app";
3 | import { RecoilRoot } from "recoil";
4 | import { theme } from "../chakra/theme";
5 | import Layout from "../components/Layout";
6 | import "../firebase/clientApp";
7 | import "../styles/globals.css";
8 |
9 | function MyApp({ Component, pageProps }: AppProps) {
10 | return (
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | );
19 | }
20 |
21 | export default MyApp;
22 |
--------------------------------------------------------------------------------
/ingress.yml:
--------------------------------------------------------------------------------
1 | apiVersion: networking.k8s.io/v1
2 | kind: Ingress
3 | metadata:
4 | name: ingress-reddit-app
5 | spec:
6 | rules:
7 | - host: "domain.com"
8 | http:
9 | paths:
10 | - pathType: Prefix
11 | path: "/test"
12 | backend:
13 | service:
14 | name: reddit-clone-service
15 | port:
16 | number: 3000
17 | - host: "*.domain.com"
18 | http:
19 | paths:
20 | - pathType: Prefix
21 | path: "/test"
22 | backend:
23 | service:
24 | name: reddit-clone-service
25 | port:
26 | number: 3000
27 |
--------------------------------------------------------------------------------
/src/components/Navbar/RightContent/index.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Flex } from "@chakra-ui/react";
3 | import { User } from "firebase/auth";
4 | import AuthModal from "../../Modal/Auth";
5 | import AuthButtons from "./AuthButtons";
6 | import Icons from "./Icons";
7 | import MenuWrapper from "./ProfileMenu/MenuWrapper";
8 |
9 | type RightContentProps = {
10 | user: User;
11 | };
12 |
13 | const RightContent: React.FC = ({ user }) => {
14 | return (
15 | <>
16 |
17 |
18 | {user ? : }
19 |
20 |
21 | >
22 | );
23 | };
24 | export default RightContent;
25 |
--------------------------------------------------------------------------------
/functions/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | env: {
4 | es6: true,
5 | node: true,
6 | },
7 | extends: [
8 | "eslint:recommended",
9 | "plugin:import/errors",
10 | "plugin:import/warnings",
11 | "plugin:import/typescript",
12 | "google",
13 | "plugin:@typescript-eslint/recommended",
14 | ],
15 | parser: "@typescript-eslint/parser",
16 | parserOptions: {
17 | project: ["tsconfig.json", "tsconfig.dev.json"],
18 | sourceType: "module",
19 | },
20 | ignorePatterns: [
21 | "/lib/**/*", // Ignore built files.
22 | ],
23 | plugins: [
24 | "@typescript-eslint",
25 | "import",
26 | ],
27 | rules: {
28 | "quotes": ["error", "double"],
29 | "import/no-unresolved": 0,
30 | },
31 | };
32 |
--------------------------------------------------------------------------------
/src/components/Modal/ModalWrapper.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Button,
3 | Modal,
4 | ModalOverlay,
5 | ModalContent,
6 | ModalHeader,
7 | ModalCloseButton,
8 | ModalBody,
9 | ModalFooter,
10 | useDisclosure,
11 | } from "@chakra-ui/react";
12 | import React from "react";
13 |
14 | type ModalWrapperProps = {
15 | isOpen: boolean;
16 | onClose: () => void;
17 | };
18 |
19 | const ModalWrapper: React.FC = ({
20 | children,
21 | isOpen,
22 | onClose,
23 | }) => {
24 | return (
25 | <>
26 |
27 |
28 | {children}
29 |
30 | >
31 | );
32 | };
33 | export default ModalWrapper;
34 |
--------------------------------------------------------------------------------
/src/hooks/useSelectFile.ts:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 |
3 | const useSelectFile = () => {
4 | const [selectedFile, setSelectedFile] = useState();
5 |
6 | const onSelectFile = (event: React.ChangeEvent) => {
7 | console.log("THIS IS HAPPENING", event);
8 |
9 | const reader = new FileReader();
10 |
11 | if (event.target.files?.[0]) {
12 | reader.readAsDataURL(event.target.files[0]);
13 | }
14 |
15 | reader.onload = (readerEvent) => {
16 | if (readerEvent.target?.result) {
17 | setSelectedFile(readerEvent.target.result as string);
18 | }
19 | };
20 | };
21 |
22 | return {
23 | selectedFile,
24 | setSelectedFile,
25 | onSelectFile,
26 | };
27 | };
28 | export default useSelectFile;
29 |
--------------------------------------------------------------------------------
/src/atoms/directoryMenuAtom.ts:
--------------------------------------------------------------------------------
1 | import { atom } from "recoil";
2 | import { IconType } from "react-icons";
3 | import { TiHome } from "react-icons/ti";
4 |
5 | export type DirectoryMenuItem = {
6 | displayText: string;
7 | link: string;
8 | icon: IconType;
9 | iconColor: string;
10 | imageURL?: string;
11 | };
12 |
13 | interface DirectoryMenuState {
14 | isOpen: boolean;
15 | selectedMenuItem: DirectoryMenuItem;
16 | }
17 |
18 | export const defaultMenuItem = {
19 | displayText: "Home",
20 | link: "/",
21 | icon: TiHome,
22 | iconColor: "black",
23 | };
24 |
25 | export const defaultMenuState: DirectoryMenuState = {
26 | isOpen: false,
27 | selectedMenuItem: defaultMenuItem,
28 | };
29 |
30 | export const directoryMenuState = atom({
31 | key: "directoryMenuState",
32 | default: defaultMenuState,
33 | });
34 |
--------------------------------------------------------------------------------
/src/components/Post/Loader.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Stack, Box, SkeletonText, Skeleton } from "@chakra-ui/react";
3 |
4 | const PostLoader: React.FC = () => {
5 | return (
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | );
19 | };
20 | export default PostLoader;
21 |
--------------------------------------------------------------------------------
/src/components/Modal/Auth/Inputs.tsx:
--------------------------------------------------------------------------------
1 | import { Flex } from "@chakra-ui/react";
2 | import React from "react";
3 | import { useRecoilState, useRecoilValue } from "recoil";
4 | import { authModalState, ModalView } from "../../../atoms/authModalAtom";
5 | import Login from "./Login";
6 | import SignUp from "./SignUp";
7 |
8 | type AuthInputsProps = {
9 | toggleView: (view: ModalView) => void;
10 | };
11 |
12 | const AuthInputs: React.FC = ({ toggleView }) => {
13 | const modalState = useRecoilValue(authModalState);
14 |
15 | return (
16 |
17 | {modalState.view === "login" ? (
18 |
19 | ) : (
20 |
21 | )}
22 |
23 | );
24 | };
25 | export default AuthInputs;
26 |
--------------------------------------------------------------------------------
/src/chakra/input.ts:
--------------------------------------------------------------------------------
1 | import type { ComponentStyleConfig } from "@chakra-ui/theme";
2 |
3 | export const Input: ComponentStyleConfig = {
4 | baseStyle: {
5 | field: {
6 | fontSize: "10pt",
7 | bg: "gray.50",
8 | _placeholder: {
9 | color: "gray.500",
10 | },
11 | _hover: {
12 | bg: "white",
13 | border: "1px solid",
14 | borderColor: "blue.500",
15 | },
16 | _focus: {
17 | outline: "none",
18 | border: "1px solid",
19 | borderColor: "blue.500",
20 | },
21 | },
22 | addons: {
23 | height: "30px",
24 | },
25 | },
26 | sizes: {
27 | md: {
28 | field: {
29 | // height: "30px",
30 | fontSize: "10pt",
31 | },
32 | },
33 | },
34 | variants: {},
35 | defaultProps: {
36 | variant: null,
37 | },
38 | };
39 |
--------------------------------------------------------------------------------
/src/components/Navbar/RightContent/ProfileMenu/NoUserList.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { MenuItem, Flex, Icon } from "@chakra-ui/react";
3 | import { MdOutlineLogin } from "react-icons/md";
4 | import { AuthModalState } from "../../../../atoms/authModalAtom";
5 |
6 | type NoUserListProps = {
7 | setModalState: (value: AuthModalState) => void;
8 | };
9 |
10 | const NoUserList: React.FC = ({ setModalState }) => {
11 | return (
12 | <>
13 |
24 | >
25 | );
26 | };
27 | export default NoUserList;
28 |
--------------------------------------------------------------------------------
/src/components/Community/Premium.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Flex, Icon, Text, Stack, Button } from "@chakra-ui/react";
3 | import { GiCheckedShield } from "react-icons/gi";
4 |
5 | const Premium: React.FC = () => {
6 | return (
7 |
16 |
17 |
18 |
19 | Reddit Premium
20 | The best Reddit experience, with monthly Coins
21 |
22 |
23 |
26 |
27 | );
28 | };
29 | export default Premium;
30 |
--------------------------------------------------------------------------------
/src/firebase/clientApp.ts:
--------------------------------------------------------------------------------
1 | import { initializeApp, getApp, getApps } from "firebase/app";
2 | import { getAuth } from "firebase/auth";
3 | import { getFirestore } from "firebase/firestore";
4 | import { getStorage } from "firebase/storage";
5 |
6 | // For Firebase JS SDK v7.20.0 and later, measurementId is optional
7 | const firebaseConfig = {
8 | apiKey: "AIzaSyBqe95nCUomWkU9PnZT_OlV_NCAZnTmyz0",
9 | authDomain: "reddit-clone-r.firebaseapp.com",
10 | projectId: "reddit-clone-r",
11 | storageBucket: "reddit-clone-r.appspot.com",
12 | messagingSenderId: "233455437474",
13 | appId: "1:233455437474:web:5f21ac4b94a42fddb379fb",
14 | measurementId: "G-3L9HMZR4XQ"
15 | };
16 | // Initialize Firebase for SSR
17 | const app = !getApps().length ? initializeApp(firebaseConfig) : getApp();
18 | const firestore = getFirestore(app);
19 | const auth = getAuth(app);
20 | const storage = getStorage(app);
21 |
22 | export { app, auth, firestore, storage };
23 |
--------------------------------------------------------------------------------
/src/chakra/button.ts:
--------------------------------------------------------------------------------
1 | import type { ComponentStyleConfig } from "@chakra-ui/theme";
2 |
3 | export const Button: ComponentStyleConfig = {
4 | baseStyle: {
5 | borderRadius: "60px",
6 | fontSize: "10pt",
7 | fontWeight: 700,
8 | _focus: {
9 | boxShadow: "none",
10 | },
11 | },
12 | sizes: {
13 | sm: {
14 | fontSize: "8pt",
15 | },
16 | md: {
17 | fontSize: "10pt",
18 | // height: "28px",
19 | },
20 | },
21 | variants: {
22 | solid: {
23 | color: "white",
24 | bg: "blue.500",
25 | _hover: {
26 | bg: "blue.400",
27 | },
28 | },
29 | outline: {
30 | color: "blue.500",
31 | border: "1px solid",
32 | borderColor: "blue.500",
33 | },
34 | oauth: {
35 | height: "34px",
36 | border: "1px solid",
37 | borderColor: "gray.300",
38 | _hover: {
39 | bg: "gray.50",
40 | },
41 | },
42 | },
43 | };
44 |
--------------------------------------------------------------------------------
/functions/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "functions",
3 | "scripts": {
4 | "lint": "eslint --ext .js,.ts",
5 | "build": "tsc",
6 | "serve": "npm run build && firebase emulators:start --only functions",
7 | "shell": "npm run build && firebase functions:shell",
8 | "start": "npm run shell",
9 | "deploy": "firebase deploy --only functions",
10 | "logs": "firebase functions:log"
11 | },
12 | "engines": {
13 | "node": "16"
14 | },
15 | "main": "lib/index.js",
16 | "dependencies": {
17 | "firebase-admin": "^10.0.2",
18 | "firebase-functions": "^3.18.0"
19 | },
20 | "devDependencies": {
21 | "@typescript-eslint/eslint-plugin": "^5.12.0",
22 | "@typescript-eslint/parser": "^5.12.0",
23 | "eslint": "^8.9.0",
24 | "eslint-config-google": "^0.14.0",
25 | "eslint-plugin-import": "^2.25.4",
26 | "firebase-functions-test": "^0.2.0",
27 | "typescript": "^4.5.4"
28 | },
29 | "private": true
30 | }
31 |
--------------------------------------------------------------------------------
/src/components/Navbar/Directory/Moderating.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Box, Text } from "@chakra-ui/react";
3 | import { FaReddit } from "react-icons/fa";
4 | import { CommunitySnippet } from "../../../atoms/communitiesAtom";
5 | import MenuListItem from "./MenuListItem";
6 |
7 | type ModeratingProps = {
8 | snippets: CommunitySnippet[];
9 | };
10 |
11 | const Moderating: React.FC = ({ snippets }) => {
12 | return (
13 |
14 |
15 | MODERATING
16 |
17 | {snippets.map((snippet) => (
18 |
25 | ))}
26 |
27 | );
28 | };
29 | export default Moderating;
30 |
--------------------------------------------------------------------------------
/src/components/Layout/InputField.tsx:
--------------------------------------------------------------------------------
1 | import { FormControl, Input } from "@chakra-ui/react";
2 | import React from "react";
3 |
4 | type InputFieldProps = {
5 | name: string;
6 | placeholder: string;
7 | type: string;
8 | isRequired?: boolean;
9 | mb?: number;
10 | };
11 |
12 | const InputField: React.FC = ({
13 | name,
14 | placeholder,
15 | type,
16 | isRequired, // not sure if will need this
17 | mb,
18 | }) => {
19 | return (
20 | <>
21 |
41 | >
42 | );
43 | };
44 | export default InputField;
45 |
--------------------------------------------------------------------------------
/src/components/Layout/PageContent.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Box, Flex } from "@chakra-ui/react";
3 |
4 | interface PageContentLayoutProps {
5 | maxWidth?: string;
6 | }
7 |
8 | // Assumes array of two children are passed
9 | const PageContentLayout: React.FC = ({
10 | children,
11 | maxWidth,
12 | }) => {
13 | return (
14 |
15 |
16 |
21 | {children && children[0 as keyof typeof children]}
22 |
23 | {/* Right Content */}
24 |
29 | {children && children[1 as keyof typeof children]}
30 |
31 |
32 |
33 | );
34 | };
35 |
36 | export default PageContentLayout;
37 |
--------------------------------------------------------------------------------
/src/components/Modal/Auth/OAuthButtons.tsx:
--------------------------------------------------------------------------------
1 | import { Button, Flex, Image, Text } from "@chakra-ui/react";
2 | import React from "react";
3 | import { useAuthState, useSignInWithGoogle } from "react-firebase-hooks/auth";
4 | import { auth } from "../../../firebase/clientApp";
5 |
6 | type OAuthButtonsProps = {};
7 |
8 | const OAuthButtons: React.FC = () => {
9 | const [signInWithGoogle, _, loading, error] = useSignInWithGoogle(auth);
10 |
11 | return (
12 |
13 |
22 |
23 | {error && (
24 |
25 | {error}
26 |
27 | )}
28 |
29 | );
30 | };
31 | export default OAuthButtons;
32 |
--------------------------------------------------------------------------------
/public/vercel.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/functions/src/index.ts:
--------------------------------------------------------------------------------
1 | import * as functions from "firebase-functions";
2 | import * as admin from "firebase-admin";
3 |
4 | admin.initializeApp();
5 | const db = admin.firestore();
6 |
7 | export const createUserDocument = functions.auth
8 | .user()
9 | .onCreate(async (user) => {
10 | db.collection("users")
11 | .doc(user.uid)
12 | .set(JSON.parse(JSON.stringify(user)));
13 | });
14 |
15 | export const deletePostComments = functions.firestore
16 | .document(`posts/{postId}`)
17 | .onDelete(async (snap) => {
18 | const postId = snap.id;
19 | console.log("HERE IS POST ID", postId);
20 |
21 | admin
22 | .firestore()
23 | .collection("comments")
24 | .get()
25 | .then((snapshot) => {
26 | snapshot.forEach((doc) => {
27 | if (doc.data().postId === postId) {
28 | console.log("DELETING COMMENT: ", doc.id, doc.data().text);
29 | doc.ref.delete();
30 | }
31 | });
32 | })
33 | .catch((error) => {
34 | console.log("Error deleting post comments");
35 | });
36 | });
37 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "reddit-clone-yt",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev",
7 | "build": "next build",
8 | "start": "next start",
9 | "lint": "next lint"
10 | },
11 | "dependencies": {
12 | "@chakra-ui/icons": "^1.1.7",
13 | "@chakra-ui/react": "^1.8.6",
14 | "@emotion/react": "^11.8.1",
15 | "@emotion/styled": "^11.8.1",
16 | "firebase": "^9.6.8",
17 | "firebase-admin": "^10.0.2",
18 | "firebase-functions": "^3.18.1",
19 | "framer-motion": "^6.2.8",
20 | "moment": "^2.29.1",
21 | "next": "12.1.0",
22 | "nookies": "^2.5.2",
23 | "react": "17.0.2",
24 | "react-dom": "17.0.2",
25 | "react-firebase-hooks": "^5.0.3",
26 | "react-icons": "^4.3.1",
27 | "recoil": "^0.6.1",
28 | "safe-json-stringify": "^1.2.0"
29 | },
30 | "devDependencies": {
31 | "@types/node": "17.0.21",
32 | "@types/react": "17.0.39",
33 | "@types/safe-json-stringify": "^1.1.2",
34 | "eslint": "8.10.0",
35 | "eslint-config-next": "12.1.0",
36 | "typescript": "4.6.2"
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/components/Post/PostForm/TabItem.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Flex, Icon, Text } from "@chakra-ui/react";
3 | import { TabItem } from "./NewPostForm";
4 |
5 | type TabItemProps = {
6 | item: TabItem;
7 | selected: boolean;
8 | setSelectedTab: (value: string) => void;
9 | };
10 |
11 | const TabItem: React.FC = ({
12 | item,
13 | selected,
14 | setSelectedTab,
15 | }) => {
16 | return (
17 | setSelectedTab(item.title)}
30 | >
31 |
32 |
33 |
34 | {item.title}
35 |
36 | );
37 | };
38 | export default TabItem;
39 |
--------------------------------------------------------------------------------
/src/components/Navbar/RightContent/AuthButtons.tsx:
--------------------------------------------------------------------------------
1 | import { Button } from "@chakra-ui/react";
2 | import React, { useState } from "react";
3 | import { useSetRecoilState } from "recoil";
4 | import { authModalState } from "../../../atoms/authModalAtom";
5 | import AuthModal from "../../Modal/Auth";
6 |
7 | type AuthButtonsProps = {};
8 |
9 | const AuthButtons: React.FC = () => {
10 | const setAuthModalState = useSetRecoilState(authModalState);
11 |
12 | return (
13 | <>
14 |
24 |
34 | >
35 | );
36 | };
37 | export default AuthButtons;
38 |
--------------------------------------------------------------------------------
/src/components/Navbar/Directory/MenuListItem.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Flex, Icon, MenuItem, Image } from "@chakra-ui/react";
3 | import { IconType } from "react-icons";
4 | import useDirectory from "../../../hooks/useDirectory";
5 |
6 | type DirectoryItemProps = {
7 | displayText: string;
8 | link: string;
9 | icon: IconType;
10 | iconColor: string;
11 | imageURL?: string;
12 | };
13 |
14 | const MenuListItem: React.FC = ({
15 | displayText,
16 | link,
17 | icon,
18 | iconColor,
19 | imageURL,
20 | }) => {
21 | const { onSelectMenuItem } = useDirectory();
22 | return (
23 |
40 | );
41 | };
42 | export default MenuListItem;
43 |
--------------------------------------------------------------------------------
/src/atoms/postsAtom.ts:
--------------------------------------------------------------------------------
1 | import { atom } from "recoil";
2 | import { Timestamp } from "firebase/firestore";
3 |
4 | export type Post = {
5 | id: string;
6 | communityId: string;
7 | communityImageURL?: string;
8 | userDisplayText: string; // change to authorDisplayText
9 | creatorId: string;
10 | title: string;
11 | body: string;
12 | numberOfComments: number;
13 | voteStatus: number;
14 | currentUserVoteStatus?: {
15 | id: string;
16 | voteValue: number;
17 | };
18 | imageURL?: string;
19 | postIdx?: number;
20 | createdAt?: Timestamp;
21 | editedAt?: Timestamp;
22 | };
23 |
24 | export type PostVote = {
25 | id?: string;
26 | postId: string;
27 | communityId: string;
28 | voteValue: number;
29 | };
30 |
31 | interface PostState {
32 | selectedPost: Post | null;
33 | posts: Post[];
34 | postVotes: PostVote[];
35 | postsCache: {
36 | [key: string]: Post[];
37 | };
38 | postUpdateRequired: boolean;
39 | }
40 |
41 | export const defaultPostState: PostState = {
42 | selectedPost: null,
43 | posts: [],
44 | postVotes: [],
45 | postsCache: {},
46 | postUpdateRequired: true,
47 | };
48 |
49 | export const postState = atom({
50 | key: "postState",
51 | default: defaultPostState,
52 | });
53 |
--------------------------------------------------------------------------------
/src/components/Layout/InputItem.tsx:
--------------------------------------------------------------------------------
1 | import { FormControl, FormErrorMessage, Input } from "@chakra-ui/react";
2 | import React from "react";
3 |
4 | type InputItemProps = {
5 | name: string;
6 | value?: string;
7 | placeholder?: string;
8 | type: string;
9 | onChange?: (event: React.ChangeEvent) => void;
10 | mb?: number;
11 | bg?: string;
12 | size?: string;
13 | };
14 |
15 | const InputItem: React.FC = ({
16 | name,
17 | placeholder,
18 | value,
19 | type,
20 | onChange,
21 | mb,
22 | bg,
23 | size,
24 | }) => {
25 | return (
26 |
51 | );
52 | };
53 | export default InputItem;
54 |
--------------------------------------------------------------------------------
/src/atoms/communitiesAtom.ts:
--------------------------------------------------------------------------------
1 | import { atom } from "recoil";
2 | import { FieldValue, Timestamp } from "firebase/firestore";
3 |
4 | export interface Community {
5 | id: string;
6 | creatorId: string;
7 | numberOfMembers: number;
8 | privacyType: "public" | "restrictied" | "private";
9 | createdAt?: Timestamp;
10 | imageURL?: string;
11 | }
12 |
13 | export interface CommunitySnippet {
14 | communityId: string;
15 | isModerator?: boolean;
16 | imageURL?: string;
17 | }
18 |
19 | interface CommunityState {
20 | [key: string]:
21 | | CommunitySnippet[]
22 | | { [key: string]: Community }
23 | | Community
24 | | boolean
25 | | undefined;
26 | mySnippets: CommunitySnippet[];
27 | initSnippetsFetched: boolean;
28 | visitedCommunities: {
29 | [key: string]: Community;
30 | };
31 | currentCommunity: Community;
32 | }
33 |
34 | export const defaultCommunity: Community = {
35 | id: "",
36 | creatorId: "",
37 | numberOfMembers: 0,
38 | privacyType: "public",
39 | };
40 |
41 | export const defaultCommunityState: CommunityState = {
42 | mySnippets: [],
43 | initSnippetsFetched: false,
44 | visitedCommunities: {},
45 | currentCommunity: defaultCommunity,
46 | };
47 |
48 | export const communityState = atom({
49 | key: "communitiesState",
50 | default: defaultCommunityState,
51 | });
52 |
--------------------------------------------------------------------------------
/src/components/Navbar/Directory/MyCommunities.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { MenuItem, Flex, Icon, Text, Box } from "@chakra-ui/react";
3 | import { FaReddit } from "react-icons/fa";
4 | import { GrAdd } from "react-icons/gr";
5 | import MenuListItem from "./MenuListItem";
6 | import { CommunitySnippet } from "../../../atoms/communitiesAtom";
7 |
8 | type MyCommunitiesProps = {
9 | snippets: CommunitySnippet[];
10 | setOpen: (value: boolean) => void;
11 | };
12 |
13 | const MyCommunities: React.FC = ({ snippets, setOpen }) => {
14 | return (
15 |
16 |
17 | MY COMMUNITIES
18 |
19 |
30 | {snippets.map((snippet) => (
31 |
38 | ))}
39 |
40 | );
41 | };
42 | export default MyCommunities;
43 |
--------------------------------------------------------------------------------
/src/components/Community/PersonalHome.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Button, Flex, Icon, Stack, Text } from "@chakra-ui/react";
3 | import { FaReddit } from "react-icons/fa";
4 |
5 | const PersonalHome: React.FC = () => {
6 | return (
7 |
16 |
27 |
28 |
29 |
30 | Home
31 |
32 |
33 |
34 | Your personal Reddit frontpage, built for you.
35 |
36 |
37 |
40 |
41 |
42 |
43 | );
44 | };
45 | export default PersonalHome;
46 |
--------------------------------------------------------------------------------
/src/components/Navbar/SearchInput.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Flex, InputGroup, InputLeftElement, Input } from "@chakra-ui/react";
3 | import { SearchIcon } from "@chakra-ui/icons";
4 | import { auth } from "firebase-admin";
5 | import { user } from "firebase-functions/v1/auth";
6 | import { User } from "firebase/auth";
7 |
8 | type SearchInputProps = {
9 | user: User;
10 | };
11 |
12 | const SearchInput: React.FC = ({ user }) => {
13 | return (
14 |
20 |
21 | }
25 | >
26 |
27 |
28 |
45 |
46 |
47 | );
48 | };
49 | export default SearchInput;
50 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Reddit Clone App on Kubernetes with Ingress
2 | This project demonstrates how to deploy a Reddit clone app on Kubernetes with Ingress and expose it to the world using Minikube as the cluster.
3 |
4 | ## Prerequisites
5 | Before you begin, you should have the following tools installed on your local machine:
6 |
7 | - Docker
8 | - Kubeatm master and worker node
9 | - kubectl
10 | - Git
11 |
12 | You can install Prerequisites by doing this steps. [click here & complete all steps one by one]().
13 |
14 |
15 | ## Installation
16 | Follow these steps to install and run the Reddit clone app on your local machine:
17 |
18 | 1) Clone this repository to your local machine: `git clone https://github.com/LondheShubham153/reddit-clone-k8s-ingress.git`
19 | 2) Navigate to the project directory: `cd reddit-clone-k8s-ingress`
20 | 3) Build the Docker image for the Reddit clone app: `docker build -t reddit-clone-app .`
21 | 4) Deploy the app to Kubernetes: `kubectl apply -f deployment.yaml`
22 | 1) Deploy the Service for deployment to Kubernetes: `kubectl apply -f service.yaml`
23 | 5) Enable Ingress by using Command: `minikube addons enable ingress`
24 | 6) Expose the app as a Kubernetes service: `kubectl expose deployment reddit-deployment --type=NodePort --port=5000`
25 | 7) Create an Ingress resource: `kubectl apply -f ingress.yaml`
26 |
27 |
28 | ## Test Ingress DNS for the app:
29 |
30 |
31 | ## Contributing
32 | If you'd like to contribute to this project, please open an issue or submit a pull request.
33 |
34 |
35 |
--------------------------------------------------------------------------------
/src/components/Navbar/RightContent/ProfileMenu/UserList.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Flex, Icon, MenuDivider, MenuItem } from "@chakra-ui/react";
3 | import { signOut } from "firebase/auth";
4 | import { CgProfile } from "react-icons/cg";
5 | import { MdOutlineLogin } from "react-icons/md";
6 | import { useResetRecoilState } from "recoil";
7 | import { communityState } from "../../../../atoms/communitiesAtom";
8 | import { auth } from "../../../../firebase/clientApp";
9 |
10 | type UserListProps = {};
11 |
12 | const UserList: React.FC = () => {
13 | const resetCommunityState = useResetRecoilState(communityState);
14 |
15 | const logout = async () => {
16 | await signOut(auth);
17 | resetCommunityState();
18 | };
19 |
20 | return (
21 | <>
22 |
32 |
33 |
44 | >
45 | );
46 | };
47 | export default UserList;
48 |
--------------------------------------------------------------------------------
/src/hooks/useAuth.ts:
--------------------------------------------------------------------------------
1 | import { doc, onSnapshot } from "firebase/firestore";
2 | import { useEffect } from "react";
3 | import { useAuthState } from "react-firebase-hooks/auth";
4 | import { useRecoilState } from "recoil";
5 | import { userState } from "../atoms/userAtom";
6 | import { auth, firestore } from "../firebase/clientApp";
7 | import nookies from "nookies";
8 | import { User } from "firebase/auth";
9 |
10 | const useAuth = () => {
11 | const [user] = useAuthState(auth);
12 | // const [currentUser, setCurrentUser] = useRecoilState(userState); maybe later
13 |
14 | useEffect(() => {
15 | console.log("HERE IS USER", user);
16 |
17 | user ? setUserCookie(user) : nookies.set(undefined, "token", "");
18 | }, [user]);
19 |
20 | const setUserCookie = async (user: User) => {
21 | const token = await user.getIdToken();
22 | console.log("HERE IS TOKEN", token);
23 | nookies.set(undefined, "token", token);
24 | };
25 |
26 | // useEffect(() => {
27 | // // User has logged out; firebase auth state has been cleared
28 | // if (!user?.uid && userState) {
29 | // return setCurrentUser(null);
30 | // }
31 |
32 | // const userDoc = doc(firestore, "users", user?.uid as string);
33 | // const unsubscribe = onSnapshot(userDoc, (doc) => {
34 | // console.log("CURRENT DATA", doc.data());
35 | // if (!doc.data()) return;
36 | // if (currentUser) return;
37 | // setCurrentUser(doc.data() as any);
38 | // });
39 |
40 | // if (currentUser) {
41 | // unsubscribe();
42 | // }
43 |
44 | // return () => unsubscribe();
45 | // }, [user, currentUser]);
46 | };
47 | export default useAuth;
48 |
--------------------------------------------------------------------------------
/src/components/Navbar/index.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Box, Flex, Image } from "@chakra-ui/react";
3 | import { User } from "firebase/auth";
4 | import { useAuthState } from "react-firebase-hooks/auth";
5 | import { useSetRecoilState } from "recoil";
6 | import {
7 | defaultMenuItem,
8 | directoryMenuState,
9 | } from "../../atoms/directoryMenuAtom";
10 | import { auth } from "../../firebase/clientApp";
11 | import Directory from "./Directory";
12 | import RightContent from "./RightContent";
13 | import SearchInput from "./SearchInput";
14 | import router from "next/router";
15 | import useDirectory from "../../hooks/useDirectory";
16 |
17 | const Navbar: React.FC = () => {
18 | const [user] = useAuthState(auth);
19 |
20 | // Use for initial build; implement directory logic near end
21 | const { onSelectMenuItem } = useDirectory();
22 |
23 | return (
24 |
30 | onSelectMenuItem(defaultMenuItem)}
36 | >
37 |
38 |
43 |
44 | {user && }
45 |
46 |
47 |
48 | );
49 | };
50 | export default Navbar;
51 |
--------------------------------------------------------------------------------
/src/components/Post/PostForm/TextInputs.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Stack, Input, Textarea, Flex, Button } from "@chakra-ui/react";
3 |
4 | type TextInputsProps = {
5 | textInputs: {
6 | title: string;
7 | body: string;
8 | };
9 | onChange: (
10 | event: React.ChangeEvent
11 | ) => void;
12 | handleCreatePost: () => void;
13 | loading: boolean;
14 | };
15 |
16 | const TextInputs: React.FC = ({
17 | textInputs,
18 | onChange,
19 | handleCreatePost,
20 | loading,
21 | }) => {
22 | return (
23 |
24 |
39 |
54 |
55 |
64 |
65 |
66 | );
67 | };
68 | export default TextInputs;
69 |
--------------------------------------------------------------------------------
/public/images/redditText.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/components/Post/PostForm/ImageUpload.tsx:
--------------------------------------------------------------------------------
1 | import React, { Ref } from "react";
2 | import { Flex, Stack, Button, Image } from "@chakra-ui/react";
3 |
4 | type ImageUploadProps = {
5 | selectedFile?: string;
6 | setSelectedFile: (value: string) => void;
7 | setSelectedTab: (value: string) => void;
8 | selectFileRef: React.RefObject;
9 | onSelectImage: (event: React.ChangeEvent) => void;
10 | };
11 |
12 | const ImageUpload: React.FC = ({
13 | selectedFile,
14 | setSelectedFile,
15 | setSelectedTab,
16 | selectFileRef,
17 | onSelectImage,
18 | }) => {
19 | return (
20 |
21 | {selectedFile ? (
22 | <>
23 |
28 |
29 |
32 |
39 |
40 | >
41 | ) : (
42 |
51 |
58 |
66 |
67 | )}
68 |
69 | );
70 | };
71 | export default ImageUpload;
72 |
--------------------------------------------------------------------------------
/src/hooks/useDirectory.ts:
--------------------------------------------------------------------------------
1 | import { useRouter } from "next/router";
2 | import React, { useEffect } from "react";
3 | import { useRecoilState, useRecoilValue } from "recoil";
4 | import { communityState } from "../atoms/communitiesAtom";
5 | import {
6 | defaultMenuItem,
7 | DirectoryMenuItem,
8 | directoryMenuState,
9 | } from "../atoms/directoryMenuAtom";
10 | import { FaReddit } from "react-icons/fa";
11 |
12 | const useDirectory = () => {
13 | const [directoryState, setDirectoryState] =
14 | useRecoilState(directoryMenuState);
15 | const router = useRouter();
16 |
17 | const communityStateValue = useRecoilValue(communityState);
18 |
19 | const onSelectMenuItem = (menuItem: DirectoryMenuItem) => {
20 | setDirectoryState((prev) => ({
21 | ...prev,
22 | selectedMenuItem: menuItem,
23 | }));
24 |
25 | router?.push(menuItem.link);
26 | if (directoryState.isOpen) {
27 | toggleMenuOpen();
28 | }
29 | };
30 |
31 | const toggleMenuOpen = () => {
32 | setDirectoryState((prev) => ({
33 | ...prev,
34 | isOpen: !directoryState.isOpen,
35 | }));
36 | };
37 |
38 | useEffect(() => {
39 | const { community } = router.query;
40 |
41 | // const existingCommunity =
42 | // communityStateValue.visitedCommunities[community as string];
43 |
44 | const existingCommunity = communityStateValue.currentCommunity;
45 |
46 | if (existingCommunity.id) {
47 | setDirectoryState((prev) => ({
48 | ...prev,
49 | selectedMenuItem: {
50 | displayText: `r/${existingCommunity.id}`,
51 | link: `r/${existingCommunity.id}`,
52 | icon: FaReddit,
53 | iconColor: "blue.500",
54 | imageURL: existingCommunity.imageURL,
55 | },
56 | }));
57 | return;
58 | }
59 | setDirectoryState((prev) => ({
60 | ...prev,
61 | selectedMenuItem: defaultMenuItem,
62 | }));
63 | }, [communityStateValue.currentCommunity]);
64 | // ^ used to be communityStateValue.vistedCommunities
65 |
66 | return { directoryState, onSelectMenuItem, toggleMenuOpen };
67 | };
68 |
69 | export default useDirectory;
70 |
--------------------------------------------------------------------------------
/src/components/Community/CreatePostLink.tsx:
--------------------------------------------------------------------------------
1 | import { Flex, Icon, Input } from "@chakra-ui/react";
2 | import Link from "next/link";
3 | import { useRouter } from "next/router";
4 | import React from "react";
5 | import { BsLink45Deg } from "react-icons/bs";
6 | import { FaReddit } from "react-icons/fa";
7 | import { IoImageOutline } from "react-icons/io5";
8 | import useDirectory from "../../hooks/useDirectory";
9 |
10 | type CreatePostProps = {};
11 |
12 | const CreatePostLink: React.FC = () => {
13 | const router = useRouter();
14 | const { toggleMenuOpen } = useDirectory();
15 | const onClick = () => {
16 | // Could check for user to open auth modal before redirecting to submit
17 | const { community } = router.query;
18 | if (community) {
19 | router.push(`/r/${router.query.community}/submit`);
20 | return;
21 | }
22 | // Open directory menu to select community to post to
23 | toggleMenuOpen();
24 | };
25 | return (
26 |
37 |
38 |
60 |
67 |
68 |
69 | );
70 | };
71 | export default CreatePostLink;
72 |
--------------------------------------------------------------------------------
/src/components/Community/Temp.tsx:
--------------------------------------------------------------------------------
1 | import { Flex, Icon, Input } from "@chakra-ui/react";
2 | import { useRouter } from "next/router";
3 | import React from "react";
4 | import { useAuthState } from "react-firebase-hooks/auth";
5 | import { BsLink45Deg } from "react-icons/bs";
6 | import { FaReddit } from "react-icons/fa";
7 | import { IoImageOutline } from "react-icons/io5";
8 | import { useSetRecoilState } from "recoil";
9 | import { authModalState } from "../../atoms/authModalAtom";
10 | import { auth } from "../../firebase/clientApp";
11 |
12 | const CreatePostLink: React.FC = () => {
13 | const router = useRouter();
14 | const [user] = useAuthState(auth);
15 | const setAuthModalState = useSetRecoilState(authModalState);
16 |
17 | const onClick = () => {
18 | if (!user) {
19 | setAuthModalState({ open: true, view: "login" });
20 | return;
21 | }
22 | const { communityId } = router.query;
23 | router.push(`/r/${communityId}/submit`);
24 | };
25 |
26 | return (
27 |
38 |
39 |
61 |
68 |
69 |
70 | );
71 | };
72 | export default CreatePostLink;
73 |
--------------------------------------------------------------------------------
/public/images/redditFace.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/pages/r/[community]/submit.tsx:
--------------------------------------------------------------------------------
1 | import { Box, Text } from "@chakra-ui/react";
2 | import { NextPage } from "next";
3 | import { useRouter } from "next/router";
4 | import { useEffect } from "react";
5 | import { useAuthState } from "react-firebase-hooks/auth";
6 | import { useRecoilValue } from "recoil";
7 | import { communityState } from "../../../atoms/communitiesAtom";
8 | import About from "../../../components/Community/About";
9 | import PageContentLayout from "../../../components/Layout/PageContent";
10 | import NewPostForm from "../../../components/Post/PostForm/NewPostForm";
11 | import { auth } from "../../../firebase/clientApp";
12 | import useCommunityData from "../../../hooks/useCommunityData";
13 |
14 | const CreateCommmunityPostPage: NextPage = () => {
15 | const [user, loadingUser, error] = useAuthState(auth);
16 | const router = useRouter();
17 | const { community } = router.query;
18 | // const visitedCommunities = useRecoilValue(communityState).visitedCommunities;
19 | const communityStateValue = useRecoilValue(communityState);
20 | const { loading } = useCommunityData();
21 |
22 | /**
23 | * Not sure why not working
24 | * Attempting to redirect user if not authenticated
25 | */
26 | useEffect(() => {
27 | if (!user && !loadingUser && communityStateValue.currentCommunity.id) {
28 | router.push(`/r/${communityStateValue.currentCommunity.id}`);
29 | }
30 | }, [user, loadingUser, communityStateValue.currentCommunity]);
31 |
32 | console.log("HERE IS USER", user, loadingUser);
33 |
34 | return (
35 |
36 | <>
37 |
38 | Create a post
39 |
40 | {user && (
41 |
46 | )}
47 | >
48 | {communityStateValue.currentCommunity && (
49 | <>
50 |
56 | >
57 | )}
58 |
59 | );
60 | };
61 |
62 | export default CreateCommmunityPostPage;
63 |
--------------------------------------------------------------------------------
/src/components/Post/Comments/Input.tsx:
--------------------------------------------------------------------------------
1 | import React, { MouseEventHandler, useState } from "react";
2 | import { Flex, Textarea, Button, Text } from "@chakra-ui/react";
3 | import { User } from "firebase/auth";
4 | import AuthButtons from "../../Navbar/RightContent/AuthButtons";
5 |
6 | type CommentInputProps = {
7 | comment: string;
8 | setComment: (value: string) => void;
9 | loading: boolean;
10 | user?: User | null;
11 | onCreateComment: (comment: string) => void;
12 | };
13 |
14 | const CommentInput: React.FC = ({
15 | comment,
16 | setComment,
17 | loading,
18 | user,
19 | onCreateComment,
20 | }) => {
21 | return (
22 |
23 | {user ? (
24 | <>
25 |
26 | Comment as{" "}
27 |
28 | {user?.email?.split("@")[0]}
29 |
30 |
31 |
80 | );
81 | };
82 | export default CommentInput;
83 |
--------------------------------------------------------------------------------
/Installation-script.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | sudo apt update -y
3 | sudo touch /etc/apt/keyrings/adoptium.asc
4 | sudo wget -O /etc/apt/keyrings/adoptium.asc https://packages.adoptium.net/artifactory/api/gpg/key/public
5 | echo "deb [signed-by=/etc/apt/keyrings/adoptium.asc] https://packages.adoptium.net/artifactory/deb $(awk -F= '/^VERSION_CODENAME/{print$2}' /etc/os-release) main" | sudo tee /etc/apt/sources.list.d/adoptium.list
6 | sudo apt update -y
7 | sudo apt install temurin-17-jdk -y
8 | /usr/bin/java --version
9 | curl -fsSL https://pkg.jenkins.io/debian-stable/jenkins.io-2023.key | sudo tee \
10 | /usr/share/keyrings/jenkins-keyring.asc > /dev/null
11 | echo deb [signed-by=/usr/share/keyrings/jenkins-keyring.asc] \
12 | https://pkg.jenkins.io/debian-stable binary/ | sudo tee \
13 | /etc/apt/sources.list.d/jenkins.list > /dev/null
14 | sudo apt-get update -y
15 | sudo apt-get install jenkins -y
16 | sudo systemctl start jenkins
17 | sudo systemctl status jenkins
18 | sudo cat /var/lib/jenkins/secrets/initialAdminPassword
19 |
20 | #Install docker
21 | sudo apt install docker.io -y
22 | sudo usermod -aG docker ubuntu
23 | newgrp docker
24 | sudo chmod 777 /var/run/docker.sock
25 | docker version
26 |
27 | # Install Trivy
28 | sudo apt-get install wget apt-transport-https gnupg lsb-release -y
29 | wget -qO - https://aquasecurity.github.io/trivy-repo/deb/public.key | gpg --dearmor | sudo tee /usr/share/keyrings/trivy.gpg > /dev/null
30 | echo "deb [signed-by=/usr/share/keyrings/trivy.gpg] https://aquasecurity.github.io/trivy-repo/deb $(lsb_release -sc) main" | sudo tee -a /etc/apt/sources.list.d/trivy.list
31 | sudo apt-get update
32 | sudo apt-get install trivy -y
33 |
34 | # Install Terraform
35 | sudo apt install wget -y
36 | wget -O- https://apt.releases.hashicorp.com/gpg | sudo gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg
37 | echo "deb [signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list
38 | sudo apt update && sudo apt install terraform
39 |
40 | # Install kubectl
41 | sudo apt update
42 | sudo apt install curl -y
43 | curl -LO https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl
44 | sudo install -o root -g root -m 0755 kubectl /usr/local/bin/kubectl
45 | kubectl version --client
46 |
47 | # Install AWS CLI
48 | curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip"
49 | sudo apt-get install unzip -y
50 | unzip awscliv2.zip
51 | sudo ./aws/install
52 |
--------------------------------------------------------------------------------
/src/components/Navbar/RightContent/ProfileMenu/MenuWrapper.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | import { ChevronDownIcon } from "@chakra-ui/icons";
4 | import {
5 | Box,
6 | Flex,
7 | Icon,
8 | Menu,
9 | MenuButton,
10 | MenuList,
11 | Text,
12 | } from "@chakra-ui/react";
13 | import { useAuthState } from "react-firebase-hooks/auth";
14 | import { useRecoilState } from "recoil";
15 | import { authModalState } from "../../../../atoms/authModalAtom";
16 | import { auth } from "../../../../firebase/clientApp";
17 |
18 | import NoUserList from "./NoUserList";
19 | import UserList from "./UserList";
20 |
21 | import { FaRedditSquare } from "react-icons/fa";
22 | import { VscAccount } from "react-icons/vsc";
23 | import { IoSparkles } from "react-icons/io5";
24 |
25 | type MenuWrapperProps = {};
26 |
27 | const MenuWrapper: React.FC = () => {
28 | const [authModal, setModalState] = useRecoilState(authModalState);
29 | const [user] = useAuthState(auth);
30 | return (
31 |
75 | );
76 | };
77 | export default MenuWrapper;
78 |
--------------------------------------------------------------------------------
/src/components/Navbar/Directory/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import { ChevronDownIcon } from "@chakra-ui/icons";
3 | import {
4 | Box,
5 | Flex,
6 | Icon,
7 | Menu,
8 | MenuButton,
9 | MenuList,
10 | Text,
11 | Image,
12 | } from "@chakra-ui/react";
13 | import useDirectory from "../../../hooks/useDirectory";
14 | import Communities from "./Communities";
15 |
16 | const Directory: React.FC = () => {
17 | const [open, setOpen] = useState(false);
18 | const handleClose = () => setOpen(false);
19 |
20 | const { directoryState, toggleMenuOpen } = useDirectory();
21 |
22 | return (
23 |
77 | );
78 | };
79 | export default Directory;
80 |
--------------------------------------------------------------------------------
/src/components/Navbar/RightContent/Icons.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { AddIcon } from "@chakra-ui/icons";
3 | import { Box, Flex, Icon } from "@chakra-ui/react";
4 | import { BsArrowUpRightCircle, BsChatDots } from "react-icons/bs";
5 | import { GrAdd } from "react-icons/gr";
6 | import {
7 | IoFilterCircleOutline,
8 | IoNotificationsOutline,
9 | IoVideocamOutline,
10 | } from "react-icons/io5";
11 | import useDirectory from "../../../hooks/useDirectory";
12 |
13 | type ActionIconsProps = {};
14 |
15 | const ActionIcons: React.FC = () => {
16 | const { toggleMenuOpen } = useDirectory();
17 | return (
18 |
19 |
25 |
33 |
34 |
35 |
43 |
44 |
45 |
53 |
54 |
55 |
56 | <>
57 |
65 |
66 |
67 |
75 |
76 |
77 |
87 |
88 |
89 | >
90 |
91 | );
92 | };
93 | export default ActionIcons;
94 |
--------------------------------------------------------------------------------
/src/components/Community/Header.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Box, Button, Flex, Icon, Text, Image } from "@chakra-ui/react";
3 | import { FaReddit } from "react-icons/fa";
4 | import { Community, communityState } from "../../atoms/communitiesAtom";
5 | import useCommunityData from "../../hooks/useCommunityData";
6 | import { useSetRecoilState } from "recoil";
7 |
8 | type HeaderProps = {
9 | communityData: Community;
10 | };
11 |
12 | const Header: React.FC = ({ communityData }) => {
13 | /**
14 | * !!!Don't pass communityData boolean until the end
15 | * It's a small optimization!!!
16 | */
17 | const { communityStateValue, loading, error, onJoinLeaveCommunity } =
18 | useCommunityData(!!communityData);
19 | const isJoined = !!communityStateValue.mySnippets.find(
20 | (item) => item.communityId === communityData.id
21 | );
22 |
23 | return (
24 |
25 |
26 |
27 |
28 | {/* IMAGE URL IS ADDED AT THE VERY END BEFORE DUMMY DATA - USE ICON AT FIRST */}
29 | {communityStateValue.currentCommunity.imageURL ? (
30 |
40 | ) : (
41 |
50 | )}
51 |
52 |
53 |
54 | {communityData.id}
55 |
56 |
57 | r/{communityData.id}
58 |
59 |
60 |
61 |
71 |
72 |
73 |
74 |
75 |
76 | );
77 | };
78 | export default Header;
79 |
--------------------------------------------------------------------------------
/src/components/Navbar/Directory/Communities.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import { Box, Flex, Icon, MenuItem, Text } from "@chakra-ui/react";
3 | import { useAuthState } from "react-firebase-hooks/auth";
4 | import { FaReddit } from "react-icons/fa";
5 | import { GrAdd } from "react-icons/gr";
6 | import { useRecoilValue } from "recoil";
7 | import { communityState } from "../../../atoms/communitiesAtom";
8 | import { auth } from "../../../firebase/clientApp";
9 | import CreateCommunityModal from "../../Modal/CreateCommunity";
10 | import MenuListItem from "./MenuListItem";
11 |
12 | type CommunitiesProps = {
13 | menuOpen: boolean;
14 | };
15 |
16 | const Communities: React.FC = ({ menuOpen }) => {
17 | const [user] = useAuthState(auth);
18 | const [open, setOpen] = useState(false);
19 | const mySnippets = useRecoilValue(communityState).mySnippets;
20 |
21 | return (
22 | <>
23 | setOpen(false)}
26 | userId={user?.uid!}
27 | />
28 | {/* COULD DO THIS FOR CLEANER COMPONENTS */}
29 | {/* item.isModerator)} />
30 | */}
31 | {mySnippets.find((item) => item.isModerator) && (
32 |
33 |
34 | MODERATING
35 |
36 | {mySnippets
37 | .filter((item) => item.isModerator)
38 | .map((snippet) => (
39 |
46 | ))}
47 |
48 | )}
49 |
50 |
51 | MY COMMUNITIES
52 |
53 |
64 | {mySnippets.map((snippet) => (
65 |
73 | ))}
74 |
75 | >
76 | );
77 | };
78 |
79 | export default Communities;
80 |
--------------------------------------------------------------------------------
/src/components/Modal/Auth/SignUp.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import { Button, Flex, Text } from "@chakra-ui/react";
3 | import { useCreateUserWithEmailAndPassword } from "react-firebase-hooks/auth";
4 | import { ModalView } from "../../../atoms/authModalAtom";
5 | import { auth } from "../../../firebase/clientApp";
6 | import { FIREBASE_ERRORS } from "../../../firebase/errors";
7 | import InputItem from "../../Layout/InputItem";
8 |
9 | type SignUpProps = {
10 | toggleView: (view: ModalView) => void;
11 | };
12 |
13 | const SignUp: React.FC = ({ toggleView }) => {
14 | const [form, setForm] = useState({
15 | email: "",
16 | password: "",
17 | confirmPassword: "",
18 | });
19 | const [formError, setFormError] = useState("");
20 | const [createUserWithEmailAndPassword, _, loading, authError] =
21 | useCreateUserWithEmailAndPassword(auth);
22 |
23 | const onSubmit = (event: React.FormEvent) => {
24 | event.preventDefault();
25 | if (formError) setFormError("");
26 | if (!form.email.includes("@")) {
27 | return setFormError("Please enter a valid email");
28 | }
29 |
30 | if (form.password !== form.confirmPassword) {
31 | return setFormError("Passwords do not match");
32 | }
33 |
34 | // Valid form inputs
35 | createUserWithEmailAndPassword(form.email, form.password);
36 | };
37 |
38 | const onChange = ({
39 | target: { name, value },
40 | }: React.ChangeEvent) => {
41 | setForm((prev) => ({
42 | ...prev,
43 | [name]: value,
44 | }));
45 | };
46 |
47 | return (
48 |
95 | );
96 | };
97 | export default SignUp;
98 |
--------------------------------------------------------------------------------
/src/components/Modal/Auth/Login.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import { Button, Flex, Text } from "@chakra-ui/react";
3 | import { useSignInWithEmailAndPassword } from "react-firebase-hooks/auth";
4 | import { ModalView } from "../../../atoms/authModalAtom";
5 | import { auth } from "../../../firebase/clientApp";
6 | import { FIREBASE_ERRORS } from "../../../firebase/errors";
7 | import InputItem from "../../Layout/InputItem";
8 |
9 | type LoginProps = {
10 | toggleView: (view: ModalView) => void;
11 | };
12 |
13 | const Login: React.FC = ({ toggleView }) => {
14 | const [form, setForm] = useState({
15 | email: "",
16 | password: "",
17 | });
18 | const [formError, setFormError] = useState("");
19 |
20 | const [signInWithEmailAndPassword, _, loading, authError] =
21 | useSignInWithEmailAndPassword(auth);
22 |
23 | const onSubmit = (event: React.FormEvent) => {
24 | event.preventDefault();
25 | if (formError) setFormError("");
26 | if (!form.email.includes("@")) {
27 | return setFormError("Please enter a valid email");
28 | }
29 |
30 | // Valid form inputs
31 | signInWithEmailAndPassword(form.email, form.password);
32 | };
33 |
34 | const onChange = ({
35 | target: { name, value },
36 | }: React.ChangeEvent) => {
37 | setForm((prev) => ({
38 | ...prev,
39 | [name]: value,
40 | }));
41 | };
42 |
43 | return (
44 |
97 | );
98 | };
99 | export default Login;
100 |
--------------------------------------------------------------------------------
/src/components/Post/Comments/CommentItem.tsx:
--------------------------------------------------------------------------------
1 | import React, { useCallback, useState } from "react";
2 | import {
3 | Avatar,
4 | Box,
5 | Flex,
6 | Icon,
7 | Spinner,
8 | Stack,
9 | Text,
10 | } from "@chakra-ui/react";
11 | import { Timestamp } from "firebase/firestore";
12 | import moment from "moment";
13 | import { FaReddit } from "react-icons/fa";
14 | import {
15 | IoArrowDownCircleOutline,
16 | IoArrowUpCircleOutline,
17 | } from "react-icons/io5";
18 |
19 | export type Comment = {
20 | id?: string;
21 | creatorId: string;
22 | creatorDisplayText: string;
23 | creatorPhotoURL: string;
24 | communityId: string;
25 | postId: string;
26 | postTitle: string;
27 | text: string;
28 | createdAt?: Timestamp;
29 | };
30 |
31 | type CommentItemProps = {
32 | comment: Comment;
33 | onDeleteComment: (comment: Comment) => void;
34 | isLoading: boolean;
35 | userId?: string;
36 | };
37 |
38 | const CommentItem: React.FC = ({
39 | comment,
40 | onDeleteComment,
41 | isLoading,
42 | userId,
43 | }) => {
44 | // const [loading, setLoading] = useState(false);
45 |
46 | // const handleDelete = useCallback(async () => {
47 | // setLoading(true);
48 | // try {
49 | // const success = await onDeleteComment(comment);
50 |
51 | // if (!success) {
52 | // throw new Error("Error deleting comment");
53 | // }
54 | // } catch (error: any) {
55 | // console.log(error.message);
56 | // // setError
57 | // setLoading(false);
58 | // }
59 | // }, [setLoading]);
60 |
61 | return (
62 |
63 |
64 |
65 |
66 |
67 |
68 |
72 | {comment.creatorDisplayText}
73 |
74 | {comment.createdAt?.seconds && (
75 |
76 | {moment(new Date(comment.createdAt?.seconds * 1000)).fromNow()}
77 |
78 | )}
79 | {isLoading && }
80 |
81 | {comment.text}
82 |
89 |
90 |
91 | {userId === comment.creatorId && (
92 | <>
93 |
94 | Edit
95 |
96 | onDeleteComment(comment)}
100 | >
101 | Delete
102 |
103 | >
104 | )}
105 |
106 |
107 |
108 | );
109 | };
110 | export default CommentItem;
111 |
--------------------------------------------------------------------------------
/Eks-terraform/main.tf:
--------------------------------------------------------------------------------
1 | data "aws_iam_policy_document" "assume_role" {
2 | statement {
3 | effect = "Allow"
4 |
5 | principals {
6 | type = "Service"
7 | identifiers = ["eks.amazonaws.com"]
8 | }
9 |
10 | actions = ["sts:AssumeRole"]
11 | }
12 | }
13 |
14 | resource "aws_iam_role" "example" {
15 | name = "eks-cluster-cloud"
16 | assume_role_policy = data.aws_iam_policy_document.assume_role.json
17 | }
18 |
19 | resource "aws_iam_role_policy_attachment" "example-AmazonEKSClusterPolicy" {
20 | policy_arn = "arn:aws:iam::aws:policy/AmazonEKSClusterPolicy"
21 | role = aws_iam_role.example.name
22 | }
23 |
24 | #get vpc data
25 | data "aws_vpc" "default" {
26 | default = true
27 | }
28 | #get public subnets for cluster
29 | data "aws_subnets" "public" {
30 | filter {
31 | name = "vpc-id"
32 | values = [data.aws_vpc.default.id]
33 | }
34 | }
35 | #cluster provision
36 | resource "aws_eks_cluster" "example" {
37 | name = "EKS_CLOUD"
38 | role_arn = aws_iam_role.example.arn
39 |
40 | vpc_config {
41 | subnet_ids = data.aws_subnets.public.ids
42 | }
43 |
44 | # Ensure that IAM Role permissions are created before and deleted after EKS Cluster handling.
45 | # Otherwise, EKS will not be able to properly delete EKS managed EC2 infrastructure such as Security Groups.
46 | depends_on = [
47 | aws_iam_role_policy_attachment.example-AmazonEKSClusterPolicy,
48 | ]
49 | }
50 |
51 | resource "aws_iam_role" "example1" {
52 | name = "eks-node-group-cloud"
53 |
54 | assume_role_policy = jsonencode({
55 | Statement = [{
56 | Action = "sts:AssumeRole"
57 | Effect = "Allow"
58 | Principal = {
59 | Service = "ec2.amazonaws.com"
60 | }
61 | }]
62 | Version = "2012-10-17"
63 | })
64 | }
65 |
66 | resource "aws_iam_role_policy_attachment" "example-AmazonEKSWorkerNodePolicy" {
67 | policy_arn = "arn:aws:iam::aws:policy/AmazonEKSWorkerNodePolicy"
68 | role = aws_iam_role.example1.name
69 | }
70 |
71 | resource "aws_iam_role_policy_attachment" "example-AmazonEKS_CNI_Policy" {
72 | policy_arn = "arn:aws:iam::aws:policy/AmazonEKS_CNI_Policy"
73 | role = aws_iam_role.example1.name
74 | }
75 |
76 | resource "aws_iam_role_policy_attachment" "example-AmazonEC2ContainerRegistryReadOnly" {
77 | policy_arn = "arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly"
78 | role = aws_iam_role.example1.name
79 | }
80 |
81 | #create node group
82 | resource "aws_eks_node_group" "example" {
83 | cluster_name = aws_eks_cluster.example.name
84 | node_group_name = "Node-cloud"
85 | node_role_arn = aws_iam_role.example1.arn
86 | subnet_ids = data.aws_subnets.public.ids
87 |
88 | scaling_config {
89 | desired_size = 1
90 | max_size = 2
91 | min_size = 1
92 | }
93 | instance_types = ["t2.medium"]
94 |
95 | # Ensure that IAM Role permissions are created before and deleted after EKS Node Group handling.
96 | # Otherwise, EKS will not be able to properly delete EC2 Instances and Elastic Network Interfaces.
97 | depends_on = [
98 | aws_iam_role_policy_attachment.example-AmazonEKSWorkerNodePolicy,
99 | aws_iam_role_policy_attachment.example-AmazonEKS_CNI_Policy,
100 | aws_iam_role_policy_attachment.example-AmazonEC2ContainerRegistryReadOnly,
101 | ]
102 | }
103 |
--------------------------------------------------------------------------------
/src/components/Modal/Auth/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from "react";
2 | import {
3 | Flex,
4 | Modal,
5 | ModalBody,
6 | ModalCloseButton,
7 | ModalContent,
8 | ModalHeader,
9 | ModalOverlay,
10 | } from "@chakra-ui/react";
11 | import { useAuthState } from "react-firebase-hooks/auth";
12 | import { useRecoilState, useRecoilValue } from "recoil";
13 | import { authModalState } from "../../../atoms/authModalAtom";
14 | import { userState } from "../../../atoms/userAtom";
15 | import { auth } from "../../../firebase/clientApp";
16 | import AuthInputs from "./Inputs";
17 | import OAuthButtons from "./OAuthButtons";
18 | import ResetPassword from "./ResetPassword";
19 | import ModalWrapper from "../ModalWrapper";
20 |
21 | type AuthModalProps = {};
22 |
23 | const AuthModal: React.FC = () => {
24 | const [modalState, setModalState] = useRecoilState(authModalState);
25 | const handleClose = () =>
26 | setModalState((prev) => ({
27 | ...prev,
28 | open: false,
29 | }));
30 |
31 | const currentUser = useRecoilValue(userState);
32 | const [user, error] = useAuthState(auth);
33 |
34 | // Can implement at the end
35 | // useEffect(() => {
36 | // if (currentUser) handleClose();
37 | // }, [currentUser]);
38 | const toggleView = (view: string) => {
39 | setModalState({
40 | ...modalState,
41 | view: view as typeof modalState.view,
42 | });
43 | };
44 |
45 | useEffect(() => {
46 | if (user) handleClose();
47 | }, [user]);
48 |
49 | return (
50 |
51 |
52 | {modalState.view === "login" && "Login"}
53 | {modalState.view === "signup" && "Sign Up"}
54 | {modalState.view === "resetPassword" && "Reset Password"}
55 |
56 |
57 |
64 |
70 | {modalState.view === "login" || modalState.view === "signup" ? (
71 | <>
72 |
73 | OR
74 |
75 | >
76 | ) : (
77 |
78 | )}
79 | {/* // Will implement at end of tutorial */}
80 | {/* {user && !currentUser && (
81 | <>
82 |
83 |
84 | You are logged in. You will be redirected soon
85 |
86 | >
87 | )} */}
88 | {/* {false ? (
89 |
95 |
96 | ) : (
97 | )} */}
98 |
99 |
100 |
101 | );
102 | };
103 | export default AuthModal;
104 |
--------------------------------------------------------------------------------
/src/pages/r/[community]/index.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect } from "react";
2 | import { doc, getDoc } from "firebase/firestore";
3 | import type { GetServerSidePropsContext, NextPage } from "next";
4 | import { useAuthState } from "react-firebase-hooks/auth";
5 | import { useRecoilState } from "recoil";
6 | import safeJsonStringify from "safe-json-stringify";
7 | import { Community, communityState } from "../../../atoms/communitiesAtom";
8 | import About from "../../../components/Community/About";
9 | import CommunityNotFound from "../../../components/Community/CommunityNotFound";
10 | import CreatePostLink from "../../../components/Community/CreatePostLink";
11 | import Header from "../../../components/Community/Header";
12 | import PageContentLayout from "../../../components/Layout/PageContent";
13 | import Posts from "../../../components/Post/Posts";
14 | import { auth, firestore } from "../../../firebase/clientApp";
15 |
16 | interface CommunityPageProps {
17 | communityData: Community;
18 | }
19 |
20 | const CommunityPage: NextPage = ({ communityData }) => {
21 | const [user, loadingUser] = useAuthState(auth);
22 |
23 | const [communityStateValue, setCommunityStateValue] =
24 | useRecoilState(communityState);
25 |
26 | // useEffect(() => {
27 | // // First time the user has navigated to this community page during session - add to cache
28 | // const firstSessionVisit =
29 | // !communityStateValue.visitedCommunities[communityData.id!];
30 |
31 | // if (firstSessionVisit) {
32 | // setCommunityStateValue((prev) => ({
33 | // ...prev,
34 | // visitedCommunities: {
35 | // ...prev.visitedCommunities,
36 | // [communityData.id!]: communityData,
37 | // },
38 | // }));
39 | // }
40 | // }, [communityData]);
41 |
42 | useEffect(() => {
43 | setCommunityStateValue((prev) => ({
44 | ...prev,
45 | currentCommunity: communityData,
46 | }));
47 | }, [communityData]);
48 |
49 | // Community was not found in the database
50 | if (!communityData) {
51 | return ;
52 | }
53 |
54 | return (
55 | <>
56 |
57 |
58 | {/* Left Content */}
59 | <>
60 |
61 |
66 | >
67 | {/* Right Content */}
68 | <>
69 |
70 | >
71 |
72 | >
73 | );
74 | };
75 |
76 | export default CommunityPage;
77 |
78 | export async function getServerSideProps(context: GetServerSidePropsContext) {
79 | console.log("GET SERVER SIDE PROPS RUNNING");
80 |
81 | try {
82 | const communityDocRef = doc(
83 | firestore,
84 | "communities",
85 | context.query.community as string
86 | );
87 | const communityDoc = await getDoc(communityDocRef);
88 | return {
89 | props: {
90 | communityData: communityDoc.exists()
91 | ? JSON.parse(
92 | safeJsonStringify({ id: communityDoc.id, ...communityDoc.data() }) // needed for dates
93 | )
94 | : "",
95 | },
96 | };
97 | } catch (error) {
98 | // Could create error page here
99 | console.log("getServerSideProps error - [community]", error);
100 | }
101 | }
102 |
--------------------------------------------------------------------------------
/src/components/Modal/Auth/ResetPassword.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import { Button, Flex, Icon, Input, Text } from "@chakra-ui/react";
3 | import { useSendPasswordResetEmail } from "react-firebase-hooks/auth";
4 | import { BsDot, BsReddit } from "react-icons/bs";
5 | import { authModalState, ModalView } from "../../../atoms/authModalAtom";
6 | import { auth } from "../../../firebase/clientApp";
7 | import { useSetRecoilState } from "recoil";
8 |
9 | type ResetPasswordProps = {
10 | toggleView: (view: ModalView) => void;
11 | };
12 |
13 | const ResetPassword: React.FC = ({ toggleView }) => {
14 | const setAuthModalState = useSetRecoilState(authModalState);
15 | const [email, setEmail] = useState("");
16 | const [success, setSuccess] = useState(false);
17 | const [sendPasswordResetEmail, sending, error] =
18 | useSendPasswordResetEmail(auth);
19 |
20 | const onSubmit = async (event: React.FormEvent) => {
21 | event.preventDefault();
22 |
23 | await sendPasswordResetEmail(email);
24 | setSuccess(true);
25 | };
26 | return (
27 |
28 |
29 |
30 | Reset your password
31 |
32 | {success ? (
33 | Check your email :)
34 | ) : (
35 | <>
36 |
37 | Enter the email associated with your account and we will send you a
38 | reset link
39 |
40 |
77 | >
78 | )}
79 |
86 |
88 | setAuthModalState((prev) => ({
89 | ...prev,
90 | view: "login",
91 | }))
92 | }
93 | >
94 | LOGIN
95 |
96 |
97 |
99 | setAuthModalState((prev) => ({
100 | ...prev,
101 | view: "signup",
102 | }))
103 | }
104 | >
105 | SIGN UP
106 |
107 |
108 |
109 | );
110 | };
111 | export default ResetPassword;
112 |
--------------------------------------------------------------------------------
/src/pages/r/[community]/comments/[pid].tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from "react";
2 | import { doc, getDoc } from "firebase/firestore";
3 | import { useRouter } from "next/router";
4 | import { useAuthState } from "react-firebase-hooks/auth";
5 | import { Post } from "../../../../atoms/postsAtom";
6 | import About from "../../../../components/Community/About";
7 | import PageContentLayout from "../../../../components/Layout/PageContent";
8 | import Comments from "../../../../components/Post/Comments";
9 | import PostLoader from "../../../../components/Post/Loader";
10 | import PostItem from "../../../../components/Post/PostItem";
11 | import { auth, firestore } from "../../../../firebase/clientApp";
12 | import useCommunityData from "../../../../hooks/useCommunityData";
13 | import usePosts from "../../../../hooks/usePosts";
14 |
15 | type PostPageProps = {};
16 |
17 | const PostPage: React.FC = () => {
18 | const [user] = useAuthState(auth);
19 | const router = useRouter();
20 | const { community, pid } = router.query;
21 | const { communityStateValue } = useCommunityData();
22 |
23 | // Need to pass community data here to see if current post [pid] has been voted on
24 | const {
25 | postStateValue,
26 | setPostStateValue,
27 | onDeletePost,
28 | loading,
29 | setLoading,
30 | onVote,
31 | } = usePosts(communityStateValue.currentCommunity);
32 |
33 | const fetchPost = async () => {
34 | console.log("FETCHING POST");
35 |
36 | setLoading(true);
37 | try {
38 | const postDocRef = doc(firestore, "posts", pid as string);
39 | const postDoc = await getDoc(postDocRef);
40 | setPostStateValue((prev) => ({
41 | ...prev,
42 | selectedPost: { id: postDoc.id, ...postDoc.data() } as Post,
43 | }));
44 | // setPostStateValue((prev) => ({
45 | // ...prev,
46 | // selectedPost: {} as Post,
47 | // }));
48 | } catch (error: any) {
49 | console.log("fetchPost error", error.message);
50 | }
51 | setLoading(false);
52 | };
53 |
54 | // Fetch post if not in already in state
55 | useEffect(() => {
56 | const { pid } = router.query;
57 |
58 | if (pid && !postStateValue.selectedPost) {
59 | fetchPost();
60 | }
61 | }, [router.query, postStateValue.selectedPost]);
62 |
63 | return (
64 |
65 | {/* Left Content */}
66 | <>
67 | {loading ? (
68 |
69 | ) : (
70 | <>
71 | {postStateValue.selectedPost && (
72 | <>
73 | item.postId === postStateValue.selectedPost!.id
81 | )?.voteValue
82 | }
83 | userIsCreator={
84 | user?.uid === postStateValue.selectedPost.creatorId
85 | }
86 | router={router}
87 | />
88 |
93 | >
94 | )}
95 | >
96 | )}
97 | >
98 | {/* Right Content */}
99 | <>
100 |
107 | >
108 |
109 | );
110 | };
111 | export default PostPage;
112 |
--------------------------------------------------------------------------------
/src/components/Post/PostForm/NewPostForm.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useRef, useState } from "react";
2 | import {
3 | Box,
4 | Button,
5 | Flex,
6 | Icon,
7 | Input,
8 | Stack,
9 | Textarea,
10 | Image,
11 | } from "@chakra-ui/react";
12 | import { User } from "firebase/auth";
13 | import {
14 | addDoc,
15 | collection,
16 | doc,
17 | serverTimestamp,
18 | updateDoc,
19 | } from "firebase/firestore";
20 | import { useRouter } from "next/router";
21 | import { BiPoll } from "react-icons/bi";
22 | import { BsLink45Deg, BsMic } from "react-icons/bs";
23 | import { IoDocumentText, IoImageOutline } from "react-icons/io5";
24 | import { AiFillCloseCircle } from "react-icons/ai";
25 | import { useRecoilState, useSetRecoilState } from "recoil";
26 | import { firestore, storage } from "../../../firebase/clientApp";
27 | import TabItem from "./TabItem";
28 | import { postState } from "../../../atoms/postsAtom";
29 | import { getDownloadURL, ref, uploadString } from "firebase/storage";
30 | import TextInputs from "./TextInputs";
31 | import ImageUpload from "./ImageUpload";
32 |
33 | const formTabs = [
34 | {
35 | title: "Post",
36 | icon: IoDocumentText,
37 | },
38 | {
39 | title: "Images & Video",
40 | icon: IoImageOutline,
41 | },
42 | {
43 | title: "Link",
44 | icon: BsLink45Deg,
45 | },
46 | {
47 | title: "Poll",
48 | icon: BiPoll,
49 | },
50 | {
51 | title: "Talk",
52 | icon: BsMic,
53 | },
54 | ];
55 |
56 | export type TabItem = {
57 | title: string;
58 | icon: typeof Icon.arguments;
59 | };
60 |
61 | type NewPostFormProps = {
62 | communityId: string;
63 | communityImageURL?: string;
64 | user: User;
65 | };
66 |
67 | const NewPostForm: React.FC = ({
68 | communityId,
69 | communityImageURL,
70 | user,
71 | }) => {
72 | const [selectedTab, setSelectedTab] = useState(formTabs[0].title);
73 | const [textInputs, setTextInputs] = useState({
74 | title: "",
75 | body: "",
76 | });
77 | const [selectedFile, setSelectedFile] = useState();
78 | const selectFileRef = useRef(null);
79 | const [loading, setLoading] = useState(false);
80 | const [error, setError] = useState("");
81 | const router = useRouter();
82 | const setPostItems = useSetRecoilState(postState);
83 |
84 | const handleCreatePost = async () => {
85 | setLoading(true);
86 | const { title, body } = textInputs;
87 | try {
88 | const postDocRef = await addDoc(collection(firestore, "posts"), {
89 | communityId,
90 | communityImageURL: communityImageURL || "",
91 | creatorId: user.uid,
92 | userDisplayText: user.email!.split("@")[0],
93 | title,
94 | body,
95 | numberOfComments: 0,
96 | voteStatus: 0,
97 | createdAt: serverTimestamp(),
98 | editedAt: serverTimestamp(),
99 | });
100 |
101 | console.log("HERE IS NEW POST ID", postDocRef.id);
102 |
103 | // // check if selectedFile exists, if it does, do image processing
104 | if (selectedFile) {
105 | const imageRef = ref(storage, `posts/${postDocRef.id}/image`);
106 | await uploadString(imageRef, selectedFile, "data_url");
107 | const downloadURL = await getDownloadURL(imageRef);
108 | await updateDoc(postDocRef, {
109 | imageURL: downloadURL,
110 | });
111 | console.log("HERE IS DOWNLOAD URL", downloadURL);
112 | }
113 |
114 | // Clear the cache to cause a refetch of the posts
115 | setPostItems((prev) => ({
116 | ...prev,
117 | postUpdateRequired: true,
118 | }));
119 | router.back();
120 | } catch (error) {
121 | console.log("createPost error", error);
122 | setError("Error creating post");
123 | }
124 | setLoading(false);
125 | };
126 |
127 | const onSelectImage = (event: React.ChangeEvent) => {
128 | const reader = new FileReader();
129 | if (event.target.files?.[0]) {
130 | reader.readAsDataURL(event.target.files[0]);
131 | }
132 |
133 | reader.onload = (readerEvent) => {
134 | if (readerEvent.target?.result) {
135 | setSelectedFile(readerEvent.target?.result as string);
136 | }
137 | };
138 | };
139 |
140 | const onTextChange = ({
141 | target: { name, value },
142 | }: React.ChangeEvent) => {
143 | setTextInputs((prev) => ({
144 | ...prev,
145 | [name]: value,
146 | }));
147 | };
148 |
149 | return (
150 |
151 |
152 | {formTabs.map((item, index) => (
153 |
159 | ))}
160 |
161 |
162 | {selectedTab === "Post" && (
163 |
169 | )}
170 | {selectedTab === "Images & Video" && (
171 |
178 | )}
179 |
180 |
181 | );
182 | };
183 | export default NewPostForm;
184 |
--------------------------------------------------------------------------------
/src/components/Community/Recommendations.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Box,
3 | Button,
4 | Flex,
5 | Icon,
6 | Image,
7 | Skeleton,
8 | SkeletonCircle,
9 | Stack,
10 | Text,
11 | } from "@chakra-ui/react";
12 | import { collection, getDocs, limit, orderBy, query } from "firebase/firestore";
13 | import Link from "next/link";
14 | import React, { useEffect, useState } from "react";
15 | import { FaReddit } from "react-icons/fa";
16 | import { Community } from "../../atoms/communitiesAtom";
17 | import { firestore } from "../../firebase/clientApp";
18 | import useCommunityData from "../../hooks/useCommunityData";
19 |
20 | type RecommendationsProps = {};
21 |
22 | const Recommendations: React.FC = () => {
23 | const [communities, setCommunities] = useState([]);
24 | const [loading, setLoading] = useState(false);
25 | const { communityStateValue, onJoinLeaveCommunity } = useCommunityData();
26 |
27 | const getCommunityRecommendations = async () => {
28 | setLoading(true);
29 | try {
30 | const communityQuery = query(
31 | collection(firestore, "communities"),
32 | orderBy("numberOfMembers", "desc"),
33 | limit(5)
34 | );
35 | const communityDocs = await getDocs(communityQuery);
36 | const communities = communityDocs.docs.map((doc) => ({
37 | id: doc.id,
38 | ...doc.data(),
39 | })) as Community[];
40 | console.log("HERE ARE COMS", communities);
41 |
42 | setCommunities(communities);
43 | } catch (error: any) {
44 | console.log("getCommunityRecommendations error", error.message);
45 | }
46 | setLoading(false);
47 | };
48 |
49 | useEffect(() => {
50 | getCommunityRecommendations();
51 | }, []);
52 |
53 | return (
54 |
62 |
75 | Top Communities
76 |
77 |
78 | {loading ? (
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 | ) : (
94 | <>
95 | {communities.map((item, index) => {
96 | const isJoined = !!communityStateValue.mySnippets.find(
97 | (snippet) => snippet.communityId === item.id
98 | );
99 | return (
100 |
101 |
110 |
111 |
112 | {index + 1}
113 |
114 |
115 | {item.imageURL ? (
116 |
122 | ) : (
123 |
129 | )}
130 | {`r/${item.id}`}
137 |
138 |
139 |
140 |
151 |
152 |
153 |
154 | );
155 | })}
156 |
157 |
160 |
161 | >
162 | )}
163 |
164 |
165 | );
166 | };
167 | export default Recommendations;
168 |
--------------------------------------------------------------------------------
/src/hooks/useCommunityData.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from "react";
2 | import { doc, getDoc, increment, writeBatch } from "firebase/firestore";
3 | import { useRouter } from "next/router";
4 | import { useAuthState } from "react-firebase-hooks/auth";
5 | import { useRecoilState, useSetRecoilState } from "recoil";
6 | import { authModalState } from "../atoms/authModalAtom";
7 | import {
8 | Community,
9 | CommunitySnippet,
10 | communityState,
11 | defaultCommunity,
12 | } from "../atoms/communitiesAtom";
13 | import { auth, firestore } from "../firebase/clientApp";
14 | import { getMySnippets } from "../helpers/firestore";
15 |
16 | // Add ssrCommunityData near end as small optimization
17 | const useCommunityData = (ssrCommunityData?: boolean) => {
18 | const [user] = useAuthState(auth);
19 | const router = useRouter();
20 | const [communityStateValue, setCommunityStateValue] =
21 | useRecoilState(communityState);
22 | const setAuthModalState = useSetRecoilState(authModalState);
23 | const [loading, setLoading] = useState(false);
24 | const [error, setError] = useState("");
25 |
26 | useEffect(() => {
27 | if (!user || !!communityStateValue.mySnippets.length) return;
28 |
29 | getSnippets();
30 | }, [user]);
31 |
32 | const getSnippets = async () => {
33 | setLoading(true);
34 | try {
35 | const snippets = await getMySnippets(user?.uid!);
36 | setCommunityStateValue((prev) => ({
37 | ...prev,
38 | mySnippets: snippets as CommunitySnippet[],
39 | initSnippetsFetched: true,
40 | }));
41 | setLoading(false);
42 | } catch (error: any) {
43 | console.log("Error getting user snippets", error);
44 | setError(error.message);
45 | }
46 | setLoading(false);
47 | };
48 |
49 | const getCommunityData = async (communityId: string) => {
50 | // this causes weird memory leak error - not sure why
51 | // setLoading(true);
52 | console.log("GETTING COMMUNITY DATA");
53 |
54 | try {
55 | const communityDocRef = doc(
56 | firestore,
57 | "communities",
58 | communityId as string
59 | );
60 | const communityDoc = await getDoc(communityDocRef);
61 | // setCommunityStateValue((prev) => ({
62 | // ...prev,
63 | // visitedCommunities: {
64 | // ...prev.visitedCommunities,
65 | // [communityId as string]: {
66 | // id: communityDoc.id,
67 | // ...communityDoc.data(),
68 | // } as Community,
69 | // },
70 | // }));
71 | setCommunityStateValue((prev) => ({
72 | ...prev,
73 | currentCommunity: {
74 | id: communityDoc.id,
75 | ...communityDoc.data(),
76 | } as Community,
77 | }));
78 | } catch (error: any) {
79 | console.log("getCommunityData error", error.message);
80 | }
81 | setLoading(false);
82 | };
83 |
84 | const onJoinLeaveCommunity = (community: Community, isJoined?: boolean) => {
85 | console.log("ON JOIN LEAVE", community.id);
86 |
87 | if (!user) {
88 | setAuthModalState({ open: true, view: "login" });
89 | return;
90 | }
91 |
92 | setLoading(true);
93 | if (isJoined) {
94 | leaveCommunity(community.id);
95 | return;
96 | }
97 | joinCommunity(community);
98 | };
99 |
100 | const joinCommunity = async (community: Community) => {
101 | console.log("JOINING COMMUNITY: ", community.id);
102 | try {
103 | const batch = writeBatch(firestore);
104 |
105 | const newSnippet: CommunitySnippet = {
106 | communityId: community.id,
107 | imageURL: community.imageURL || "",
108 | };
109 | batch.set(
110 | doc(
111 | firestore,
112 | `users/${user?.uid}/communitySnippets`,
113 | community.id // will for sure have this value at this point
114 | ),
115 | newSnippet
116 | );
117 |
118 | batch.update(doc(firestore, "communities", community.id), {
119 | numberOfMembers: increment(1),
120 | });
121 |
122 | // perform batch writes
123 | await batch.commit();
124 |
125 | // Add current community to snippet
126 | setCommunityStateValue((prev) => ({
127 | ...prev,
128 | mySnippets: [...prev.mySnippets, newSnippet],
129 | }));
130 | } catch (error) {
131 | console.log("joinCommunity error", error);
132 | }
133 | setLoading(false);
134 | };
135 |
136 | const leaveCommunity = async (communityId: string) => {
137 | try {
138 | const batch = writeBatch(firestore);
139 |
140 | batch.delete(
141 | doc(firestore, `users/${user?.uid}/communitySnippets/${communityId}`)
142 | );
143 |
144 | batch.update(doc(firestore, "communities", communityId), {
145 | numberOfMembers: increment(-1),
146 | });
147 |
148 | await batch.commit();
149 |
150 | setCommunityStateValue((prev) => ({
151 | ...prev,
152 | mySnippets: prev.mySnippets.filter(
153 | (item) => item.communityId !== communityId
154 | ),
155 | }));
156 | } catch (error) {
157 | console.log("leaveCommunity error", error);
158 | }
159 | setLoading(false);
160 | };
161 |
162 | // useEffect(() => {
163 | // if (ssrCommunityData) return;
164 | // const { community } = router.query;
165 | // if (community) {
166 | // const communityData =
167 | // communityStateValue.visitedCommunities[community as string];
168 | // if (!communityData) {
169 | // getCommunityData(community as string);
170 | // return;
171 | // }
172 | // }
173 | // }, [router.query]);
174 |
175 | useEffect(() => {
176 | // if (ssrCommunityData) return;
177 | const { community } = router.query;
178 | if (community) {
179 | const communityData = communityStateValue.currentCommunity;
180 |
181 | if (!communityData.id) {
182 | getCommunityData(community as string);
183 | return;
184 | }
185 | // console.log("this is happening", communityStateValue);
186 | } else {
187 | /**
188 | * JUST ADDED THIS APRIL 24
189 | * FOR NEW LOGIC OF NOT USING visitedCommunities
190 | */
191 | setCommunityStateValue((prev) => ({
192 | ...prev,
193 | currentCommunity: defaultCommunity,
194 | }));
195 | }
196 | }, [router.query, communityStateValue.currentCommunity]);
197 |
198 | // console.log("LOL", communityStateValue);
199 |
200 | return {
201 | communityStateValue,
202 | onJoinLeaveCommunity,
203 | loading,
204 | setLoading,
205 | error,
206 | };
207 | };
208 |
209 | export default useCommunityData;
210 |
--------------------------------------------------------------------------------
/src/components/Post/Comments/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useCallback, useEffect, useState } from "react";
2 | import {
3 | Box,
4 | Flex,
5 | SkeletonCircle,
6 | SkeletonText,
7 | Stack,
8 | Text,
9 | } from "@chakra-ui/react";
10 | import { User } from "firebase/auth";
11 | import {
12 | collection,
13 | doc,
14 | getDocs,
15 | increment,
16 | orderBy,
17 | query,
18 | serverTimestamp,
19 | where,
20 | writeBatch,
21 | } from "firebase/firestore";
22 | import { useSetRecoilState } from "recoil";
23 | import { authModalState } from "../../../atoms/authModalAtom";
24 | import { Post, postState } from "../../../atoms/postsAtom";
25 | import { firestore } from "../../../firebase/clientApp";
26 | import CommentItem, { Comment } from "./CommentItem";
27 | import CommentInput from "./Input";
28 |
29 | type CommentsProps = {
30 | user?: User | null;
31 | selectedPost: Post;
32 | community: string;
33 | };
34 |
35 | const Comments: React.FC = ({
36 | user,
37 | selectedPost,
38 | community,
39 | }) => {
40 | const [comment, setComment] = useState("");
41 | const [comments, setComments] = useState([]);
42 | const [commentFetchLoading, setCommentFetchLoading] = useState(false);
43 | const [commentCreateLoading, setCommentCreateLoading] = useState(false);
44 | const [deleteLoading, setDeleteLoading] = useState("");
45 | const setAuthModalState = useSetRecoilState(authModalState);
46 | const setPostState = useSetRecoilState(postState);
47 |
48 | const onCreateComment = async (comment: string) => {
49 | if (!user) {
50 | setAuthModalState({ open: true, view: "login" });
51 | return;
52 | }
53 |
54 | setCommentCreateLoading(true);
55 | try {
56 | const batch = writeBatch(firestore);
57 |
58 | // Create comment document
59 | const commentDocRef = doc(collection(firestore, "comments"));
60 | batch.set(commentDocRef, {
61 | postId: selectedPost.id,
62 | creatorId: user.uid,
63 | creatorDisplayText: user.email!.split("@")[0],
64 | creatorPhotoURL: user.photoURL,
65 | communityId: community,
66 | text: comment,
67 | postTitle: selectedPost.title,
68 | createdAt: serverTimestamp(),
69 | } as Comment);
70 |
71 | // Update post numberOfComments
72 | batch.update(doc(firestore, "posts", selectedPost.id), {
73 | numberOfComments: increment(1),
74 | });
75 | await batch.commit();
76 |
77 | setComment("");
78 | const { id: postId, title } = selectedPost;
79 | setComments((prev) => [
80 | {
81 | id: commentDocRef.id,
82 | creatorId: user.uid,
83 | creatorDisplayText: user.email!.split("@")[0],
84 | creatorPhotoURL: user.photoURL,
85 | communityId: community,
86 | postId,
87 | postTitle: title,
88 | text: comment,
89 | createdAt: {
90 | seconds: Date.now() / 1000,
91 | },
92 | } as Comment,
93 | ...prev,
94 | ]);
95 |
96 | // Fetch posts again to update number of comments
97 | setPostState((prev) => ({
98 | ...prev,
99 | selectedPost: {
100 | ...prev.selectedPost,
101 | numberOfComments: prev.selectedPost?.numberOfComments! + 1,
102 | } as Post,
103 | postUpdateRequired: true,
104 | }));
105 | } catch (error: any) {
106 | console.log("onCreateComment error", error.message);
107 | }
108 | setCommentCreateLoading(false);
109 | };
110 |
111 | const onDeleteComment = useCallback(
112 | async (comment: Comment) => {
113 | setDeleteLoading(comment.id as string);
114 | try {
115 | if (!comment.id) throw "Comment has no ID";
116 | const batch = writeBatch(firestore);
117 | const commentDocRef = doc(firestore, "comments", comment.id);
118 | batch.delete(commentDocRef);
119 |
120 | batch.update(doc(firestore, "posts", comment.postId), {
121 | numberOfComments: increment(-1),
122 | });
123 |
124 | await batch.commit();
125 |
126 | setPostState((prev) => ({
127 | ...prev,
128 | selectedPost: {
129 | ...prev.selectedPost,
130 | numberOfComments: prev.selectedPost?.numberOfComments! - 1,
131 | } as Post,
132 | postUpdateRequired: true,
133 | }));
134 |
135 | setComments((prev) => prev.filter((item) => item.id !== comment.id));
136 | // return true;
137 | } catch (error: any) {
138 | console.log("Error deletig comment", error.message);
139 | // return false;
140 | }
141 | setDeleteLoading("");
142 | },
143 | [setComments, setPostState]
144 | );
145 |
146 | const getPostComments = async () => {
147 | try {
148 | const commentsQuery = query(
149 | collection(firestore, "comments"),
150 | where("postId", "==", selectedPost.id),
151 | orderBy("createdAt", "desc")
152 | );
153 | const commentDocs = await getDocs(commentsQuery);
154 | const comments = commentDocs.docs.map((doc) => ({
155 | id: doc.id,
156 | ...doc.data(),
157 | }));
158 | setComments(comments as Comment[]);
159 | } catch (error: any) {
160 | console.log("getPostComments error", error.message);
161 | }
162 | setCommentFetchLoading(false);
163 | };
164 |
165 | useEffect(() => {
166 | console.log("HERE IS SELECTED POST", selectedPost.id);
167 |
168 | getPostComments();
169 | }, []);
170 |
171 | return (
172 |
173 |
181 |
188 |
189 |
190 | {commentFetchLoading ? (
191 | <>
192 | {[0, 1, 2].map((item) => (
193 |
194 |
195 |
196 |
197 | ))}
198 | >
199 | ) : (
200 | <>
201 | {!!comments.length ? (
202 | <>
203 | {comments.map((item: Comment) => (
204 |
211 | ))}
212 | >
213 | ) : (
214 |
222 |
223 | No Comments Yet
224 |
225 |
226 | )}
227 | >
228 | )}
229 |
230 |
231 | );
232 | };
233 | export default Comments;
234 |
--------------------------------------------------------------------------------
/src/pages/index.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect } from "react";
2 | import { Stack } from "@chakra-ui/react";
3 | import {
4 | collection,
5 | DocumentData,
6 | getDocs,
7 | limit,
8 | onSnapshot,
9 | orderBy,
10 | query,
11 | QuerySnapshot,
12 | where,
13 | } from "firebase/firestore";
14 | import type { NextPage } from "next";
15 | import { useRouter } from "next/router";
16 | import { useAuthState } from "react-firebase-hooks/auth";
17 | import { useRecoilValue } from "recoil";
18 | import { communityState } from "../atoms/communitiesAtom";
19 | import { Post, PostVote } from "../atoms/postsAtom";
20 | import CreatePostLink from "../components/Community/CreatePostLink";
21 | import Recommendations from "../components/Community/Recommendations";
22 | import PageContentLayout from "../components/Layout/PageContent";
23 | import PostLoader from "../components/Post/Loader";
24 | import PostItem from "../components/Post/PostItem";
25 | import { auth, firestore } from "../firebase/clientApp";
26 | import usePosts from "../hooks/usePosts";
27 | import Premium from "../components/Community/Premium";
28 | import PersonalHome from "../components/Community/PersonalHome";
29 |
30 | const Home: NextPage = () => {
31 | const [user, loadingUser] = useAuthState(auth);
32 | const {
33 | postStateValue,
34 | setPostStateValue,
35 | onVote,
36 | onSelectPost,
37 | onDeletePost,
38 | loading,
39 | setLoading,
40 | } = usePosts();
41 | const communityStateValue = useRecoilValue(communityState);
42 |
43 | const getUserHomePosts = async () => {
44 | console.log("GETTING USER FEED");
45 | setLoading(true);
46 | try {
47 | /**
48 | * if snippets has no length (i.e. user not in any communities yet)
49 | * do query for 20 posts ordered by voteStatus
50 | */
51 | const feedPosts: Post[] = [];
52 |
53 | // User has joined communities
54 | if (communityStateValue.mySnippets.length) {
55 | console.log("GETTING POSTS IN USER COMMUNITIES");
56 |
57 | const myCommunityIds = communityStateValue.mySnippets.map(
58 | (snippet) => snippet.communityId
59 | );
60 | // Getting 2 posts from 3 communities that user has joined
61 | let postPromises: Array>> = [];
62 | [0, 1, 2].forEach((index) => {
63 | if (!myCommunityIds[index]) return;
64 |
65 | postPromises.push(
66 | getDocs(
67 | query(
68 | collection(firestore, "posts"),
69 | where("communityId", "==", myCommunityIds[index]),
70 | limit(3)
71 | )
72 | )
73 | );
74 | });
75 | const queryResults = await Promise.all(postPromises);
76 | /**
77 | * queryResults is an array of length 3, each with 0-2 posts from
78 | * 3 communities that the user has joined
79 | */
80 | queryResults.forEach((result) => {
81 | const posts = result.docs.map((doc) => ({
82 | id: doc.id,
83 | ...doc.data(),
84 | })) as Post[];
85 | feedPosts.push(...posts);
86 | });
87 | }
88 | // User has not joined any communities yet
89 | else {
90 | console.log("USER HAS NO COMMUNITIES - GETTING GENERAL POSTS");
91 |
92 | const postQuery = query(
93 | collection(firestore, "posts"),
94 | orderBy("voteStatus", "desc"),
95 | limit(10)
96 | );
97 | const postDocs = await getDocs(postQuery);
98 | const posts = postDocs.docs.map((doc) => ({
99 | id: doc.id,
100 | ...doc.data(),
101 | })) as Post[];
102 | feedPosts.push(...posts);
103 | }
104 |
105 | console.log("HERE ARE FEED POSTS", feedPosts);
106 |
107 | setPostStateValue((prev) => ({
108 | ...prev,
109 | posts: feedPosts,
110 | }));
111 |
112 | // if not in any, get 5 communities ordered by number of members
113 | // for each one, get 2 posts ordered by voteStatus and set these to postState posts
114 | } catch (error: any) {
115 | console.log("getUserHomePosts error", error.message);
116 | }
117 | setLoading(false);
118 | };
119 |
120 | const getNoUserHomePosts = async () => {
121 | console.log("GETTING NO USER FEED");
122 | setLoading(true);
123 | try {
124 | const postQuery = query(
125 | collection(firestore, "posts"),
126 | orderBy("voteStatus", "desc"),
127 | limit(10)
128 | );
129 | const postDocs = await getDocs(postQuery);
130 | const posts = postDocs.docs.map((doc) => ({
131 | id: doc.id,
132 | ...doc.data(),
133 | }));
134 | console.log("NO USER FEED", posts);
135 |
136 | setPostStateValue((prev) => ({
137 | ...prev,
138 | posts: posts as Post[],
139 | }));
140 | } catch (error: any) {
141 | console.log("getNoUserHomePosts error", error.message);
142 | }
143 | setLoading(false);
144 | };
145 |
146 | const getUserPostVotes = async () => {
147 | const postIds = postStateValue.posts.map((post) => post.id);
148 | const postVotesQuery = query(
149 | collection(firestore, `users/${user?.uid}/postVotes`),
150 | where("postId", "in", postIds)
151 | );
152 | const unsubscribe = onSnapshot(postVotesQuery, (querySnapshot) => {
153 | const postVotes = querySnapshot.docs.map((postVote) => ({
154 | id: postVote.id,
155 | ...postVote.data(),
156 | }));
157 |
158 | setPostStateValue((prev) => ({
159 | ...prev,
160 | postVotes: postVotes as PostVote[],
161 | }));
162 | });
163 |
164 | return () => unsubscribe();
165 | };
166 |
167 | useEffect(() => {
168 | /**
169 | * initSnippetsFetched ensures that user snippets have been retrieved;
170 | * the value is set to true when snippets are first retrieved inside
171 | * of getSnippets in useCommunityData
172 | */
173 | if (!communityStateValue.initSnippetsFetched) return;
174 |
175 | if (user) {
176 | getUserHomePosts();
177 | }
178 | }, [user, communityStateValue.initSnippetsFetched]);
179 |
180 | useEffect(() => {
181 | if (!user && !loadingUser) {
182 | getNoUserHomePosts();
183 | }
184 | }, [user, loadingUser]);
185 |
186 | useEffect(() => {
187 | if (!user?.uid || !postStateValue.posts.length) return;
188 | getUserPostVotes();
189 |
190 | // Clear postVotes on dismount
191 | return () => {
192 | setPostStateValue((prev) => ({
193 | ...prev,
194 | postVotes: [],
195 | }));
196 | };
197 | }, [postStateValue.posts, user?.uid]);
198 |
199 | return (
200 |
201 | <>
202 |
203 | {loading ? (
204 |
205 | ) : (
206 |
207 | {postStateValue.posts.map((post: Post, index) => (
208 | item.postId === post.id
217 | )?.voteValue
218 | }
219 | userIsCreator={user?.uid === post.creatorId}
220 | onSelectPost={onSelectPost}
221 | homePage
222 | />
223 | ))}
224 |
225 | )}
226 | >
227 |
228 |
229 |
230 |
231 |
232 |
233 | );
234 | };
235 |
236 | export default Home;
237 |
--------------------------------------------------------------------------------
/src/components/Modal/CreateCommunity/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import {
3 | Box,
4 | Button,
5 | Checkbox,
6 | Divider,
7 | Flex,
8 | Icon,
9 | Input,
10 | ModalBody,
11 | ModalCloseButton,
12 | ModalFooter,
13 | ModalHeader,
14 | Stack,
15 | Text,
16 | } from "@chakra-ui/react";
17 | import { doc, runTransaction, serverTimestamp } from "firebase/firestore";
18 | import { useRouter } from "next/router";
19 | import { BsFillEyeFill, BsFillPersonFill } from "react-icons/bs";
20 | import { HiLockClosed } from "react-icons/hi";
21 | import { useSetRecoilState } from "recoil";
22 | import { communityState } from "../../../atoms/communitiesAtom";
23 | import { firestore } from "../../../firebase/clientApp";
24 | import ModalWrapper from "../ModalWrapper";
25 |
26 | type CreateCommunityModalProps = {
27 | isOpen: boolean;
28 | handleClose: () => void;
29 | userId: string;
30 | };
31 |
32 | const CreateCommunityModal: React.FC = ({
33 | isOpen,
34 | handleClose,
35 | userId,
36 | }) => {
37 | const setSnippetState = useSetRecoilState(communityState);
38 | const [name, setName] = useState("");
39 | const [charsRemaining, setCharsRemaining] = useState(21);
40 | const [nameError, setNameError] = useState("");
41 | const [communityType, setCommunityType] = useState("public");
42 | const [loading, setLoading] = useState(false);
43 | const router = useRouter();
44 |
45 | const handleChange = (event: React.ChangeEvent) => {
46 | if (event.target.value.length > 21) return;
47 | setName(event.target.value);
48 | setCharsRemaining(21 - event.target.value.length);
49 | };
50 |
51 | const handleCreateCommunity = async () => {
52 | if (nameError) setNameError("");
53 | const format = /[ `!@#$%^&*()+\-=\[\]{};':"\\|,.<>\/?~]/;
54 |
55 | if (format.test(name) || name.length < 3) {
56 | return setNameError(
57 | "Community names must be between 3–21 characters, and can only contain letters, numbers, or underscores."
58 | );
59 | }
60 |
61 | setLoading(true);
62 | try {
63 | // Create community document and communitySnippet subcollection document on user
64 | const communityDocRef = doc(firestore, "communities", name);
65 | await runTransaction(firestore, async (transaction) => {
66 | const communityDoc = await transaction.get(communityDocRef);
67 | if (communityDoc.exists()) {
68 | throw new Error(`Sorry, /r${name} is taken. Try another.`);
69 | }
70 |
71 | transaction.set(communityDocRef, {
72 | creatorId: userId,
73 | createdAt: serverTimestamp(),
74 | numberOfMembers: 1,
75 | privacyType: "public",
76 | });
77 |
78 | transaction.set(
79 | doc(firestore, `users/${userId}/communitySnippets`, name),
80 | {
81 | communityId: name,
82 | isModerator: true,
83 | }
84 | );
85 | });
86 | } catch (error: any) {
87 | console.log("Transaction error", error);
88 | setNameError(error.message);
89 | }
90 | setSnippetState((prev) => ({
91 | ...prev,
92 | mySnippets: [],
93 | }));
94 | handleClose();
95 | router.push(`r/${name}`);
96 | setLoading(false);
97 | };
98 |
99 | const onCommunityTypeChange = (
100 | event: React.ChangeEvent
101 | ) => {
102 | const {
103 | target: { name },
104 | } = event;
105 | if (name === communityType) return;
106 | setCommunityType(name);
107 | };
108 |
109 | return (
110 |
111 |
117 | Create a community
118 |
119 |
120 |
121 |
122 |
123 |
124 | Name
125 |
126 |
127 | Community names including capitalization cannot be changed
128 |
129 |
136 | r/
137 |
138 |
147 |
152 | {charsRemaining} Characters remaining
153 |
154 |
155 | {nameError}
156 |
157 |
158 |
159 | Community Type
160 |
161 |
162 |
168 |
169 |
170 |
171 | Public
172 |
173 |
174 | Anyone can view, post, and comment to this community
175 |
176 |
177 |
178 |
184 |
185 |
186 |
187 | Restricted
188 |
189 |
190 | Anyone can view this community, but only approved users can
191 | post
192 |
193 |
194 |
195 |
201 |
202 |
203 |
204 | Private
205 |
206 |
207 | Only approved users can view and submit to this community
208 |
209 |
210 |
211 |
212 |
213 |
214 |
215 |
216 |
219 |
227 |
228 |
229 | );
230 | };
231 | export default CreateCommunityModal;
232 |
--------------------------------------------------------------------------------
/src/components/Post/PostItem/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import {
3 | Flex,
4 | Icon,
5 | Image,
6 | Skeleton,
7 | Spinner,
8 | Stack,
9 | Text,
10 | } from "@chakra-ui/react";
11 | import moment from "moment";
12 | import { NextRouter } from "next/router";
13 | import { AiOutlineDelete } from "react-icons/ai";
14 | import { BsChat, BsDot } from "react-icons/bs";
15 | import { FaReddit } from "react-icons/fa";
16 | import {
17 | IoArrowDownCircleOutline,
18 | IoArrowDownCircleSharp,
19 | IoArrowRedoOutline,
20 | IoArrowUpCircleOutline,
21 | IoArrowUpCircleSharp,
22 | IoBookmarkOutline,
23 | } from "react-icons/io5";
24 | import { Post } from "../../../atoms/postsAtom";
25 | import Link from "next/link";
26 |
27 | export type PostItemContentProps = {
28 | post: Post;
29 | onVote: (
30 | event: React.MouseEvent,
31 | post: Post,
32 | vote: number,
33 | communityId: string,
34 | postIdx?: number
35 | ) => void;
36 | onDeletePost: (post: Post) => Promise;
37 | userIsCreator: boolean;
38 | onSelectPost?: (value: Post, postIdx: number) => void;
39 | router?: NextRouter;
40 | postIdx?: number;
41 | userVoteValue?: number;
42 | homePage?: boolean;
43 | };
44 |
45 | const PostItem: React.FC = ({
46 | post,
47 | postIdx,
48 | onVote,
49 | onSelectPost,
50 | router,
51 | onDeletePost,
52 | userVoteValue,
53 | userIsCreator,
54 | homePage,
55 | }) => {
56 | const [loadingImage, setLoadingImage] = useState(true);
57 | const [loadingDelete, setLoadingDelete] = useState(false);
58 | const singlePostView = !onSelectPost; // function not passed to [pid]
59 |
60 | const handleDelete = async (
61 | event: React.MouseEvent
62 | ) => {
63 | event.stopPropagation();
64 | setLoadingDelete(true);
65 | try {
66 | const success = await onDeletePost(post);
67 | if (!success) throw new Error("Failed to delete post");
68 |
69 | console.log("Post successfully deleted");
70 |
71 | // Could proably move this logic to onDeletePost function
72 | if (router) router.back();
73 | } catch (error: any) {
74 | console.log("Error deleting post", error.message);
75 | /**
76 | * Don't need to setLoading false if no error
77 | * as item will be removed from DOM
78 | */
79 | setLoadingDelete(false);
80 | // setError
81 | }
82 | };
83 |
84 | return (
85 | onSelectPost && post && onSelectPost(post, postIdx!)}
93 | >
94 |
102 | onVote(event, post, 1, post.communityId)}
110 | />
111 |
112 | {post.voteStatus}
113 |
114 | onVote(event, post, -1, post.communityId)}
124 | />
125 |
126 |
127 |
128 | {post.createdAt && (
129 |
130 | {homePage && (
131 | <>
132 | {post.communityImageURL ? (
133 |
139 | ) : (
140 |
141 | )}
142 |
143 | event.stopPropagation()}
147 | >{`r/${post.communityId}`}
148 |
149 |
150 | >
151 | )}
152 |
153 | Posted by u/{post.userDisplayText}{" "}
154 | {moment(new Date(post.createdAt.seconds * 1000)).fromNow()}
155 |
156 |
157 | )}
158 |
159 | {post.title}
160 |
161 | {post.body}
162 | {post.imageURL && (
163 |
164 | {loadingImage && (
165 |
166 | )}
167 | setLoadingImage(false)}
174 | alt="Post Image"
175 | />
176 |
177 | )}
178 |
179 |
180 |
187 |
188 | {post.numberOfComments}
189 |
190 |
197 |
198 | Share
199 |
200 |
207 |
208 | Save
209 |
210 | {userIsCreator && (
211 |
219 | {loadingDelete ? (
220 |
221 | ) : (
222 | <>
223 |
224 | Delete
225 | >
226 | )}
227 |
228 | )}
229 |
230 |
231 |
232 | );
233 | };
234 |
235 | export default PostItem;
236 |
--------------------------------------------------------------------------------
/src/components/Community/About.tsx:
--------------------------------------------------------------------------------
1 | import React, { useRef, useState } from "react";
2 | import {
3 | Box,
4 | Button,
5 | Divider,
6 | Flex,
7 | Icon,
8 | Skeleton,
9 | SkeletonCircle,
10 | Stack,
11 | Text,
12 | Image,
13 | Spinner,
14 | } from "@chakra-ui/react";
15 | import { HiOutlineDotsHorizontal } from "react-icons/hi";
16 | import { RiCakeLine } from "react-icons/ri";
17 | import Link from "next/link";
18 | import { useRouter } from "next/router";
19 | import { useAuthState } from "react-firebase-hooks/auth";
20 | import { auth, firestore, storage } from "../../firebase/clientApp";
21 | import { Community, communityState } from "../../atoms/communitiesAtom";
22 | import moment from "moment";
23 | import { useRecoilValue, useSetRecoilState } from "recoil";
24 | import { FaReddit } from "react-icons/fa";
25 | import { getDownloadURL, ref, uploadString } from "firebase/storage";
26 | import { doc, updateDoc } from "firebase/firestore";
27 |
28 | type AboutProps = {
29 | communityData: Community;
30 | pt?: number;
31 | onCreatePage?: boolean;
32 | loading?: boolean;
33 | };
34 |
35 | const About: React.FC = ({
36 | communityData,
37 | pt,
38 | onCreatePage,
39 | loading,
40 | }) => {
41 | const [user] = useAuthState(auth); // will revisit how 'auth' state is passed
42 | const router = useRouter();
43 | const selectFileRef = useRef(null);
44 | const setCommunityStateValue = useSetRecoilState(communityState);
45 |
46 | // April 24 - moved this logic to custom hook in tutorial build (useSelectFile)
47 | const [selectedFile, setSelectedFile] = useState();
48 |
49 | // Added last!
50 | const [imageLoading, setImageLoading] = useState(false);
51 |
52 | const onSelectImage = (event: React.ChangeEvent) => {
53 | const reader = new FileReader();
54 | if (event.target.files?.[0]) {
55 | reader.readAsDataURL(event.target.files[0]);
56 | }
57 |
58 | reader.onload = (readerEvent) => {
59 | if (readerEvent.target?.result) {
60 | setSelectedFile(readerEvent.target?.result as string);
61 | }
62 | };
63 | };
64 |
65 | const updateImage = async () => {
66 | if (!selectedFile) return;
67 | setImageLoading(true);
68 | try {
69 | const imageRef = ref(storage, `communities/${communityData.id}/image`);
70 | await uploadString(imageRef, selectedFile, "data_url");
71 | const downloadURL = await getDownloadURL(imageRef);
72 | await updateDoc(doc(firestore, "communities", communityData.id), {
73 | imageURL: downloadURL,
74 | });
75 | console.log("HERE IS DOWNLOAD URL", downloadURL);
76 |
77 | // April 24 - added state update
78 | setCommunityStateValue((prev) => ({
79 | ...prev,
80 | currentCommunity: {
81 | ...prev.currentCommunity,
82 | imageURL: downloadURL,
83 | },
84 | }));
85 | } catch (error: any) {
86 | console.log("updateImage error", error.message);
87 | }
88 | // April 24 - removed reload
89 | // window.location.reload();
90 |
91 | setImageLoading(false);
92 | };
93 |
94 | return (
95 |
96 |
104 |
105 | About Community
106 |
107 |
108 |
109 |
110 | {loading ? (
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 | ) : (
119 | <>
120 | {user?.uid === communityData?.creatorId && (
121 |
130 |
131 | Add description
132 |
133 |
134 | )}
135 |
136 |
137 |
138 |
139 | {communityData?.numberOfMembers?.toLocaleString()}
140 |
141 | Members
142 |
143 |
144 | 1
145 | Online
146 |
147 |
148 |
149 |
156 |
157 | {communityData?.createdAt && (
158 |
159 | Created{" "}
160 | {moment(
161 | new Date(communityData.createdAt!.seconds * 1000)
162 | ).format("MMM DD, YYYY")}
163 |
164 | )}
165 |
166 | {!onCreatePage && (
167 |
168 |
171 |
172 | )}
173 | {/* !!!ADDED AT THE VERY END!!! INITIALLY DOES NOT EXIST */}
174 | {user?.uid === communityData?.creatorId && (
175 | <>
176 |
177 |
178 | Admin
179 |
180 | selectFileRef.current?.click()}
185 | >
186 | Change Image
187 |
188 | {communityData?.imageURL || selectedFile ? (
189 |
195 | ) : (
196 |
202 | )}
203 |
204 | {selectedFile &&
205 | (imageLoading ? (
206 |
207 | ) : (
208 |
209 | Save Changes
210 |
211 | ))}
212 |
220 |
221 | >
222 | )}
223 |
224 | >
225 | )}
226 |
227 |
228 | );
229 | };
230 | export default About;
231 |
--------------------------------------------------------------------------------
/src/components/Post/Posts.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from "react";
2 | import { Stack } from "@chakra-ui/react";
3 | import {
4 | collection,
5 | doc,
6 | getDocs,
7 | onSnapshot,
8 | orderBy,
9 | query,
10 | where,
11 | writeBatch,
12 | } from "firebase/firestore";
13 | import { useRecoilState, useSetRecoilState } from "recoil";
14 | import { authModalState } from "../../atoms/authModalAtom";
15 | import { Community } from "../../atoms/communitiesAtom";
16 | import { firestore } from "../../firebase/clientApp";
17 | import PostLoader from "./Loader";
18 | import { Post, postState, PostVote } from "../../atoms/postsAtom";
19 | import PostItem from "./PostItem";
20 | import { useRouter } from "next/router";
21 | import usePosts from "../../hooks/usePosts";
22 |
23 | type PostsProps = {
24 | communityData?: Community;
25 | userId?: string;
26 | loadingUser: boolean;
27 | };
28 |
29 | const Posts: React.FC = ({
30 | communityData,
31 | userId,
32 | loadingUser,
33 | }) => {
34 | /**
35 | * PART OF INITIAL SOLUTION BEFORE CUSTOM HOOK
36 | */
37 | const [loading, setLoading] = useState(false);
38 | // const setAuthModalState = useSetRecoilState(authModalState);
39 | const router = useRouter();
40 |
41 | const { postStateValue, setPostStateValue, onVote, onDeletePost } = usePosts(
42 | communityData!
43 | );
44 |
45 | /**
46 | * USE ALL BELOW INITIALLY THEN CONVERT TO A CUSTOM HOOK AFTER
47 | * CREATING THE [PID] PAGE TO EXTRACT REPEATED LOGIC
48 | */
49 | // const onVote = async (
50 | // event: React.MouseEvent,
51 | // post: Post,
52 | // vote: number
53 | // ) => {
54 | // event.stopPropagation();
55 | // if (!userId) {
56 | // setAuthModalState({ open: true, view: "login" });
57 | // return;
58 | // }
59 |
60 | // const { voteStatus } = post;
61 |
62 | // // is this an upvote or a downvote?
63 | // // has this user voted on this post already? was it up or down?
64 | // const existingVote = postItems.postVotes.find(
65 | // (item: PostVote) => item.postId === post.id
66 | // );
67 |
68 | // try {
69 | // let voteChange = vote;
70 | // const batch = writeBatch(firestore);
71 |
72 | // // New vote
73 | // if (!existingVote) {
74 | // const newVote: PostVote = {
75 | // postId: post.id,
76 | // communityId: communityData.id!,
77 | // voteValue: vote,
78 | // };
79 |
80 | // const postVoteRef = doc(
81 | // collection(firestore, "users", `${userId}/postVotes`)
82 | // );
83 |
84 | // // Needed for frontend state since we're not getting resource back
85 | // newVote.id = postVoteRef.id;
86 | // batch.set(postVoteRef, {
87 | // postId: post.id,
88 | // communityId: communityData.id!,
89 | // voteValue: vote,
90 | // });
91 |
92 | // // Optimistically update state
93 | // setPostItems((prev) => ({
94 | // ...prev,
95 | // postVotes: [...prev.postVotes, newVote],
96 | // }));
97 | // }
98 | // // Removing existing vote
99 | // else {
100 | // // Used for both possible cases of batch writes
101 | // const postVoteRef = doc(
102 | // firestore,
103 | // "users",
104 | // `${userId}/postVotes/${existingVote.id}`
105 | // );
106 |
107 | // // Removing vote
108 | // if (existingVote.voteValue === vote) {
109 | // voteChange *= -1;
110 |
111 | // setPostItems((prev) => ({
112 | // ...prev,
113 | // postVotes: prev.postVotes.filter((item) => item.postId !== post.id),
114 | // }));
115 | // batch.delete(postVoteRef);
116 | // }
117 | // // Changing vote
118 | // else {
119 | // voteChange = 2 * vote;
120 |
121 | // batch.update(postVoteRef, {
122 | // voteValue: vote,
123 | // });
124 | // // Optimistically update state
125 | // const existingPostIdx = postItems.postVotes.findIndex(
126 | // (item) => item.postId === post.id
127 | // );
128 | // const updatedVotes = [...postItems.postVotes];
129 | // updatedVotes[existingPostIdx] = { ...existingVote, voteValue: vote };
130 | // setPostItems((prev) => ({
131 | // ...prev,
132 | // postVotes: updatedVotes,
133 | // }));
134 | // }
135 | // }
136 |
137 | // const postRef = doc(firestore, "posts", post.id);
138 | // batch.update(postRef, { voteStatus: voteStatus + voteChange });
139 |
140 | // /**
141 | // * Perform writes
142 | // * Could move state updates to after this
143 | // * but decided to optimistically update
144 | // */
145 | // await batch.commit();
146 | // } catch (error) {
147 | // console.log("onVote error", error);
148 | // }
149 | // };
150 |
151 | // const getUserPostVotes = async () => {
152 | // try {
153 | // const postVotesQuery = query(
154 | // collection(firestore, `users/${userId}/postVotes`),
155 | // where("communityId", "==", communityData.id)
156 | // );
157 |
158 | // const postVoteDocs = await getDocs(postVotesQuery);
159 | // const postVotes = postVoteDocs.docs.map((doc) => ({
160 | // id: doc.id,
161 | // ...doc.data(),
162 | // }));
163 | // setPostItems((prev) => ({
164 | // ...prev,
165 | // postVotes: postVotes as PostVote[],
166 | // }));
167 | // } catch (error) {
168 | // console.log("getUserPostVotes error", error);
169 | // }
170 | // };
171 |
172 | const onSelectPost = (post: Post, postIdx: number) => {
173 | setPostStateValue((prev) => ({
174 | ...prev,
175 | selectedPost: { ...post, postIdx },
176 | }));
177 | router.push(`/r/${communityData?.id!}/comments/${post.id}`);
178 | };
179 |
180 | useEffect(() => {
181 | if (
182 | postStateValue.postsCache[communityData?.id!] &&
183 | !postStateValue.postUpdateRequired
184 | ) {
185 | setPostStateValue((prev) => ({
186 | ...prev,
187 | posts: postStateValue.postsCache[communityData?.id!],
188 | }));
189 | return;
190 | }
191 |
192 | getPosts();
193 | /**
194 | * REAL-TIME POST LISTENER
195 | * IMPLEMENT AT FIRST THEN CHANGE TO POSTS CACHE
196 | *
197 | * UPDATE - MIGHT KEEP THIS AS CACHE IS TOO COMPLICATED
198 | *
199 | * LATEST UPDATE - FOUND SOLUTION THAT MEETS IN THE MIDDLE
200 | * CACHE POST DATA, BUT REMOVE POSTVOTES CACHE AND HAVE
201 | * REAL-TIME LISTENER ON POSTVOTES
202 | */
203 | // const postsQuery = query(
204 | // collection(firestore, "posts"),
205 | // where("communityId", "==", communityData.id),
206 | // orderBy("createdAt", "desc")
207 | // );
208 | // const unsubscribe = onSnapshot(postsQuery, (querySnaption) => {
209 | // const posts = querySnaption.docs.map((post) => ({
210 | // id: post.id,
211 | // ...post.data(),
212 | // }));
213 | // setPostItems((prev) => ({
214 | // ...prev,
215 | // posts: posts as [],
216 | // }));
217 | // setLoading(false);
218 | // });
219 |
220 | // // Remove real-time listener on component dismount
221 | // return () => unsubscribe();
222 | }, [communityData, postStateValue.postUpdateRequired]);
223 |
224 | const getPosts = async () => {
225 | console.log("WE ARE GETTING POSTS!!!");
226 |
227 | setLoading(true);
228 | try {
229 | const postsQuery = query(
230 | collection(firestore, "posts"),
231 | where("communityId", "==", communityData?.id!),
232 | orderBy("createdAt", "desc")
233 | );
234 | const postDocs = await getDocs(postsQuery);
235 | const posts = postDocs.docs.map((doc) => ({ id: doc.id, ...doc.data() }));
236 | setPostStateValue((prev) => ({
237 | ...prev,
238 | posts: posts as Post[],
239 | postsCache: {
240 | ...prev.postsCache,
241 | [communityData?.id!]: posts as Post[],
242 | },
243 | postUpdateRequired: false,
244 | }));
245 | } catch (error: any) {
246 | console.log("getPosts error", error.message);
247 | }
248 | setLoading(false);
249 | };
250 |
251 | console.log("HERE IS POST STATE", postStateValue);
252 |
253 | return (
254 | <>
255 | {loading ? (
256 |
257 | ) : (
258 |
259 | {postStateValue.posts.map((post: Post, index) => (
260 | item.postId === post.id)
268 | ?.voteValue
269 | }
270 | userIsCreator={userId === post.creatorId}
271 | onSelectPost={onSelectPost}
272 | />
273 | ))}
274 |
275 | )}
276 | >
277 | );
278 | };
279 | export default Posts;
280 |
--------------------------------------------------------------------------------
/src/hooks/usePosts.ts:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from "react";
2 | import {
3 | collection,
4 | deleteDoc,
5 | doc,
6 | getDocs,
7 | onSnapshot,
8 | query,
9 | where,
10 | writeBatch,
11 | } from "firebase/firestore";
12 | import { deleteObject, ref } from "firebase/storage";
13 | import { useAuthState } from "react-firebase-hooks/auth";
14 | import { useRecoilState, useRecoilValue, useSetRecoilState } from "recoil";
15 | import { authModalState } from "../atoms/authModalAtom";
16 | import { Community, communityState } from "../atoms/communitiesAtom";
17 | import { Post, postState, PostVote } from "../atoms/postsAtom";
18 | import { auth, firestore, storage } from "../firebase/clientApp";
19 | import { useRouter } from "next/router";
20 |
21 | const usePosts = (communityData?: Community) => {
22 | const [user, loadingUser] = useAuthState(auth);
23 | const [postStateValue, setPostStateValue] = useRecoilState(postState);
24 | const setAuthModalState = useSetRecoilState(authModalState);
25 | const [loading, setLoading] = useState(false);
26 | const [error, setError] = useState("");
27 | const router = useRouter();
28 | const communityStateValue = useRecoilValue(communityState);
29 |
30 | const onSelectPost = (post: Post, postIdx: number) => {
31 | console.log("HERE IS STUFF", post, postIdx);
32 |
33 | setPostStateValue((prev) => ({
34 | ...prev,
35 | selectedPost: { ...post, postIdx },
36 | }));
37 | router.push(`/r/${post.communityId}/comments/${post.id}`);
38 | };
39 |
40 | const onVote = async (
41 | event: React.MouseEvent,
42 | post: Post,
43 | vote: number,
44 | communityId: string
45 | // postIdx?: number
46 | ) => {
47 | event.stopPropagation();
48 | if (!user?.uid) {
49 | setAuthModalState({ open: true, view: "login" });
50 | return;
51 | }
52 |
53 | const { voteStatus } = post;
54 | // const existingVote = post.currentUserVoteStatus;
55 | const existingVote = postStateValue.postVotes.find(
56 | (vote) => vote.postId === post.id
57 | );
58 |
59 | // is this an upvote or a downvote?
60 | // has this user voted on this post already? was it up or down?
61 |
62 | try {
63 | let voteChange = vote;
64 | const batch = writeBatch(firestore);
65 |
66 | const updatedPost = { ...post };
67 | const updatedPosts = [...postStateValue.posts];
68 | let updatedPostVotes = [...postStateValue.postVotes];
69 |
70 | // New vote
71 | if (!existingVote) {
72 | const postVoteRef = doc(
73 | collection(firestore, "users", `${user.uid}/postVotes`)
74 | );
75 |
76 | const newVote: PostVote = {
77 | id: postVoteRef.id,
78 | postId: post.id,
79 | communityId,
80 | voteValue: vote,
81 | };
82 |
83 | console.log("NEW VOTE!!!", newVote);
84 |
85 | // APRIL 25 - DON'T THINK WE NEED THIS
86 | // newVote.id = postVoteRef.id;
87 |
88 | batch.set(postVoteRef, newVote);
89 |
90 | updatedPost.voteStatus = voteStatus + vote;
91 | updatedPostVotes = [...updatedPostVotes, newVote];
92 | }
93 | // Removing existing vote
94 | else {
95 | // Used for both possible cases of batch writes
96 | const postVoteRef = doc(
97 | firestore,
98 | "users",
99 | `${user.uid}/postVotes/${existingVote.id}`
100 | );
101 |
102 | // Removing vote
103 | if (existingVote.voteValue === vote) {
104 | voteChange *= -1;
105 | updatedPost.voteStatus = voteStatus - vote;
106 | updatedPostVotes = updatedPostVotes.filter(
107 | (vote) => vote.id !== existingVote.id
108 | );
109 | batch.delete(postVoteRef);
110 | }
111 | // Changing vote
112 | else {
113 | voteChange = 2 * vote;
114 | updatedPost.voteStatus = voteStatus + 2 * vote;
115 | const voteIdx = postStateValue.postVotes.findIndex(
116 | (vote) => vote.id === existingVote.id
117 | );
118 | // console.log("HERE IS VOTE INDEX", voteIdx);
119 |
120 | // Vote was found - findIndex returns -1 if not found
121 | if (voteIdx !== -1) {
122 | updatedPostVotes[voteIdx] = {
123 | ...existingVote,
124 | voteValue: vote,
125 | };
126 | }
127 | batch.update(postVoteRef, {
128 | voteValue: vote,
129 | });
130 | }
131 | }
132 |
133 | let updatedState = { ...postStateValue, postVotes: updatedPostVotes };
134 |
135 | const postIdx = postStateValue.posts.findIndex(
136 | (item) => item.id === post.id
137 | );
138 |
139 | // if (postIdx !== undefined) {
140 | updatedPosts[postIdx!] = updatedPost;
141 | updatedState = {
142 | ...updatedState,
143 | posts: updatedPosts,
144 | postsCache: {
145 | ...updatedState.postsCache,
146 | [communityId]: updatedPosts,
147 | },
148 | };
149 | // }
150 |
151 | /**
152 | * Optimistically update the UI
153 | * Used for single page view [pid]
154 | * since we don't have real-time listener there
155 | */
156 | if (updatedState.selectedPost) {
157 | updatedState = {
158 | ...updatedState,
159 | selectedPost: updatedPost,
160 | };
161 | }
162 |
163 | // Optimistically update the UI
164 | setPostStateValue(updatedState);
165 |
166 | // Update database
167 | const postRef = doc(firestore, "posts", post.id);
168 | batch.update(postRef, { voteStatus: voteStatus + voteChange });
169 | await batch.commit();
170 | } catch (error) {
171 | console.log("onVote error", error);
172 | }
173 | };
174 |
175 | const onDeletePost = async (post: Post): Promise => {
176 | console.log("DELETING POST: ", post.id);
177 |
178 | try {
179 | // if post has an image url, delete it from storage
180 | if (post.imageURL) {
181 | const imageRef = ref(storage, `posts/${post.id}/image`);
182 | await deleteObject(imageRef);
183 | }
184 |
185 | // delete post from posts collection
186 | const postDocRef = doc(firestore, "posts", post.id);
187 | await deleteDoc(postDocRef);
188 |
189 | // Update post state
190 | setPostStateValue((prev) => ({
191 | ...prev,
192 | posts: prev.posts.filter((item) => item.id !== post.id),
193 | postsCache: {
194 | ...prev.postsCache,
195 | [post.communityId]: prev.postsCache[post.communityId]?.filter(
196 | (item) => item.id !== post.id
197 | ),
198 | },
199 | }));
200 |
201 | /**
202 | * Cloud Function will trigger on post delete
203 | * to delete all comments with postId === post.id
204 | */
205 | return true;
206 | } catch (error) {
207 | console.log("THERE WAS AN ERROR", error);
208 | return false;
209 | }
210 | };
211 |
212 | const getCommunityPostVotes = async (communityId: string) => {
213 | const postVotesQuery = query(
214 | collection(firestore, `users/${user?.uid}/postVotes`),
215 | where("communityId", "==", communityId)
216 | );
217 | const postVoteDocs = await getDocs(postVotesQuery);
218 | const postVotes = postVoteDocs.docs.map((doc) => ({
219 | id: doc.id,
220 | ...doc.data(),
221 | }));
222 | setPostStateValue((prev) => ({
223 | ...prev,
224 | postVotes: postVotes as PostVote[],
225 | }));
226 |
227 | // const unsubscribe = onSnapshot(postVotesQuery, (querySnapshot) => {
228 | // const postVotes = querySnapshot.docs.map((postVote) => ({
229 | // id: postVote.id,
230 | // ...postVote.data(),
231 | // }));
232 |
233 | // });
234 |
235 | // return () => unsubscribe();
236 | };
237 |
238 | useEffect(() => {
239 | if (!user?.uid || !communityStateValue.currentCommunity) return;
240 | getCommunityPostVotes(communityStateValue.currentCommunity.id);
241 | }, [user, communityStateValue.currentCommunity]);
242 |
243 | /**
244 | * DO THIS INITIALLY FOR POST VOTES
245 | */
246 | // useEffect(() => {
247 | // if (!user?.uid || !communityData) return;
248 | // const postVotesQuery = query(
249 | // collection(firestore, `users/${user?.uid}/postVotes`),
250 | // where("communityId", "==", communityData?.id)
251 | // );
252 | // const unsubscribe = onSnapshot(postVotesQuery, (querySnapshot) => {
253 | // const postVotes = querySnapshot.docs.map((postVote) => ({
254 | // id: postVote.id,
255 | // ...postVote.data(),
256 | // }));
257 |
258 | // setPostStateValue((prev) => ({
259 | // ...prev,
260 | // postVotes: postVotes as PostVote[],
261 | // }));
262 | // });
263 |
264 | // return () => unsubscribe();
265 | // }, [user, communityData]);
266 |
267 | useEffect(() => {
268 | // Logout or no authenticated user
269 | if (!user?.uid && !loadingUser) {
270 | setPostStateValue((prev) => ({
271 | ...prev,
272 | postVotes: [],
273 | }));
274 | return;
275 | }
276 | }, [user, loadingUser]);
277 |
278 | return {
279 | postStateValue,
280 | setPostStateValue,
281 | onSelectPost,
282 | onDeletePost,
283 | loading,
284 | setLoading,
285 | onVote,
286 | error,
287 | };
288 | };
289 |
290 | export default usePosts;
291 |
--------------------------------------------------------------------------------