├── .eslintignore
├── .eslintrc.json
├── .gitignore
├── .prettierrc
├── package-lock.json
├── package.json
├── postcss.config.js
├── public
├── favicon.ico
└── images
│ ├── banner.jpg
│ └── logos
│ ├── tweeter-light.svg
│ ├── tweeter-small.svg
│ └── tweeter.svg
├── src
├── components
│ ├── AuthForm
│ │ └── AuthForm.jsx
│ ├── Avatar
│ │ └── Avatar.jsx
│ ├── Banner
│ │ └── Banner.jsx
│ ├── CommentInput
│ │ └── CommentInput.jsx
│ ├── Comments
│ │ └── Comments.jsx
│ ├── ExploreFIlters
│ │ └── ExploreFilters.jsx
│ ├── Filters
│ │ └── Filters.jsx
│ ├── FollowButton
│ │ └── FollowButton.jsx
│ ├── Footer
│ │ └── Footer.jsx
│ ├── Modal
│ │ └── Modal.jsx
│ ├── NavBar
│ │ └── NavBar.jsx
│ ├── Post
│ │ └── Post.jsx
│ ├── ProfileDropDown
│ │ └── ProfileDropDown.jsx
│ ├── Suggestions
│ │ └── Suggestions.jsx
│ ├── Trends
│ │ └── Trends.jsx
│ ├── TweetInput
│ │ └── TweetInput.jsx
│ └── UserInfo
│ │ └── UserInfo.jsx
├── context
│ ├── BookmarksTweetsContext.js
│ ├── ExploreTweetsContext.js
│ ├── HomeTweetsContext.js
│ └── UserContext.js
├── firebase
│ └── init.js
├── hooks
│ ├── useFollowers.js
│ └── useFollowings.js
├── layouts
│ └── index.jsx
├── pages
│ ├── [username]
│ │ ├── index.jsx
│ │ └── status
│ │ │ └── [tweetId].jsx
│ ├── _app.jsx
│ ├── _document.jsx
│ ├── bookmarks.jsx
│ ├── explore.jsx
│ ├── home.jsx
│ ├── index.jsx
│ ├── login.jsx
│ └── signup.jsx
├── services
│ ├── Authentication.js
│ ├── DeleteAccount.js
│ ├── DeleteTweet.js
│ ├── FetchData.js
│ └── PostTweet.js
└── styles
│ ├── global.css
│ └── reset.css
└── tailwind.config.js
/.eslintignore:
--------------------------------------------------------------------------------
1 | # don't ever lint node_modules
2 | node_modules
3 | # don't lint build output (make sure it's set to your correct build folder name)
4 | dist
5 | # don't lint nyc coverage output
6 | coverage
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ShubhamVerma1811/Tweeter/9f5932da65cfc9ec475225e399c1fdca028f6eb5/.eslintrc.json
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # next.js
12 | /.next/
13 | /out/
14 |
15 | # production
16 | /build
17 |
18 | # misc
19 | .DS_Store
20 | *.pem
21 |
22 | # debug
23 | npm-debug.log*
24 | yarn-debug.log*
25 | yarn-error.log*
26 |
27 | # local env files
28 | .env.local
29 | .env.development.local
30 | .env.test.local
31 | .env.production.local
32 |
33 | # vercel
34 | .vercel
35 |
36 | # Created by https://www.toptal.com/developers/gitignore/api/react,firebase
37 | # Edit at https://www.toptal.com/developers/gitignore?templates=react,firebase
38 |
39 | ### Firebase ###
40 | .idea
41 | **/node_modules/*
42 | **/.firebaserc
43 |
44 | ### Firebase Patch ###
45 | .runtimeconfig.json
46 | .firebase/
47 |
48 | ### react ###
49 | .DS_*
50 | *.log
51 | logs
52 | **/*.backup.*
53 | **/*.back.*
54 |
55 | /testData
56 |
57 | node_modules
58 | bower_components
59 |
60 | *.sublime*
61 |
62 | psd
63 | thumb
64 | sketch
65 |
66 | # End of https://www.toptal.com/developers/gitignore/api/react,firebase
67 |
68 | .env
69 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "trailingComma": "es5",
3 | "jsxBracketSameLine": true
4 | }
5 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "tweeter",
3 | "version": "0.1.0",
4 | "private": true,
5 | "author": {
6 | "name": "Shubham Verma"
7 | },
8 | "scripts": {
9 | "dev": "next dev",
10 | "build": "next build",
11 | "start": "next start",
12 | "lint": "eslint src/**/* -c .eslintrc.json --ext js,jsx",
13 | "lint-fix": "eslint src/**/* -c .eslintrc.json --fix --ext js,jsx",
14 | "format": "prettier --write src/**/*.{js,jsx,html,css,scss,json}"
15 | },
16 | "dependencies": {
17 | "@material-ui/core": "^4.11.1",
18 | "@material-ui/icons": "^4.9.1",
19 | "firebase": "^8.1.1",
20 | "next": "10.0.2",
21 | "react": "17.0.1",
22 | "react-dom": "17.0.1",
23 | "shortid": "^2.2.16"
24 | },
25 | "devDependencies": {
26 | "babel-eslint": "^10.1.0",
27 | "eslint": "^7.14.0",
28 | "eslint-config-airbnb": "^18.2.1",
29 | "eslint-config-prettier": "^6.15.0",
30 | "eslint-plugin-import": "^2.22.1",
31 | "eslint-plugin-jsx-a11y": "^6.4.1",
32 | "eslint-plugin-prettier": "^3.1.4",
33 | "eslint-plugin-react": "^7.21.5",
34 | "eslint-plugin-react-hooks": "^4.2.0",
35 | "prettier": "^2.2.0",
36 | "@fullhuman/postcss-purgecss": "^2.3.0",
37 | "autoprefixer": "^9.8.6",
38 | "tailwindcss": "^1.8.12"
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | ...(process.env.NODE_ENV === "production"
6 | ? {
7 | "@fullhuman/postcss-purgecss": {
8 | content: ["./src/**/*.jsx"],
9 | defaultExtractor: (content) =>
10 | content.match(/[\w-/:]+(?
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/public/images/logos/tweeter-small.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/public/images/logos/tweeter.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/src/components/AuthForm/AuthForm.jsx:
--------------------------------------------------------------------------------
1 | import CircularProgress from "@material-ui/core/CircularProgress";
2 | import Link from "next/link";
3 | import { useState } from "react";
4 | import shortID from "shortid";
5 | import { handleSignIn, handleSignUp } from "../../services/Authentication";
6 |
7 | const AuthForm = ({ type }) => {
8 | const [email, setEmail] = useState("");
9 | const [password, setPassword] = useState("");
10 | const [username, setUsername] = useState("");
11 | const [name, setName] = useState("");
12 | const [authLoading, setAuthLoading] = useState(false);
13 | const [authErrMsg, setAuthErrMsg] = useState(null);
14 |
15 | const signIn = () => (
16 |
103 | );
104 |
105 | const signUp = () => (
106 |
249 | );
250 |
251 | return (
252 |
253 | {type === "signIn" && signIn()}
254 | {type === "signUp" && signUp()}
255 |
256 | );
257 | };
258 |
259 | export default AuthForm;
260 |
--------------------------------------------------------------------------------
/src/components/Avatar/Avatar.jsx:
--------------------------------------------------------------------------------
1 | import PropTypes from "prop-types";
2 |
3 | const Avatar = ({ src }) => {
4 | return (
5 |
6 | );
7 | };
8 |
9 | Avatar.propTypes = {
10 | src: PropTypes.string,
11 | };
12 |
13 | Avatar.defaultProps = { src: "" };
14 | export default Avatar;
15 |
--------------------------------------------------------------------------------
/src/components/Banner/Banner.jsx:
--------------------------------------------------------------------------------
1 | const Banner = () => {
2 | return (
3 |
8 |

13 |
14 | );
15 | };
16 |
17 | export default Banner;
18 |
--------------------------------------------------------------------------------
/src/components/CommentInput/CommentInput.jsx:
--------------------------------------------------------------------------------
1 | import { useContext, useState } from "react";
2 | import UserContext from "../../context/UserContext";
3 | import postTweet from "../../services/PostTweet";
4 | import Avatar from "../Avatar/Avatar";
5 |
6 | const CommentInput = ({ tweetID }) => {
7 | const { user } = useContext(UserContext);
8 | const [comment, setComment] = useState("");
9 |
10 | return (
11 |
35 | );
36 | };
37 |
38 | export default CommentInput;
39 |
--------------------------------------------------------------------------------
/src/components/Comments/Comments.jsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from "react";
2 | import firebase from "../../firebase/init";
3 | import { fetchUser } from "../../services/FetchData";
4 | import Avatar from "../Avatar/Avatar";
5 |
6 | const Comments = ({ tweetID }) => {
7 | const [comments, setComments] = useState([]);
8 |
9 | useEffect(() => {
10 | const getComments = async () => {
11 | try {
12 | firebase
13 | .firestore()
14 | .collection("tweets")
15 | .where("parentTweet", "==", tweetID)
16 | .onSnapshot(async (tweetsRef) => {
17 | const localComments = [];
18 |
19 | for (let i = 0; i < tweetsRef.size; i++) {
20 | const tweet = tweetsRef.docs[i].data({
21 | serverTimestamps: "estimate",
22 | });
23 | const id = tweetsRef.docs[i].id;
24 | const userInfo = await fetchUser({
25 | userID: tweet.authorId,
26 | });
27 | localComments.push({
28 | ...tweet,
29 | id,
30 | createdAt: tweet.createdAt.toDate().toString(),
31 | author: userInfo,
32 | });
33 | }
34 | setComments(localComments);
35 | });
36 | } catch (err) {
37 | console.log(err);
38 | }
39 | };
40 |
41 | getComments();
42 | }, []);
43 |
44 | return (
45 |
46 | {comments &&
47 | comments.length > 0 &&
48 | comments.map((comment) => (
49 |
50 |
51 |
54 |
55 |
56 |
57 |
58 | {comment.author.name}
59 |
60 |
61 | {comment.createdAt}{" "}
62 |
63 |
64 |
67 |
68 | {/*
69 |
75 |
.
76 |
77 | 12k Likes
78 |
79 |
*/}
80 |
81 |
82 |
83 | ))}
84 |
85 | );
86 | };
87 |
88 | export default Comments;
89 |
--------------------------------------------------------------------------------
/src/components/ExploreFIlters/ExploreFilters.jsx:
--------------------------------------------------------------------------------
1 | const ExploreFilters = () => {
2 | return (
3 |
6 |
7 | Top
8 |
9 |
10 | Latest
11 |
12 |
13 | People
14 |
15 |
16 | Media
17 |
18 |
19 | );
20 | };
21 |
22 | export default ExploreFilters;
23 |
--------------------------------------------------------------------------------
/src/components/Filters/Filters.jsx:
--------------------------------------------------------------------------------
1 | const Filters = () => {
2 | return (
3 |
6 |
7 | Tweets
8 |
9 |
10 | Tweets & Replies
11 |
12 |
13 | Media
14 |
15 |
16 | Likes
17 |
18 |
19 | );
20 | };
21 |
22 | export default Filters;
23 |
--------------------------------------------------------------------------------
/src/components/FollowButton/FollowButton.jsx:
--------------------------------------------------------------------------------
1 | import PersonAddIcon from "@material-ui/icons/PersonAdd";
2 | import React, { useContext, useEffect, useState } from "react";
3 | import UserContext from "../../context/UserContext";
4 | import firebase from "../../firebase/init";
5 |
6 | const FollowButton = ({ userID }) => {
7 | const { user } = useContext(UserContext);
8 | const [isFollowing, setIsFollowing] = useState(false);
9 | const [connectionDocID, setFollowingDocID] = useState("");
10 |
11 | const startFollowing = () => {
12 | if (!user) {
13 | alert("You need to sign in for that");
14 | return;
15 | }
16 | const { id } = firebase.firestore().collection("connections").add({
17 | followerID: user.uid,
18 | followeeID: userID,
19 | });
20 | setFollowingDocID(id);
21 | setIsFollowing(true);
22 | };
23 |
24 | const stopFollowing = () => {
25 | if (!user) {
26 | alert("You need to sign in for that");
27 | return;
28 | }
29 | firebase
30 | .firestore()
31 | .collection("connections")
32 | .doc(connectionDocID)
33 | .delete();
34 | setIsFollowing(false);
35 | };
36 |
37 | useEffect(() => {
38 | if (user) {
39 | async function checkFollowing() {
40 | const result = await firebase
41 | .firestore()
42 | .collection("connections")
43 | .where("followeeID", "==", userID)
44 | .where("followerID", "==", user.uid)
45 | .get();
46 | if (result.size === 1) {
47 | setIsFollowing(true);
48 | setFollowingDocID(result.docs[0].id);
49 | }
50 | }
51 | checkFollowing();
52 | }
53 | }, [user]);
54 |
55 | return (
56 |
57 |
66 |
67 | );
68 | };
69 |
70 | export default FollowButton;
71 |
--------------------------------------------------------------------------------
/src/components/Footer/Footer.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | const Footer = () => {
4 | return (
5 |
18 | );
19 | };
20 |
21 | export default Footer;
22 |
--------------------------------------------------------------------------------
/src/components/Modal/Modal.jsx:
--------------------------------------------------------------------------------
1 | import CircularProgress from "@material-ui/core/CircularProgress";
2 | import Link from "next/link";
3 | import { useContext, useState } from "react";
4 | import UserContext from "../../context/UserContext";
5 | import Avatar from "../Avatar/Avatar";
6 | import FollowButton from "../FollowButton/FollowButton";
7 |
8 | const Modal = ({ users, close, loading }) => {
9 | const { user } = useContext(UserContext);
10 | const [isFollowing, setIsFollowing] = useState(false);
11 | const [followingDocID, setFollowingDocID] = useState("");
12 |
13 | return (
14 |
15 |
16 |
17 | {user.name} is following
18 |
19 |
24 |
25 | {loading && (
26 |
27 |
28 |
29 | )}
30 |
31 | {users.map((localUser) => {
32 | return (
33 |
34 |
35 |
38 |
39 |
40 |
41 | {localUser.name}
42 |
43 |
44 |
120k Followers
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 | {localUser.bio ? localUser.bio : "404 Bio Not Found"}
53 |
54 |
55 |
56 | );
57 | })}
58 |
59 | );
60 | };
61 |
62 | export default Modal;
63 |
--------------------------------------------------------------------------------
/src/components/NavBar/NavBar.jsx:
--------------------------------------------------------------------------------
1 | import BookmarkIcon from "@material-ui/icons/Bookmark";
2 | import ExploreIcon from "@material-ui/icons/Explore";
3 | import HomeIcon from "@material-ui/icons/Home";
4 | import Link from "next/link";
5 | import { useRouter } from "next/router";
6 | import React, { useContext } from "react";
7 | import UserContext from "../../context/UserContext";
8 | import ProfileDropDown from "../ProfileDropDown/ProfileDropDown";
9 |
10 | const NavBar = () => {
11 | const { user } = useContext(UserContext);
12 | const router = useRouter();
13 |
14 | return (
15 |
16 |
103 |
104 | );
105 | };
106 |
107 | export default NavBar;
108 |
--------------------------------------------------------------------------------
/src/components/Post/Post.jsx:
--------------------------------------------------------------------------------
1 | import BookmarkIcon from "@material-ui/icons/Bookmark";
2 | import BookmarkBorderIcon from "@material-ui/icons/BookmarkBorder";
3 | import ChatBubbleOutlineIcon from "@material-ui/icons/ChatBubbleOutline";
4 | import DeleteIcon from "@material-ui/icons/Delete";
5 | import FavoriteIcon from "@material-ui/icons/Favorite";
6 | import FavoriteBorderIcon from "@material-ui/icons/FavoriteBorder";
7 | import Link from "next/link";
8 | import { useContext, useEffect, useState } from "react";
9 | import UserContext from "../../context/UserContext";
10 | import firebase from "../../firebase/init";
11 | import { deleteTweet } from "../../services/DeleteTweet";
12 | import { fetchTweetLikes, fetchTweetSaves } from "../../services/FetchData";
13 | import Avatar from "../Avatar/Avatar";
14 |
15 | const Post = ({ tweet }) => {
16 | const { user } = useContext(UserContext);
17 | const [localTweet, setLocalTweet] = useState(tweet);
18 |
19 | const [likes, setLikes] = useState(0);
20 | const [isLiked, setIsLiked] = useState(false);
21 | const [likeDocID, setLikeDocID] = useState("");
22 |
23 | const [saves, setSaves] = useState(0);
24 | const [isSaved, setIsSaved] = useState(false);
25 | const [saveDocID, setSaveDocID] = useState("");
26 |
27 | const [comments, setComments] = useState(0);
28 |
29 | const [myTweet, setMyTweet] = useState(false);
30 |
31 | const likeTweet = async () => {
32 | if (!user) {
33 | alert("You need to sign in for that");
34 | return;
35 | }
36 | const { id } = await firebase.firestore().collection("likes").add({
37 | userID: user.uid,
38 | tweetID: tweet.id,
39 | });
40 | setLikes((prev) => prev + 1);
41 | setLikeDocID(id);
42 | setIsLiked(true);
43 | };
44 |
45 | const dislikeTweet = () => {
46 | if (!user) {
47 | alert("You need to sign in for that");
48 | return;
49 | }
50 | firebase.firestore().collection("likes").doc(likeDocID).delete();
51 | setLikes((prev) => prev - 1);
52 | setIsLiked(false);
53 | };
54 |
55 | const saveTweets = () => {
56 | if (!user) {
57 | alert("You need to sign in for that");
58 | return;
59 | }
60 | const { id } = firebase.firestore().collection("saves").add({
61 | tweetID: tweet.id,
62 | userID: user.uid,
63 | });
64 | setSaves((prev) => prev + 1);
65 | setSaveDocID(id);
66 | setIsSaved(true);
67 | };
68 |
69 | const unsaveTweets = () => {
70 | if (!user) {
71 | alert("You need to sign in for that");
72 | return;
73 | }
74 | firebase.firestore().collection("saves").doc(saveDocID).delete();
75 | setSaves((prev) => prev - 1);
76 | setIsSaved(false);
77 | };
78 |
79 | useEffect(async () => {
80 | setLikes((await fetchTweetLikes(localTweet.id)).size);
81 | if (user) {
82 | async function checkForLikes() {
83 | const docs = await firebase
84 | .firestore()
85 | .collection("likes")
86 | .where("userID", "==", user.uid)
87 | .where("tweetID", "==", tweet.id)
88 | .get();
89 | if (docs.size === 1) {
90 | setIsLiked(true);
91 | setLikeDocID(docs.docs[0].id);
92 | }
93 | }
94 | checkForLikes();
95 |
96 | async function checkForSaves() {
97 | const docs = await firebase
98 | .firestore()
99 | .collection("saves")
100 | .where("userID", "==", user.uid)
101 | .where("tweetID", "==", tweet.id)
102 | .get();
103 | if (docs.size === 1) {
104 | setIsSaved(true);
105 | setSaveDocID(docs.docs[0].id);
106 | }
107 | }
108 | checkForSaves();
109 |
110 | async function getCommentsCount() {
111 | const res = await firebase
112 | .firestore()
113 | .collection("tweets")
114 | .where("parentTweet", "==", tweet.id)
115 | .get();
116 | setComments(res.size);
117 | }
118 | getCommentsCount();
119 | if (user.uid === tweet.author.uid) {
120 | setMyTweet(true);
121 | }
122 | }
123 | setSaves((await fetchTweetSaves(localTweet.id)).size);
124 | }, []);
125 |
126 | return (
127 |
128 |
129 |
132 |
133 |
134 |
135 | {localTweet.author.name}
136 |
137 |
138 |
139 | @{localTweet.author.username}
140 |
141 |
142 | {localTweet.createdAt}
143 |
144 |
145 | {myTweet && (
146 |
{
149 | e.stopPropagation();
150 | const answer = confirm(
151 | "Are you sure you want to delete this tweet?"
152 | );
153 | if (answer) {
154 | deleteTweet(tweet.id);
155 | }
156 | }}>
157 |
158 |
159 | )}
160 |
161 |
162 |
163 | {localTweet.text}
164 |
165 | {tweet.imgLink && (
166 |
183 | )}
184 |
185 |
186 | {comments} Comments
187 |
188 |
189 | {likes} Likes
190 |
191 |
192 | {saves} Saved
193 |
194 |
195 |
196 |
197 |
198 |
206 | {isLiked ? (
207 |
219 | ) : (
220 |
232 | )}
233 | {isSaved ? (
234 |
246 | ) : (
247 |
259 | )}
260 |
261 |
262 | );
263 | };
264 |
265 | export default Post;
266 |
--------------------------------------------------------------------------------
/src/components/ProfileDropDown/ProfileDropDown.jsx:
--------------------------------------------------------------------------------
1 | import CircularProgress from "@material-ui/core/CircularProgress";
2 | import AccountCircleIcon from "@material-ui/icons/AccountCircle";
3 | import DeleteIcon from "@material-ui/icons/Delete";
4 | import ExitToAppIcon from "@material-ui/icons/ExitToApp";
5 | import Link from "next/link";
6 | import React, { useState } from "react";
7 | import { handleSignOut } from "../../services/Authentication";
8 | import { deleteAccount } from "../../services/DeleteAccount";
9 | import Avatar from "../Avatar/Avatar";
10 |
11 | const ProfileDropDown = ({ user }) => {
12 | const [dropdown, setDropdown] = useState(false);
13 | const [deletingAccount, setDeletingAccount] = useState(false);
14 | return (
15 |
16 |
17 |
18 |
19 |
47 |
48 |
49 | {dropdown && user && (
50 |
51 |
56 |
57 |
60 |
61 |
62 |
63 | My Profile
64 |
65 |
66 |
67 |
103 |
104 |
114 |
115 |
116 | )}
117 |
118 |
119 |
120 | );
121 | };
122 |
123 | export default ProfileDropDown;
124 |
--------------------------------------------------------------------------------
/src/components/Suggestions/Suggestions.jsx:
--------------------------------------------------------------------------------
1 | import CircularProgress from "@material-ui/core/CircularProgress";
2 | import { useContext, useEffect, useState } from "react";
3 | import UserContext from "../../context/UserContext";
4 | import { fetchUser, fetchUserFollowers } from "../../services/FetchData";
5 | import Avatar from "../Avatar/Avatar";
6 | import FollowButton from "../FollowButton/FollowButton";
7 |
8 | const Suggestions = ({ type, userID }) => {
9 | const [user, setUser] = useState(null);
10 | const [loading, setLoading] = useState(true);
11 | const { user: authUser } = useContext(UserContext);
12 |
13 | useEffect(async () => {
14 | if (authUser) {
15 | const localUser = await fetchUser({
16 | userID,
17 | });
18 | const followersCount = await fetchUserFollowers(localUser.uid);
19 | setUser({ ...localUser, followersCount: followersCount.size });
20 | setLoading(false);
21 | }
22 | }, [authUser]);
23 |
24 | return (
25 |
26 | {type === "relavant" ? (
27 |
28 | Relavant People
29 |
30 | ) : (
31 |
32 | Who to Follow
33 |
34 | )}
35 |
36 | {loading && (
37 |
38 |
39 |
40 | )}
41 | {user && (
42 |
43 |
44 |
47 |
48 |
{user.name}
49 |
50 | {user.followersCount} Followers
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 | {user.bio}
60 |
61 |
62 |
68 |

73 |
74 |
75 | )}
76 |
77 | );
78 | };
79 |
80 | export default Suggestions;
81 |
--------------------------------------------------------------------------------
/src/components/Trends/Trends.jsx:
--------------------------------------------------------------------------------
1 | const Trends = () => {
2 | return (
3 |
6 |
Trends
7 |
8 |
9 |
#programming
10 |
213k Tweets
11 |
12 |
13 |
#dev
14 |
3k Tweets
15 |
16 |
17 |
#monday
18 |
21.3k Tweets
19 |
20 |
21 |
#frontend
22 |
33k Tweets
23 |
24 |
25 |
#oneplus
26 |
213k Tweets
27 |
28 |
29 |
#programming
30 |
25k Tweets
31 |
32 |
33 |
#memes
34 |
213k Tweets
35 |
36 |
37 | );
38 | };
39 |
40 | export default Trends;
41 |
--------------------------------------------------------------------------------
/src/components/TweetInput/TweetInput.jsx:
--------------------------------------------------------------------------------
1 | import CircularProgress from "@material-ui/core/CircularProgress";
2 | import PhotoIcon from "@material-ui/icons/Photo";
3 | import React, { useContext, useState } from "react";
4 | import UserContext from "../../context/UserContext";
5 | import firebase from "../../firebase/init";
6 | import postTweet from "../../services/PostTweet";
7 | import Avatar from "../Avatar/Avatar";
8 |
9 | const TweetInput = () => {
10 | const { user } = useContext(UserContext);
11 | const [tweet, setTweet] = useState("");
12 | const [imgLink, setImgLink] = useState(null);
13 | const [file, setFile] = useState(null);
14 | const [tweeting, setTweeting] = useState(false);
15 |
16 | const fileInputRef = React.createRef();
17 |
18 | const uploadFile = async () => {
19 | const storageRef = firebase.storage().ref("tweets/" + file.name);
20 | const task = await storageRef.put(file);
21 | const link = await storageRef.getDownloadURL("tweets/" + file.name);
22 | return link;
23 | };
24 |
25 | return (
26 |
105 | );
106 | };
107 |
108 | export default TweetInput;
109 |
--------------------------------------------------------------------------------
/src/components/UserInfo/UserInfo.jsx:
--------------------------------------------------------------------------------
1 | import PersonAddIcon from "@material-ui/icons/PersonAdd";
2 | import { useContext, useState } from "react";
3 | import UserContext from "../../context/UserContext";
4 | import { useFollowers } from "../../hooks/useFollowers";
5 | import { useFollowings } from "../../hooks/useFollowings";
6 | import Avatar from "../Avatar/Avatar";
7 | import FollowButton from "../FollowButton/FollowButton";
8 | import Modal from "../Modal/Modal";
9 |
10 | const UserInfo = ({ fetchedUser }) => {
11 | const { user } = useContext(UserContext);
12 | const [isModalOpen, setIsModalOpen] = useState(false);
13 | const [isModalLoading, setIsModalLoading] = useState(true);
14 |
15 | const { followers, getFollowers } = useFollowers(fetchedUser.uid);
16 | const { followings, getFollowings } = useFollowings(fetchedUser.uid);
17 |
18 | return (
19 |
20 |
27 |
28 |
29 |
30 |
31 |
32 | {fetchedUser.name}
33 |
34 | {
37 | setIsModalOpen(true);
38 | await getFollowers();
39 | setIsModalLoading(false);
40 | }}>
41 | {fetchedUser.followersCount - 1}
42 | Followers
43 |
44 | getFollowings()}>
47 | {fetchedUser.followingsCount - 1}
48 | Following
49 |
50 |
51 | {user && fetchedUser.username === user.username ? (
52 |
60 | ) : (
61 |
62 |
63 |
64 | )}
65 |
66 |
67 | {fetchedUser.bio ? (
68 |
69 | {fetchedUser.bio}
70 |
71 | ) : (
72 |
73 | 404 Bio Not Found
74 |
75 | )}
76 |
77 |
78 | {isModalOpen && (
79 |
86 | setIsModalOpen(false)}
89 | loading={isModalLoading}
90 | />
91 |
92 | )}
93 |
94 | );
95 | };
96 |
97 | export default UserInfo;
98 |
--------------------------------------------------------------------------------
/src/context/BookmarksTweetsContext.js:
--------------------------------------------------------------------------------
1 | import { createContext } from "react";
2 |
3 | const BookmarksTweetsContext = createContext([]);
4 |
5 | export default BookmarksTweetsContext;
6 |
--------------------------------------------------------------------------------
/src/context/ExploreTweetsContext.js:
--------------------------------------------------------------------------------
1 | import { createContext } from "react";
2 |
3 | const ExploreTweetsContext = createContext([]);
4 |
5 | export default ExploreTweetsContext;
6 |
--------------------------------------------------------------------------------
/src/context/HomeTweetsContext.js:
--------------------------------------------------------------------------------
1 | import { createContext } from "react";
2 |
3 | const HomeTweetsContext = createContext([]);
4 |
5 | export default HomeTweetsContext;
6 |
--------------------------------------------------------------------------------
/src/context/UserContext.js:
--------------------------------------------------------------------------------
1 | const { createContext } = require("react");
2 |
3 | const UserContext = createContext();
4 |
5 | export default UserContext;
6 |
--------------------------------------------------------------------------------
/src/firebase/init.js:
--------------------------------------------------------------------------------
1 | import firebase from "firebase";
2 |
3 | const firebaseConfig = {
4 | apiKey: process.env.NEXT_PUBLIC_FIREBASE_FIRESTORE_API_KEY,
5 | authDomain: process.env.NEXT_PUBLIC_FIREBASE_FIRESTORE_AUTH_DOMAIN,
6 | databaseURL: process.env.NEXT_PUBLIC_FIREBASE_FIRESTORE_DATABASE_URL,
7 | projectId: process.env.NEXT_PUBLIC_FIREBASE_FIRESTORE_PROJECT_ID,
8 | storageBucket: process.env.NEXT_PUBLIC_FIREBASE_FIRESTORE_STORAGE_BUCKET,
9 | messagingSenderId:
10 | process.env.NEXT_PUBLIC_FIREBASE_FIRESTORE_MESSAGE_SENDER_ID,
11 | appId: process.env.NEXT_PUBLIC_FIREBASE_FIRESTORE_APP_ID,
12 | };
13 |
14 | !firebase.apps.length && firebase.initializeApp(firebaseConfig);
15 | export default firebase;
16 |
--------------------------------------------------------------------------------
/src/hooks/useFollowers.js:
--------------------------------------------------------------------------------
1 | import React, { useContext } from "react";
2 | import UserContext from "../context/UserContext";
3 | import { fetchUser, fetchUserFollowers } from "../services/FetchData";
4 |
5 | export function useFollowers(userId, authUserID) {
6 | const { user } = useContext(UserContext);
7 | const [followers, setFollowers] = React.useState([]);
8 | const [isFollowersLoading, setIsFollowersLoading] = React.useState(true);
9 |
10 | const getFollowers = async () => {
11 | const data = [];
12 | setIsFollowersLoading(true);
13 | if (user) {
14 | const followersSnapShot = await fetchUserFollowers(userId, authUserID);
15 | for (let i = 0; i < followersSnapShot.size; i++) {
16 | data.push(
17 | await fetchUser({
18 | userID: followersSnapShot.docs[i].data().followerID,
19 | })
20 | );
21 | }
22 | }
23 | setFollowers(data);
24 | setIsFollowersLoading(false);
25 | };
26 |
27 | return { followers, isFollowersLoading, getFollowers };
28 | }
29 |
--------------------------------------------------------------------------------
/src/hooks/useFollowings.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { fetchUser, fetchUserFollowings } from "../services/FetchData";
3 |
4 | export function useFollowings(userId) {
5 | const [followings, setFollowings] = React.useState([]);
6 | const [isFollowingsLoading, setIsFollowingsLoading] = React.useState(true);
7 |
8 | const getFollowings = async () => {
9 | const data = [];
10 | setIsFollowingsLoading(true);
11 | const followersSnapShot = await fetchUserFollowings(userId);
12 | for (let i = 0; i < followersSnapShot.size; i++) {
13 | data.push(
14 | await fetchUser({
15 | userID: followersSnapShot.docs[i].data().followeeID,
16 | })
17 | );
18 | }
19 | setFollowings(data);
20 | setIsFollowingsLoading(false);
21 | };
22 |
23 | return { followings, isFollowingsLoading, getFollowings };
24 | }
25 |
--------------------------------------------------------------------------------
/src/layouts/index.jsx:
--------------------------------------------------------------------------------
1 | import Footer from "../components/Footer/Footer";
2 | import NavBar from "../components/NavBar/NavBar";
3 |
4 | const Layout = (props) => {
5 | return (
6 | <>
7 |
8 | {props.children}
9 |
10 | >
11 | );
12 | };
13 |
14 | export default Layout;
15 |
--------------------------------------------------------------------------------
/src/pages/[username]/index.jsx:
--------------------------------------------------------------------------------
1 | import Head from "next/head";
2 | import Link from "next/link";
3 | import { useEffect, useState } from "react";
4 | import Layout from "../..//layouts/index";
5 | import Banner from "../../components/Banner/Banner";
6 | import Filters from "../../components/Filters/Filters";
7 | import Post from "../../components/Post/Post";
8 | import UserInfo from "../../components/UserInfo/UserInfo";
9 | import fetchAllUserData from "../../services/FetchData";
10 |
11 | const UserName = ({ fetchedUser, tweets }) => {
12 | const [userExits, setUserExits] = useState(false);
13 |
14 | useEffect(() => {
15 | if (fetchedUser) {
16 | setUserExits(true);
17 | }
18 | }, []);
19 |
20 | return (
21 |
22 |
23 |
24 | {userExits
25 | ? `${fetchedUser.name} (@${fetchedUser.username}) | Tweeter`
26 | : `USER NOT FOUND`}
27 |
28 |
29 |
30 |
31 | {userExits ? (
32 | <>
33 |
34 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 | {tweets.map((tweet) => {
48 | return (
49 |
50 |
52 |
55 |
56 |
57 | );
58 | })}
59 |
60 |
61 |
62 | >
63 | ) : (
64 | User Not Found
65 | )}
66 |
67 |
68 | );
69 | };
70 |
71 | export async function getServerSideProps(context) {
72 | const userInfo = await fetchAllUserData(context.params.username);
73 |
74 | return {
75 | props: {
76 | ...userInfo,
77 | },
78 | };
79 | }
80 |
81 | export default UserName;
82 |
--------------------------------------------------------------------------------
/src/pages/[username]/status/[tweetId].jsx:
--------------------------------------------------------------------------------
1 | import Head from "next/head";
2 | import React, { useContext } from "react";
3 | import CommentInput from "../../../components/CommentInput/CommentInput";
4 | import Comments from "../../../components/Comments/Comments";
5 | import Post from "../../../components/Post/Post";
6 | import Suggestions from "../../../components/Suggestions/Suggestions";
7 | import UserContext from "../../../context/UserContext";
8 | import firebase from "../../../firebase/init";
9 | import Layout from "../../../layouts";
10 | import { fetchUser } from "../../../services/FetchData";
11 |
12 | const Tweet = ({ tweet }) => {
13 | const { user } = useContext(UserContext);
14 |
15 | return (
16 |
17 |
18 |
19 | {tweet.author.name} on Tweeter "{tweet.text}"
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 | {user &&
}
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 | );
38 | };
39 |
40 | export async function getServerSideProps(context) {
41 | const tweetID = context.params.tweetId;
42 | const res = await firebase
43 | .firestore()
44 | .collection("tweets")
45 | .doc(tweetID)
46 | .get();
47 | const tweet = res.data();
48 | const id = res.id;
49 | const user = await fetchUser({ userID: tweet.authorId });
50 |
51 | return {
52 | props: {
53 | tweet: {
54 | ...tweet,
55 | createdAt: tweet.createdAt.toDate().toString(),
56 | id,
57 | author: user,
58 | },
59 | },
60 | };
61 | }
62 |
63 | export default Tweet;
64 |
--------------------------------------------------------------------------------
/src/pages/_app.jsx:
--------------------------------------------------------------------------------
1 | import { useRouter } from "next/router";
2 | import { useEffect, useState } from "react";
3 | import BookmarksTweetsContext from "../context/BookmarksTweetsContext";
4 | import ExploreTweetsContext from "../context/ExploreTweetsContext";
5 | import HomeTweetsContext from "../context/HomeTweetsContext";
6 | import UserContext from "../context/UserContext";
7 | import firebase from "../firebase/init";
8 | import "../styles/global.css";
9 | import "../styles/reset.css";
10 |
11 | const db = firebase.firestore();
12 |
13 | function MyApp({ Component, pageProps }) {
14 | const Router = useRouter();
15 | const protectedRoutes = ["/home", "/bookmarks"];
16 |
17 | const [user, setUser] = useState(null);
18 | const [homeTweetsContext, setHomeTweetsContext] = useState(null);
19 | const [exploreTweetsContext, setExploreTweetsContext] = useState(null);
20 | const [bookmarksTweetsContext, setBookmarksTweetsContext] = useState(null);
21 |
22 | useEffect(() => {
23 | async function getCurrentUser(userID) {
24 | const user = await db.collection("users").doc(userID).get();
25 | setUser({ ...user.data(), uid: userID });
26 | }
27 |
28 | firebase.auth().onAuthStateChanged((loggedUser) => {
29 | if (!loggedUser) {
30 | if (protectedRoutes.includes(Router.pathname)) Router.push("/");
31 | setUser(null);
32 | } else {
33 | getCurrentUser(loggedUser.uid);
34 | }
35 | });
36 | }, []);
37 |
38 | return (
39 | <>
40 | Tweeter
41 |
42 |
44 |
46 |
48 |
49 |
50 |
51 |
52 |
53 | >
54 | );
55 | }
56 |
57 | export default MyApp;
58 |
--------------------------------------------------------------------------------
/src/pages/_document.jsx:
--------------------------------------------------------------------------------
1 | import Document, { Head, Html, Main, NextScript } from "next/document";
2 |
3 | class MyDocument extends Document {
4 | static async getInitialProps(ctx) {
5 | const initialProps = await Document.getInitialProps(ctx);
6 | return { ...initialProps };
7 | }
8 |
9 | render() {
10 | return (
11 |
12 |
13 |
18 |
19 |
20 |
21 |
22 |
23 |
24 | );
25 | }
26 | }
27 |
28 | export default MyDocument;
29 |
--------------------------------------------------------------------------------
/src/pages/bookmarks.jsx:
--------------------------------------------------------------------------------
1 | import CircularProgress from "@material-ui/core/CircularProgress";
2 | import Head from "next/head";
3 | import Link from "next/link";
4 | import { useContext, useEffect, useState } from "react";
5 | import Filters from "../components/Filters/Filters";
6 | import Post from "../components/Post/Post";
7 | import BookmarksTweetsContext from "../context/BookmarksTweetsContext";
8 | import UserContext from "../context/UserContext";
9 | import firebase from "../firebase/init";
10 | import Layout from "../layouts";
11 | import { fetchTweet } from "../services/FetchData";
12 |
13 | const Bookmarks = () => {
14 | const { user } = useContext(UserContext);
15 | const [bookmarkTweets, setBookmarkTweets] = useState([]);
16 | const [loading, setIsLoading] = useState(true);
17 | const [isEmpty, setIsEmpty] = useState(false);
18 | const { bookmarksTweetsContext, setBookmarksTweetsContext } = useContext(
19 | BookmarksTweetsContext
20 | );
21 |
22 | useEffect(() => {
23 | if (user) {
24 | if (!bookmarksTweetsContext) {
25 | async function getSavedTweets() {
26 | const localBMTweets = [];
27 | const savesSnapShot = await firebase
28 | .firestore()
29 | .collection("saves")
30 | .where("userID", "==", user.uid)
31 | .get();
32 |
33 | if (savesSnapShot.empty) {
34 | setIsEmpty(true);
35 | setBookmarkTweets([]);
36 | setIsLoading(false);
37 | } else {
38 | for (let i = 0; i < savesSnapShot.size; i++) {
39 | const tweet = await fetchTweet(
40 | savesSnapShot.docs[i].data().tweetID
41 | );
42 | localBMTweets.push(tweet);
43 | }
44 | setBookmarkTweets(localBMTweets);
45 | setBookmarksTweetsContext(localBMTweets);
46 | setIsEmpty(false);
47 | setIsLoading(false);
48 | }
49 | }
50 | getSavedTweets(user.uid);
51 | } else {
52 | setBookmarkTweets(bookmarksTweetsContext);
53 | setIsEmpty(false);
54 | setIsLoading(false);
55 | }
56 | }
57 | }, [user]);
58 |
59 | return (
60 |
61 |
62 |
Bookmarks | Tweeter
63 |
64 |
65 |
66 |
67 |
72 |
73 | {loading && (
74 |
75 |
76 |
77 | )}
78 | {isEmpty ? (
79 |
You have no Saved Tweets
80 | ) : (
81 | bookmarkTweets.map((tweet) => (
82 |
83 |
86 |
87 | ))
88 | )}
89 |
90 |
91 |
92 |
93 |
94 | );
95 | };
96 |
97 | export default Bookmarks;
98 |
--------------------------------------------------------------------------------
/src/pages/explore.jsx:
--------------------------------------------------------------------------------
1 | import CircularProgress from "@material-ui/core/CircularProgress";
2 | import Head from "next/head";
3 | import Link from "next/link";
4 | import React, { useEffect, useState } from "react";
5 | import ExploreFilters from "../components/ExploreFIlters/ExploreFilters";
6 | import Post from "../components/Post/Post";
7 | import ExploreTweetsContext from "../context/ExploreTweetsContext";
8 | import firebase from "../firebase/init";
9 | import Layout from "../layouts";
10 | import { fetchUser } from "../services/FetchData";
11 |
12 | const Explore = () => {
13 | const [exploreTweets, setExploreTweets] = useState([]);
14 | const { exploreTweetsContext, setExploreTweetsContext } = React.useContext(
15 | ExploreTweetsContext
16 | );
17 |
18 | useEffect(async () => {
19 | if (!exploreTweetsContext) {
20 | firebase
21 | .firestore()
22 | .collection("tweets")
23 | .limit(5)
24 | .onSnapshot(async (tweetRef) => {
25 | const exploreUserTweets = [];
26 |
27 | for (let i = 0; i < tweetRef.size; i++) {
28 | const userInfo = await fetchUser({
29 | userID: tweetRef.docs[i].data().authorId,
30 | });
31 | let data = tweetRef.docs[i].data();
32 |
33 | exploreUserTweets.push({
34 | ...data,
35 | createdAt: data.createdAt.toDate().toString(),
36 | id: tweetRef.docs[i].id,
37 | author: userInfo,
38 | });
39 | }
40 | setExploreTweets(exploreUserTweets);
41 | setExploreTweetsContext(exploreUserTweets);
42 | });
43 | } else {
44 | setExploreTweets(exploreTweetsContext);
45 | }
46 | }, []);
47 |
48 | return (
49 |
50 |
51 |
Explore | Tweeter
52 |
53 |
54 |
55 |
56 |
61 |
62 | {exploreTweets && exploreTweets.length > 0 ? (
63 | exploreTweets.map((tweet) => (
64 |
65 |
66 |
69 |
70 |
71 | ))
72 | ) : (
73 |
74 |
75 |
76 | )}
77 |
78 |
79 |
80 |
81 |
82 | );
83 | };
84 |
85 | export default Explore;
86 |
--------------------------------------------------------------------------------
/src/pages/home.jsx:
--------------------------------------------------------------------------------
1 | import CircularProgress from "@material-ui/core/CircularProgress";
2 | import Head from "next/head";
3 | import Link from "next/link";
4 | import { useContext, useEffect, useState } from "react";
5 | import Post from "../components/Post/Post";
6 | import Suggestions from "../components/Suggestions/Suggestions";
7 | import Trends from "../components/Trends/Trends";
8 | import TweetInput from "../components/TweetInput/TweetInput";
9 | import HomeTweetsContext from "../context/HomeTweetsContext";
10 | import UserContext from "../context/UserContext";
11 | import firebase from "../firebase/init";
12 | import Layout from "../layouts";
13 | import { fetchUser } from "../services/FetchData";
14 |
15 | const Home = () => {
16 | const { user } = useContext(UserContext);
17 | const [homeTweets, setHomeTweets] = useState([]);
18 | const [loading, setLoading] = useState(true);
19 | const [isEmpty, setIsEmpty] = useState(false);
20 |
21 | const { homeTweetsContext, setHomeTweetsContext } = useContext(
22 | HomeTweetsContext
23 | );
24 |
25 | useEffect(async () => {
26 | try {
27 | if (user) {
28 | if (!homeTweetsContext) {
29 | setLoading(true);
30 | const connectionsRef = await firebase
31 | .firestore()
32 | .collection("connections")
33 | .where("followerID", "==", user.uid)
34 | .get();
35 |
36 | if (connectionsRef.empty) {
37 | setIsEmpty(true);
38 | setHomeTweets([]);
39 | setLoading(false);
40 | } else {
41 | const followerIDs = connectionsRef.docs.map((connection) => {
42 | const floID = connection.data().followeeID;
43 | return floID;
44 | });
45 |
46 | firebase
47 | .firestore()
48 | .collection("tweets")
49 | .where("authorId", "in", followerIDs)
50 | .where("parentTweet", "==", null)
51 | .orderBy("createdAt", "desc")
52 | .onSnapshot(async (tweetRef) => {
53 | const homeUserTweets = [];
54 |
55 | for (let i = 0; i < tweetRef.size; i++) {
56 | const userInfo = await fetchUser({
57 | userID: tweetRef.docs[i].data().authorId,
58 | });
59 | let data = tweetRef.docs[i].data();
60 |
61 | homeUserTweets.push({
62 | ...data,
63 | createdAt: data.createdAt.toDate().toString(),
64 | id: tweetRef.docs[i].id,
65 | author: userInfo,
66 | });
67 | }
68 | setHomeTweets(homeUserTweets);
69 | setLoading(false);
70 | setHomeTweetsContext(homeUserTweets);
71 | });
72 | }
73 | } else {
74 | setLoading(false);
75 | setHomeTweets(homeTweetsContext);
76 | }
77 | }
78 | } catch (err) {
79 | console.log(err);
80 | }
81 | }, [user]);
82 |
83 | return (
84 |
85 |
86 |
Home | Tweeter
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 | {loading && (
97 |
98 |
99 |
100 | )}
101 |
102 | {isEmpty ? (
103 |
You are following no one
104 | ) : (
105 | homeTweets.map((tweet) => (
106 |
107 |
108 |
111 |
112 |
113 | ))
114 | )}
115 |
116 |
117 |
118 |
119 |
120 |
121 | {user && }
122 |
123 |
124 |
125 |
126 |
127 |
128 | );
129 | };
130 |
131 | export default Home;
132 |
--------------------------------------------------------------------------------
/src/pages/index.jsx:
--------------------------------------------------------------------------------
1 | import AuthForm from "../components/AuthForm/AuthForm";
2 |
3 | const Home = () => {
4 | return (
5 |
6 |
7 |
8 |
9 |
10 | See what's happening!
11 |
12 |
13 |
14 |
15 |
16 |
17 |

18 |
19 |
22 |
23 |
24 |
25 |
26 | );
27 | };
28 | export default Home;
29 |
--------------------------------------------------------------------------------
/src/pages/login.jsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link";
2 | import React from "react";
3 | import AuthForm from "../components/AuthForm/AuthForm";
4 |
5 | const SignUp = () => {
6 | return (
7 |
8 |
9 |
10 |
11 |

16 |
17 |
18 |
21 |
22 |
23 | );
24 | };
25 |
26 | export default SignUp;
27 |
--------------------------------------------------------------------------------
/src/pages/signup.jsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link";
2 | import React from "react";
3 | import AuthForm from "../components/AuthForm/AuthForm";
4 |
5 | const SignUp = () => {
6 | return (
7 |
8 |
9 |
10 |
11 |

16 |
17 |
18 |
21 |
22 |
23 | );
24 | };
25 |
26 | export default SignUp;
27 |
--------------------------------------------------------------------------------
/src/services/Authentication.js:
--------------------------------------------------------------------------------
1 | import firebase from "../firebase/init";
2 |
3 | async function handleSignUp(email, password, username, name) {
4 | try {
5 | const { user } = await firebase
6 | .auth()
7 | .createUserWithEmailAndPassword(email, password);
8 | await firebase.firestore().collection("users").doc(user.uid).set({
9 | username,
10 | email: user.email,
11 | profilePicture:
12 | "https://firebasestorage.googleapis.com/v0/b/tweeter-45929.appspot.com/o/defaultAvatar.jpg?alt=media&token=072d3268-84f4-4016-b0f7-d440930347f2",
13 | name,
14 | bio: null,
15 | });
16 |
17 | await firebase.firestore().collection("connections").add({
18 | followerID: user.uid,
19 | followeeID: user.uid,
20 | });
21 |
22 | window.location.replace("/home");
23 | return user;
24 | } catch (error) {
25 | console.error(error);
26 | return error;
27 | }
28 | }
29 |
30 | async function handleSignIn(email, password) {
31 | try {
32 | const user = await firebase
33 | .auth()
34 | .signInWithEmailAndPassword(email, password);
35 | window.location.replace("/home");
36 | } catch (error) {
37 | console.error(error);
38 | return error;
39 | }
40 | }
41 |
42 | async function handleSignOut() {
43 | try {
44 | await firebase.auth().signOut();
45 | window.location.replace("/");
46 | return true;
47 | } catch (error) {
48 | console.error(error);
49 |
50 | return error;
51 | }
52 | }
53 | export { handleSignIn, handleSignUp, handleSignOut };
54 |
--------------------------------------------------------------------------------
/src/services/DeleteAccount.js:
--------------------------------------------------------------------------------
1 | import firebase from "../firebase/init";
2 | import { deleteTweet } from "./DeleteTweet";
3 |
4 | const db = firebase.firestore();
5 |
6 | export const deleteAccount = async (userID) => {
7 | // Delete user from firebase authentication
8 |
9 | // Delete user doc from "users" collection
10 | db.collection("users")
11 | .doc(userID)
12 | .delete()
13 | .then(console.log("Deleted User Doc"))
14 | .catch((e) => console.log(e));
15 |
16 | // Get all tweets that has authorID = user.uid in "tweets" collection
17 | const tweetsSnapShot = await db
18 | .collection("tweets")
19 | .where("authorId", "==", userID)
20 | .get();
21 | tweetsSnapShot.forEach((tweetsDoc) => {
22 | // Delete those tweets
23 | deleteTweet(tweetsDoc.id);
24 | });
25 |
26 | // Delete all connections
27 | await db
28 | .collection("connections")
29 | .where("followerID", "==", userID)
30 | .where("followeeID", "==", userID)
31 | .get()
32 | .then((connectionsSnapShot) => {
33 | const batch = firebase.firestore().batch();
34 | connectionsSnapShot.forEach((connectionDocRef) => {
35 | batch.delete(connectionDocRef.ref);
36 | });
37 | batch
38 | .commit()
39 | .then(console.log("Deleted Connections"))
40 | .catch((e) => console.log(e));
41 | });
42 |
43 | firebase
44 | .auth()
45 | .currentUser.delete()
46 | .then(() => console.log("Deleted user from auth"))
47 | .catch((e) => console.log(e));
48 | };
49 |
--------------------------------------------------------------------------------
/src/services/DeleteTweet.js:
--------------------------------------------------------------------------------
1 | import firebase from "../firebase/init";
2 |
3 | const db = firebase.firestore();
4 |
5 | export const deleteTweet = (tweetID) => {
6 | db.collection("tweets")
7 | .doc(tweetID)
8 | .delete()
9 | .then(() => console.log("Deleted Tweet"))
10 | .catch((e) => console.log(e));
11 |
12 | db.collection("likes")
13 | .where("tweetID", "==", tweetID)
14 | .get()
15 | .then((tweetsSnapShot) => {
16 | const batch = firebase.firestore().batch();
17 | tweetsSnapShot.forEach((tweetDocRef) => {
18 | batch.delete(tweetDocRef.ref);
19 | });
20 | batch
21 | .commit()
22 | .then(console.log("Deleted Likes"))
23 | .catch((e) => console.log(e));
24 | });
25 |
26 | db.collection("saves")
27 | .where("tweetID", "==", tweetID)
28 | .get()
29 | .then((tweetsSnapShot) => {
30 | const batch = firebase.firestore().batch();
31 | tweetsSnapShot.forEach((tweetDocRef) => {
32 | batch.delete(tweetDocRef.ref);
33 | });
34 | batch
35 | .commit()
36 | .then(console.log("Deleted Saves"))
37 | .catch((e) => console.log(e));
38 | });
39 | };
40 |
--------------------------------------------------------------------------------
/src/services/FetchData.js:
--------------------------------------------------------------------------------
1 | import firebase from "../firebase/init";
2 |
3 | const db = firebase.firestore();
4 |
5 | export const fetchUser = async ({ username, userID }) => {
6 | let userQuerySnapShot;
7 |
8 | if (username) {
9 | userQuerySnapShot = await db
10 | .collection("users")
11 | .where("username", "==", username)
12 | .get();
13 | if (userQuerySnapShot.empty) {
14 | return null;
15 | }
16 | return {
17 | ...userQuerySnapShot.docs[0].data(),
18 | uid: userQuerySnapShot.docs[0].id,
19 | };
20 | }
21 |
22 | if (userID) {
23 | userQuerySnapShot = await db.collection("users").doc(userID).get();
24 | if (userQuerySnapShot.exists) {
25 | return {
26 | ...userQuerySnapShot.data(),
27 | uid: userQuerySnapShot.id,
28 | };
29 | } else {
30 | return null;
31 | }
32 | }
33 | };
34 |
35 | export const fetchUserTweets = async (userID) => {
36 | const tweetsQuerySnapShot = await db
37 | .collection("tweets")
38 | .where("authorId", "==", userID)
39 | .where("parentTweet", "==", null)
40 | .get();
41 |
42 | const fetchedUser = await fetchUser({ userID });
43 |
44 | // tweets = tweets Array of Objects
45 | const tweets = tweetsQuerySnapShot.docs.map((tweet) => {
46 | const data = tweet.data();
47 |
48 | return {
49 | id: tweet.id,
50 | ...data,
51 | author: fetchedUser,
52 | createdAt: data.createdAt.toDate().toString(),
53 | };
54 | });
55 | // returns array of objects (tweets)
56 | return tweets;
57 | };
58 |
59 | export const fetchTweet = async (tweetID) => {
60 | const tweet = await firebase
61 | .firestore()
62 | .collection("tweets")
63 | .doc(tweetID)
64 | .get();
65 |
66 | if (!tweet.exists) return null;
67 | const user = await fetchUser({ userID: tweet.data().authorId });
68 | return {
69 | ...tweet.data(),
70 | author: user,
71 | id: tweetID,
72 | createdAt: tweet.data().createdAt.toDate().toString(),
73 | };
74 | };
75 |
76 | export const fetchUserFollowers = async (userID) => {
77 | return await db
78 | .collection("connections")
79 | .where("followeeID", "==", userID)
80 | .get();
81 | };
82 |
83 | export const fetchUserFollowings = async (userID) => {
84 | return await db
85 | .collection("connections")
86 | .where("followerID", "==", userID)
87 | .get();
88 | };
89 |
90 | export const fetchTweetLikes = (tweetID) => {
91 | return firebase
92 | .firestore()
93 | .collection("likes")
94 | .where("tweetID", "==", tweetID)
95 | .get();
96 | };
97 |
98 | export const fetchTweetSaves = (tweetID) => {
99 | return firebase
100 | .firestore()
101 | .collection("saves")
102 | .where("tweetID", "==", tweetID)
103 | .get();
104 | };
105 |
106 | const fetchAllUserData = async (username) => {
107 | let fetchedUser = await fetchUser({ username });
108 | if (fetchedUser === null) {
109 | return null;
110 | }
111 | const tweets = await fetchUserTweets(fetchedUser.uid);
112 | const followersCount = (await fetchUserFollowers(fetchedUser.uid)).size;
113 | const followingsCount = (await fetchUserFollowings(fetchedUser.uid)).size;
114 | fetchedUser = {
115 | ...fetchedUser,
116 | followersCount,
117 | followingsCount,
118 | };
119 |
120 | return {
121 | fetchedUser,
122 | tweets,
123 | };
124 | };
125 |
126 | export default fetchAllUserData;
127 |
--------------------------------------------------------------------------------
/src/services/PostTweet.js:
--------------------------------------------------------------------------------
1 | import firebase from "../firebase/init";
2 |
3 | const postTweet = async (
4 | authorId,
5 | text,
6 | imgLink = null,
7 | parentTweet = null
8 | ) => {
9 | console.log(authorId, text, imgLink, parentTweet);
10 | await firebase.firestore().collection("tweets").add({
11 | authorId,
12 | text,
13 | parentTweet,
14 | imgLink,
15 | createdAt: firebase.firestore.Timestamp.now(),
16 | });
17 | };
18 |
19 | export default postTweet;
20 |
--------------------------------------------------------------------------------
/src/styles/global.css:
--------------------------------------------------------------------------------
1 | @import url("https://fonts.googleapis.com/css2?family=Poppins:wght@200;300;400;500;600&display=swap");
2 |
3 | @import url("https://fonts.googleapis.com/css2?family=Noto+Sans:wght@400;700&display=swap");
4 | /* purgecss start ignore */
5 | @tailwind base;
6 | @tailwind components;
7 | /* purgecss end ignore */
8 |
9 | @tailwind utilities;
10 |
--------------------------------------------------------------------------------
/src/styles/reset.css:
--------------------------------------------------------------------------------
1 | /*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */
2 |
3 | /* Document
4 | ========================================================================== */
5 |
6 | /**
7 | * 1. Correct the line height in all browsers.
8 | * 2. Prevent adjustments of font size after orientation changes in iOS.
9 | */
10 |
11 | html {
12 | line-height: 1.15; /* 1 */
13 | -webkit-text-size-adjust: 100%; /* 2 */
14 | }
15 |
16 | /* Sections
17 | ========================================================================== */
18 |
19 | /**
20 | * Remove the margin in all browsers.
21 | */
22 |
23 | body {
24 | margin: 0;
25 | }
26 |
27 | /**
28 | * Render the `main` element consistently in IE.
29 | */
30 |
31 | main {
32 | display: block;
33 | }
34 |
35 | /**
36 | * Correct the font size and margin on `h1` elements within `section` and
37 | * `article` contexts in Chrome, Firefox, and Safari.
38 | */
39 |
40 | h1 {
41 | font-size: 2em;
42 | margin: 0.67em 0;
43 | }
44 |
45 | /* Grouping content
46 | ========================================================================== */
47 |
48 | /**
49 | * 1. Add the correct box sizing in Firefox.
50 | * 2. Show the overflow in Edge and IE.
51 | */
52 |
53 | hr {
54 | box-sizing: content-box; /* 1 */
55 | height: 0; /* 1 */
56 | overflow: visible; /* 2 */
57 | }
58 |
59 | /**
60 | * 1. Correct the inheritance and scaling of font size in all browsers.
61 | * 2. Correct the odd `em` font sizing in all browsers.
62 | */
63 |
64 | pre {
65 | font-family: monospace, monospace; /* 1 */
66 | font-size: 1em; /* 2 */
67 | }
68 |
69 | /* Text-level semantics
70 | ========================================================================== */
71 |
72 | /**
73 | * Remove the gray background on active links in IE 10.
74 | */
75 |
76 | a {
77 | background-color: transparent;
78 | }
79 |
80 | /**
81 | * 1. Remove the bottom border in Chrome 57-
82 | * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.
83 | */
84 |
85 | abbr[title] {
86 | border-bottom: none; /* 1 */
87 | text-decoration: underline; /* 2 */
88 | text-decoration: underline dotted; /* 2 */
89 | }
90 |
91 | /**
92 | * Add the correct font weight in Chrome, Edge, and Safari.
93 | */
94 |
95 | b,
96 | strong {
97 | font-weight: bolder;
98 | }
99 |
100 | /**
101 | * 1. Correct the inheritance and scaling of font size in all browsers.
102 | * 2. Correct the odd `em` font sizing in all browsers.
103 | */
104 |
105 | code,
106 | kbd,
107 | samp {
108 | font-family: monospace, monospace; /* 1 */
109 | font-size: 1em; /* 2 */
110 | }
111 |
112 | /**
113 | * Add the correct font size in all browsers.
114 | */
115 |
116 | small {
117 | font-size: 80%;
118 | }
119 |
120 | /**
121 | * Prevent `sub` and `sup` elements from affecting the line height in
122 | * all browsers.
123 | */
124 |
125 | sub,
126 | sup {
127 | font-size: 75%;
128 | line-height: 0;
129 | position: relative;
130 | vertical-align: baseline;
131 | }
132 |
133 | sub {
134 | bottom: -0.25em;
135 | }
136 |
137 | sup {
138 | top: -0.5em;
139 | }
140 |
141 | /* Embedded content
142 | ========================================================================== */
143 |
144 | /**
145 | * Remove the border on images inside links in IE 10.
146 | */
147 |
148 | img {
149 | border-style: none;
150 | }
151 |
152 | /* Forms
153 | ========================================================================== */
154 |
155 | /**
156 | * 1. Change the font styles in all browsers.
157 | * 2. Remove the margin in Firefox and Safari.
158 | */
159 |
160 | button,
161 | input,
162 | optgroup,
163 | select,
164 | textarea {
165 | font-family: inherit; /* 1 */
166 | font-size: 100%; /* 1 */
167 | line-height: 1.15; /* 1 */
168 | margin: 0; /* 2 */
169 | }
170 |
171 | /**
172 | * Show the overflow in IE.
173 | * 1. Show the overflow in Edge.
174 | */
175 |
176 | button,
177 | input {
178 | /* 1 */
179 | overflow: visible;
180 | }
181 |
182 | /**
183 | * Remove the inheritance of text transform in Edge, Firefox, and IE.
184 | * 1. Remove the inheritance of text transform in Firefox.
185 | */
186 |
187 | button,
188 | select {
189 | /* 1 */
190 | text-transform: none;
191 | }
192 |
193 | /**
194 | * Correct the inability to style clickable types in iOS and Safari.
195 | */
196 |
197 | button,
198 | [type="button"],
199 | [type="reset"],
200 | [type="submit"] {
201 | -webkit-appearance: button;
202 | }
203 |
204 | /**
205 | * Remove the inner border and padding in Firefox.
206 | */
207 |
208 | button::-moz-focus-inner,
209 | [type="button"]::-moz-focus-inner,
210 | [type="reset"]::-moz-focus-inner,
211 | [type="submit"]::-moz-focus-inner {
212 | border-style: none;
213 | padding: 0;
214 | }
215 |
216 | /**
217 | * Restore the focus styles unset by the previous rule.
218 | */
219 |
220 | button:-moz-focusring,
221 | [type="button"]:-moz-focusring,
222 | [type="reset"]:-moz-focusring,
223 | [type="submit"]:-moz-focusring {
224 | outline: 1px dotted ButtonText;
225 | }
226 |
227 | /**
228 | * Correct the padding in Firefox.
229 | */
230 |
231 | fieldset {
232 | padding: 0.35em 0.75em 0.625em;
233 | }
234 |
235 | /**
236 | * 1. Correct the text wrapping in Edge and IE.
237 | * 2. Correct the color inheritance from `fieldset` elements in IE.
238 | * 3. Remove the padding so developers are not caught out when they zero out
239 | * `fieldset` elements in all browsers.
240 | */
241 |
242 | legend {
243 | box-sizing: border-box; /* 1 */
244 | color: inherit; /* 2 */
245 | display: table; /* 1 */
246 | max-width: 100%; /* 1 */
247 | padding: 0; /* 3 */
248 | white-space: normal; /* 1 */
249 | }
250 |
251 | /**
252 | * Add the correct vertical alignment in Chrome, Firefox, and Opera.
253 | */
254 |
255 | progress {
256 | vertical-align: baseline;
257 | }
258 |
259 | /**
260 | * Remove the default vertical scrollbar in IE 10+.
261 | */
262 |
263 | textarea {
264 | overflow: auto;
265 | }
266 |
267 | /**
268 | * 1. Add the correct box sizing in IE 10.
269 | * 2. Remove the padding in IE 10.
270 | */
271 |
272 | [type="checkbox"],
273 | [type="radio"] {
274 | box-sizing: border-box; /* 1 */
275 | padding: 0; /* 2 */
276 | }
277 |
278 | /**
279 | * Correct the cursor style of increment and decrement buttons in Chrome.
280 | */
281 |
282 | [type="number"]::-webkit-inner-spin-button,
283 | [type="number"]::-webkit-outer-spin-button {
284 | height: auto;
285 | }
286 |
287 | /**
288 | * 1. Correct the odd appearance in Chrome and Safari.
289 | * 2. Correct the outline style in Safari.
290 | */
291 |
292 | [type="search"] {
293 | -webkit-appearance: textfield; /* 1 */
294 | outline-offset: -2px; /* 2 */
295 | }
296 |
297 | /**
298 | * Remove the inner padding in Chrome and Safari on macOS.
299 | */
300 |
301 | [type="search"]::-webkit-search-decoration {
302 | -webkit-appearance: none;
303 | }
304 |
305 | /**
306 | * 1. Correct the inability to style clickable types in iOS and Safari.
307 | * 2. Change font properties to `inherit` in Safari.
308 | */
309 |
310 | ::-webkit-file-upload-button {
311 | -webkit-appearance: button; /* 1 */
312 | font: inherit; /* 2 */
313 | }
314 |
315 | /* Interactive
316 | ========================================================================== */
317 |
318 | /*
319 | * Add the correct display in Edge, IE 10+, and Firefox.
320 | */
321 |
322 | details {
323 | display: block;
324 | }
325 |
326 | /*
327 | * Add the correct display in all browsers.
328 | */
329 |
330 | summary {
331 | display: list-item;
332 | }
333 |
334 | /* Misc
335 | ========================================================================== */
336 |
337 | /**
338 | * Add the correct display in IE 10+.
339 | */
340 |
341 | template {
342 | display: none;
343 | }
344 |
345 | /**
346 | * Add the correct display in IE 10.
347 | */
348 |
349 | [hidden] {
350 | display: none;
351 | }
352 |
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | theme: {
3 | fontFamily: {
4 | poppins: ["Poppins"],
5 | noto: ["Noto Sans"],
6 | },
7 | extend: {
8 | colors: {
9 | primary: "#2F80ED",
10 | secondary: "#828282",
11 | },
12 | backgroundImage: (theme) => ({
13 | banner: "url('/images/banner.jpg')",
14 | logo:"url('/images/logos/tweeter-small.svg')"
15 | }),
16 | },
17 | },
18 | };
19 |
--------------------------------------------------------------------------------