Skip to content

Commit

Permalink
Added documentation
Browse files Browse the repository at this point in the history
  • Loading branch information
mbeps committed Feb 20, 2023
1 parent 1583151 commit b3c0779
Show file tree
Hide file tree
Showing 12 changed files with 253 additions and 71 deletions.
8 changes: 1 addition & 7 deletions src/atoms/communitiesAtom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,7 @@ import { Timestamp } from "firebase/firestore";
import { atom } from "recoil";

/**
* Interface representing a community.
* Community: id: string;
- `creatorId`: string;
- `numberOfMembers`: number;
- `privacyType`: "public" | "restricted" | "private";
- `createdAt`: Timestamp;
- `imageURL`: string;
* Interface representing a community.
*/
export interface Community {
id: string;
Expand Down
22 changes: 22 additions & 0 deletions src/atoms/postsAtom.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { Timestamp } from "firebase/firestore";
import { atom } from "recoil";

/**
* Interface representing a post.
*/
export type Post = {
id?: string; // optional because firebase will automatically add the id for the post
communityId: string;
Expand All @@ -15,25 +18,44 @@ export type Post = {
createTime: Timestamp;
};

/**
* Snippet representing user voting on a post.
* This snippet is stored in the `users` collection in Firebase.
* Stores the ID of the post, ID of the community of that post, and whether it was liked or disliked (+-1)
*/
export type PostVote = {
id: string;
postId: string;
communityId: string;
voteValue: number;
};

/**
* Represents the base state for the Recoil atom.
*/
interface PostState {
selectedPost: Post | null; // when user opens a post
posts: Post[]; // all the post
postVotes: PostVote[];
}

/**
* Represents the default state of the Recoil atom.
* Initially, no post is selected, there are no posts and posts have not been voted on.
* @requires PostState - default state type
*/
const defaultPostState: PostState = {
selectedPost: null,
posts: [],
postVotes: [],
};

/**
* Atom which describes the recoil state.
* @requires PostState - type of the state
* @requires defaultPostState - default state of the atom
* @see https://recoiljs.org/docs/basic-tutorial/atoms/
*/
export const postState = atom<PostState>({
key: "postState",
default: defaultPostState,
Expand Down
46 changes: 38 additions & 8 deletions src/components/Community/About.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,46 +21,66 @@ import { useAuthState } from "react-firebase-hooks/auth";
import { HiOutlineDotsHorizontal } from "react-icons/hi";
import { useSetRecoilState } from "recoil";

/**
* Props for About component.
*/
type AboutProps = {
communityData: Community;
};

/**
* About component displaying:
* - The number of subscribers in the community
* - Date when the community was created
* - Button for creating a new post
*
* Additional elements are displayed if the current user is an admin:
* - Button for changing image
* @param {communityData} - data required to be displayed
* @returns (React.FC<AboutProps>) - About component
*/
const About: React.FC<AboutProps> = ({ communityData }) => {
const [user] = useAuthState(auth);
const selectFileRef = useRef<HTMLInputElement>(null);
const { selectedFile, setSelectedFile, onSelectFile } = useSelectFile();
const [uploadingImage, setUploadingImage] = useState(false);
const setCommunityStateValue = useSetRecoilState(communityState);

/**
* Allows admin to change the image of the community.
* @returns null if no file is selected
*/
const onUpdateImage = async () => {
if (!selectedFile) {
// if no file is selected, do nothing
return;
}
setUploadingImage(true);
setUploadingImage(true); // set uploading image to true

try {
const imageRef = ref(storage, `communities/${communityData.id}/image`);
await uploadString(imageRef, selectedFile, "data_url");
const downloadURL = await getDownloadURL(imageRef);
const imageRef = ref(storage, `communities/${communityData.id}/image`); // create reference to image in storage
await uploadString(imageRef, selectedFile, "data_url"); // upload image to storage
const downloadURL = await getDownloadURL(imageRef); // get download url of image
await updateDoc(doc(firestore, "communities", communityData.id), {
imageURL: downloadURL,
});
}); // update imageURL in firestore

setCommunityStateValue((prev) => ({
...prev,
currentCommunity: {
...prev.currentCommunity,
imageURL: downloadURL,
} as Community,
}));
})); // update imageURL in recoil state
} catch (error) {
console.log("Error: onUploadImage", error);
} finally {
setUploadingImage(false);
setUploadingImage(false); // set uploading image to false
}
};

return (
// sticky position for the about section
<Box position="sticky" top="50px">
<Flex
justify="space-between"
Expand All @@ -76,6 +96,7 @@ const About: React.FC<AboutProps> = ({ communityData }) => {
<Icon as={HiOutlineDotsHorizontal} />
</Flex>

{/* about section */}
<Flex
direction="column"
p={3}
Expand All @@ -85,9 +106,12 @@ const About: React.FC<AboutProps> = ({ communityData }) => {
<Stack>
<Flex width="100%" p={2} fontSize="10pt">
<Flex direction="column" flexGrow={1}>
{/* number of subscribers and date created */}
<Text fontWeight={700}>Subscribers</Text>
<Text>{communityData.numberOfMembers.toLocaleString()}</Text>
</Flex>

{/* when the community was created */}
<Flex direction="column" flexGrow={1}>
<Text fontWeight={700}>Created</Text>
<Text>{communityData.createdAt && "Not Working :("}</Text>
Expand All @@ -104,6 +128,7 @@ const About: React.FC<AboutProps> = ({ communityData }) => {
<Divider />
<Stack fontSize="10pt" spacing={1}>
<Text fontWeight={600}>Admin</Text>
{/* change image button */}
<Flex align="center" justify="space-between">
<Text
color="red.500"
Expand All @@ -113,23 +138,28 @@ const About: React.FC<AboutProps> = ({ communityData }) => {
>
Change Image
</Text>
{/* image preview */}
{communityData?.imageURL || selectedFile ? (
// selected image
<Image
borderRadius="full"
boxSize="40px"
src={selectedFile || communityData?.imageURL}
alt="Dan Abramov"
alt="Selected image"
/>
) : (
// default image
<Image
src="/images/logo.svg"
height="40px"
alt="Website logo"
/>
)}
</Flex>
{/* save changes button */}
{selectedFile &&
(uploadingImage ? (
// while image is loading show spinner for loading
<Spinner />
) : (
<Text cursor="pointer" onClick={onUpdateImage}>
Expand Down
16 changes: 12 additions & 4 deletions src/components/Community/CreatePostLink.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@ import { useSetRecoilState } from "recoil";

type CreatePostProps = {};

/**
* Component for creating a new post.
* Redirects the user to the create post page.
* If the user is not logged in, the authentication modal is opened.
* @returns (React.FC<CreatePostProps>) - CreatePostLink component
*/
const CreatePostLink: React.FC<CreatePostProps> = () => {
const router = useRouter();
const [user] = useAuthState(auth);
Expand All @@ -20,12 +26,13 @@ const CreatePostLink: React.FC<CreatePostProps> = () => {
const onClick = () => {
// check if the user is logged in as post cannot be created without user
if (!user) {
setAuthModalState({ open: true, view: "login" });
return;
// if user is not logged in
setAuthModalState({ open: true, view: "login" }); // open login modal
return; // exit function
}
const { communityId } = router.query;
const { communityId } = router.query; // get community id from router
// redirect user to following link
router.push(`/community/${communityId}/submit`);
router.push(`/community/${communityId}/submit`); // redirect user to create post page
};

return (
Expand All @@ -41,6 +48,7 @@ const CreatePostLink: React.FC<CreatePostProps> = () => {
mb={4}
>
<Icon as={IoIosCreate} fontSize={36} color="gray.300" mr={4} />
{/* Input for creating a new post */}
<Input
placeholder="Create Post"
fontSize="10pt"
Expand Down
32 changes: 28 additions & 4 deletions src/components/Posts/NewPostForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,21 @@ import ImageUpload from "./PostForm/ImageUpload";
import TextInputs from "./PostForm/TextInputs";
import TabItem from "./TabItem";

/**
* Props for NewPostForm component.
* @param {user} - user object
*/
type NewPostFormProps = {
user: User; // parent component checks user so additional checks aer not needed ut
};

// Tab items which are static (not react) hence outside
/**
* Tabs for post creation form.
* Static array of objects which are used to dynamically create the navbar component.
* @param {title} - title of the tab
* @param {icon} - icon of the tab
*/
const formTabs: FormTab[] = [
{
title: "Post",
Expand All @@ -43,6 +53,11 @@ const formTabs: FormTab[] = [
// more can be added which would dynamically be fitted into post creation navbar component
];

/**
* Tab object.
* @param {title} - title of the tab
* @param {icon} - icon of the tab
*/
export type FormTab = {
title: string;
icon: typeof Icon.arguments;
Expand Down Expand Up @@ -71,10 +86,12 @@ const NewPostForm: React.FC<NewPostFormProps> = ({ user }) => {
numberOfComments: 0,
voteStatus: 0,
createTime: serverTimestamp() as Timestamp,
};
}; // object representing the post

setLoading(true);

try {
const postDocRef = await addDoc(collection(firestore, "posts"), newPost);
const postDocRef = await addDoc(collection(firestore, "posts"), newPost); // add the post to Firestore
if (selectedFile) {
// check if user has uploaded a file
const imageRef = ref(storage, `posts/${postDocRef.id}/image`); // reference to where image is saved in Firebase storage
Expand All @@ -84,7 +101,7 @@ const NewPostForm: React.FC<NewPostFormProps> = ({ user }) => {
// add image link to the posts in Firestore
imageURL: downloadURL,
});
router.back();
router.back(); // redirect user back to communities page after post is created
}
} catch (error: any) {
console.log("Error: handleCreatePost", error.message);
Expand All @@ -99,6 +116,11 @@ const NewPostForm: React.FC<NewPostFormProps> = ({ user }) => {
// get the link to file and store it to firestore
// redirect user back to communities page
};

/**
* Keeps track of the text inputs in the form and updates the state.
* @param event (React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) - event object
*/
const onTextChange = (
event: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
) => {
Expand All @@ -114,7 +136,7 @@ const NewPostForm: React.FC<NewPostFormProps> = ({ user }) => {
return (
<Flex direction="column" bg="white" borderRadius={4} mt={2}>
<Flex width="100%">
{/* 1 tab component which takes the formTab as prop */}
{/* create a tab item for each tab in the formTabs array */}
{formTabs.map((item) => (
<TabItem
key={item.title}
Expand All @@ -125,6 +147,7 @@ const NewPostForm: React.FC<NewPostFormProps> = ({ user }) => {
))}
</Flex>
<Flex p={4}>
{/* Display the correct form based on the selected tab */}
{selectedTab === "Post" && (
<TextInputs
textInputs={textInputs}
Expand All @@ -133,6 +156,7 @@ const NewPostForm: React.FC<NewPostFormProps> = ({ user }) => {
loading={loading}
/>
)}
{/* Display the image upload form if the user has selected the Images & Videos tab */}
{selectedTab === "Images & Videos" && (
<ImageUpload
selectedFile={selectedFile}
Expand Down
15 changes: 15 additions & 0 deletions src/components/Posts/PostForm/ImageUpload.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,26 @@
import { Button, Flex, Image, Stack } from "@chakra-ui/react";
import React, { useRef } from "react";

/**
* Props for ImageUpload component.
*/
type ImageUploadProps = {
selectedFile?: string; // user does not need to upload a file
onSelectImage: (event: React.ChangeEvent<HTMLInputElement>) => void;
setSelectedTab: (value: string) => void; // after image is uploaded, return to post section
setSelectedFile: (value: string) => void; // clear image and select a new one
};

/**
* Sub-component of `NewPostForm` component.
* Allows user to upload an image to be used in the post.
* Initially, the user is presented with a button to upload an image.
* After the image is uploaded, the user is presented with the image and two buttons:
* - Back to Post: returns to the post section
* - Remove Content: removes the image and returns to the upload button
* @param {selectedFile, onSelectImage, setSelectedTab, setSelectedFile} - required props
* @returns (React.FC<ImageUploadProps>) - ImageUpload component
*/
const ImageUpload: React.FC<ImageUploadProps> = ({
selectedFile,
onSelectImage,
Expand Down Expand Up @@ -52,13 +65,15 @@ const ImageUpload: React.FC<ImageUploadProps> = ({
width="100%"
borderRadius={10}
>
{/* Upload button */}
<Button
onClick={() => {
selectedFileRef.current?.click();
}}
>
Upload Content
</Button>
{/* Hidden input */}
<input
type="file"
ref={selectedFileRef}
Expand Down
Loading

0 comments on commit b3c0779

Please sign in to comment.