Make app work with Preact with Firebase in dev

This commit is contained in:
Alex Erdei 2025-01-20 20:35:44 +00:00
parent c4e85898ba
commit a943e53ed0
80 changed files with 8004 additions and 1963 deletions

5
.firebaserc Normal file
View File

@ -0,0 +1,5 @@
{
"projects": {
"default": "fakebook-2df7b"
}
}

14
firebase.json Normal file
View File

@ -0,0 +1,14 @@
{
"functions": [
{
"source": "functions",
"codebase": "default",
"ignore": [
"node_modules",
".git",
"firebase-debug.log",
"firebase-debug.*.log"
]
}
]
}

5414
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,18 +1,27 @@
{
"name": "fakebook-ainiro",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"preact": "^10.25.3"
},
"devDependencies": {
"@preact/preset-vite": "^2.9.3",
"vite": "^6.0.5"
}
"name": "fakebook-ainiro",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"preact": "^10.25.3",
"@reduxjs/toolkit": "^1.8.3",
"bootstrap": "^4.6.0",
"firebase": "^9.9.4",
"react-bootstrap": "^1.5.2",
"react-dom": "^17.0.2",
"react-icons": "^4.2.0",
"react-player": "^2.12.0",
"react-redux": "^8.0.2",
"react-router-dom": "^5.2.0"
},
"devDependencies": {
"@preact/preset-vite": "^2.9.3",
"vite": "^6.0.5"
}
}

21
src/App.css Normal file
View File

@ -0,0 +1,21 @@
.login {
width: 390px;
border: 1px solid lightgray;
border-radius: 5px;
box-shadow: 0px 9px 18px 2px lightgrey;
margin: auto;
}
.bg-200 {
background-color: #e9ecef;
}
.main-container {
max-width: 100em;
margin: auto;
}
.title-footer {
font-size: small;
font-weight: lighter;
}

24
src/app copy/store.js Normal file
View File

@ -0,0 +1,24 @@
import { configureStore } from "@reduxjs/toolkit";
import userReducer from "../features/user/userSlice";
import currentUserReducer from "../features/currentUser/currentUserSlice";
import usersReducer from "../features/users/usersSlice";
import postsReducer from "../features/posts/postsSlice";
import incomingMessagesReducer from "../features/incomingMessages/incomingMessagesSlice";
import outgoingMessagesReducer from "../features/outgoingMessages/outgoingMessagesSlice";
import imagesReducer from "../features/images/imagesSlice";
import linkReducer from "../features/link/linkSlice";
import accountPageReducer from "../features/accountPage/accountPageSlice";
export default configureStore({
reducer: {
user: userReducer,
currentUser: currentUserReducer,
users: usersReducer,
posts: postsReducer,
incomingMessages: incomingMessagesReducer,
outgoingMessages: outgoingMessagesReducer,
images: imagesReducer,
link: linkReducer,
accountPage: accountPageReducer,
},
});

View File

@ -1,43 +1,109 @@
import { useState } from 'preact/hooks'
import preactLogo from './assets/preact.svg'
import viteLogo from '/vite.svg'
import './app.css'
import React, { useState, useCallback, useEffect } from "react";
import "./App.css";
import SignupModal from "./components/SignupModal.jsx";
import Login from "./components/Login.jsx";
import "bootstrap/dist/css/bootstrap.min.css";
import Col from "react-bootstrap/Col";
import Row from "react-bootstrap/Row";
import RecentLogins from "./components/RecentLogins.jsx";
import Button from "react-bootstrap/Button";
import UserAccount from "./components/UserAccount";
import PasswordReminderModal from "./components/PasswordReminderModal";
import { useDispatch, useSelector } from "react-redux";
import { subscribeAuth } from "./backend/backend";
import { profileLinkSet } from "./features/accountPage/accountPageSlice";
export function App() {
const [count, setCount] = useState(0)
function App() {
const user = useSelector((state) => state.user);
const dispatch = useDispatch();
return (
<>
<div>
<a href="https://vite.dev" target="_blank">
<img src={viteLogo} class="logo" alt="Vite logo" />
</a>
<a href="https://preactjs.com" target="_blank">
<img src={preactLogo} class="logo preact" alt="Preact logo" />
</a>
</div>
<h1>Vite + Preact</h1>
<div class="card">
<button onClick={() => setCount((count) => count + 1)}>
count is {count}
</button>
<p>
Edit <code>src/app.jsx</code> and save to test HMR
</p>
</div>
<p>
Check out{' '}
<a
href="https://preactjs.com/guide/v10/getting-started#create-a-vite-powered-preact-app"
target="_blank"
>
create-preact
</a>
, the official Preact + Vite starter
</p>
<p class="read-the-docs">
Click on the Vite and Preact logos to learn more
</p>
</>
)
useEffect(() => {
const unsubscribe = subscribeAuth();
return unsubscribe;
}, []);
//Handle the modal
const [show, setShow] = useState(false);
function handleClose() {
setShow(false);
}
function handleShow() {
setShow(true);
}
const handleCloseCallback = useCallback(handleClose, []);
//get the first and lastName for the route of the profile
const name =
(user && user.displayName && user.displayName.trim().split(" ")) || [];
const lastName = name.pop();
const firstName = name.join(" ");
const profileLink = `/fakebook/${lastName}.${firstName}`;
useEffect(() => dispatch(profileLinkSet(profileLink)), [profileLink, dispatch]);
//handling the password reminder button
const [isModalSignup, setModalSignup] = useState(true);
function handleClickPasswordReminderBtn() {
setModalSignup(false);
handleShow();
}
if (user.isLoading) {
return <div>...Loading</div>;
}
if (user.isSignedIn && !user.error) {
if (user.isEmailVerified) return <UserAccount />;
else return <></>;
} else {
return (
<Col className="bg-200 vh-100">
<Row className="h-100 align-items-center">
<Col
lg={{ span: 5, offset: 1 }}
className="d-flex justify-content-center">
<RecentLogins />
</Col>
<Col lg={5} className="bg-200 d-flex justify-content-center">
<div className="login p-3 bg-light">
<Login
onClickForgottenPswd={handleClickPasswordReminderBtn}
></Login>
<hr />
<Button
variant="success"
size="lg"
className="d-block w-60 mx-auto mt-4"
onClick={handleShow}>
<b>Create New Account</b>
</Button>
</div>
</Col>
<SignupModal
show={show && isModalSignup}
onHide={handleCloseCallback}
onExit={() => setModalSignup(true)}></SignupModal>
<PasswordReminderModal
show={show && !isModalSignup}
onHide={handleClose}
onExit={() => setModalSignup(true)}
/>
</Row>
</Col>
);
}
}
export default App;

24
src/app/store.js Normal file
View File

@ -0,0 +1,24 @@
import { configureStore } from '@reduxjs/toolkit'
import userReducer from '../features/user/userSlice'
import currentUserReducer from '../features/currentUser/currentUserSlice'
import usersReducer from '../features/users/usersSlice'
import postsReducer from '../features/posts/postsSlice'
import incomingMessagesReducer from '../features/incomingMessages/incomingMessagesSlice'
import outgoingMessagesReducer from '../features/outgoingMessages/outgoingMessagesSlice'
import imagesReducer from '../features/images/imagesSlice'
import linkReducer from '../features/link/linkSlice'
import accountPageReducer from '../features/accountPage/accountPageSlice'
export default configureStore({
reducer: {
user: userReducer,
currentUser: currentUserReducer,
users: usersReducer,
posts: postsReducer,
incomingMessages: incomingMessagesReducer,
outgoingMessages: outgoingMessagesReducer,
images: imagesReducer,
link: linkReducer,
accountPage: accountPageReducer,
},
});

271
src/backend/backend.js Normal file
View File

@ -0,0 +1,271 @@
import firebase from "firebase/compat/app";
import "firebase/compat/storage";
import "firebase/compat/auth";
import "firebase/compat/firestore";
import "firebase/compat/app-check";
import firebaseConfig from "../firebaseConfig";
import store from "../app/store";
import {
signIn,
signOut,
errorOccured,
loadingFinished,
loadingStarted,
} from "../features/user/userSlice";
import { currentUserUpdated } from "../features/currentUser/currentUserSlice";
import { usersUpdated } from "../features/users/usersSlice";
import { postsUpdated } from "../features/posts/postsSlice";
import { incomingMessagesUpdated } from "../features/incomingMessages/incomingMessagesSlice";
import { outgoingMessagesUpdated } from "../features/outgoingMessages/outgoingMessagesSlice";
// URL of my website.
const FAKEBOOK_URL = { url: "https://alexerdei73.github.io/fakebook/" };
firebase.initializeApp(firebaseConfig);
const appCheck = firebase.appCheck();
// Pass your reCAPTCHA v3 site key (public key) to activate(). Make sure this
// key is the counterpart to the secret key you set in the Firebase console.
appCheck.activate(
"6LfCG9UhAAAAAL8vSI4Hbustx8baJEDMA0Sz1zD2",
// Optional argument. If true, the SDK automatically refreshes App Check
// tokens as needed.
true
);
const storage = firebase.storage();
export async function getImageURL(imagePath) {
const imageRef = storage.ref(imagePath);
const url = await imageRef.getDownloadURL();
return url;
}
const auth = firebase.auth();
export function subscribeAuth() {
return auth.onAuthStateChanged((user) => {
if (user) {
const id = user.uid;
const isEmailVerified = user.emailVerified;
const displayName = user.displayName;
store.dispatch(signIn({ id, displayName, isEmailVerified }));
} else {
store.dispatch(signOut());
}
store.dispatch(loadingFinished());
});
}
const firestore = firebase.firestore();
const usersCollection = firestore.collection("users");
//The following global variables get values, when the UserAccount component renders and runs
//subscribeCurrentUser. After that we use them globally in the following functions.
let userID;
let userDocRef;
export function subscribeCurrentUser() {
userID = store.getState().user.id; //These are the
userDocRef = usersCollection.doc(userID); //global values
return userDocRef.onSnapshot((doc) => {
store.dispatch(currentUserUpdated(doc.data()));
});
}
export function currentUserOnline() {
userDocRef.update({ isOnline: true });
}
export function currentUserOffline() {
return userDocRef.update({ isOnline: false });
}
export function subscribeUsers() {
return usersCollection.onSnapshot((snapshot) => {
const users = [];
snapshot.forEach((user) => {
const userData = user.data();
userData.userID = user.id;
users.push(userData);
});
store.dispatch(usersUpdated(users));
});
}
export async function signUserOut() {
store.dispatch(loadingStarted());
await currentUserOffline();
await auth.signOut();
store.dispatch(loadingFinished());
}
export function subscribePosts() {
const postsCollection = firestore.collection("posts");
return postsCollection.orderBy("timestamp", "desc").onSnapshot((snapshot) => {
const posts = [];
snapshot.forEach((post) => {
const postData = post.data();
const timestamp = postData.timestamp;
let dateString = "";
if (timestamp) dateString = timestamp.toDate().toLocaleString();
postData.timestamp = dateString;
postData.postID = post.id;
posts.push(postData);
});
store.dispatch(postsUpdated(posts));
});
}
export function subscribeMessages(typeOfMessages) {
let typeOfUser;
let actionCreator;
if (typeOfMessages === "incoming") {
typeOfUser = "recipient";
actionCreator = incomingMessagesUpdated;
} else {
typeOfUser = "sender";
actionCreator = outgoingMessagesUpdated;
}
const messagesCollection = firestore
.collection("messages")
.where(typeOfUser, "==", userID);
return messagesCollection.onSnapshot((snapshot) => {
const messages = [];
snapshot.forEach((message) => {
const messageData = message.data();
const timestamp = message.data().timestamp;
let dateString;
if (timestamp) dateString = timestamp.toDate().toISOString();
else dateString = "";
messageData.timestamp = dateString;
messageData.id = message.id;
if (dateString !== "") messages.push(messageData);
});
store.dispatch(actionCreator(messages));
});
}
export async function createUserAccount(user) {
try {
const result = await auth.createUserWithEmailAndPassword(
user.email,
user.password
);
// Update the nickname
await result.user.updateProfile({
displayName: `${user.firstname} ${user.lastname}`,
});
// get the index of the new user with the same username
const querySnapshot = await firestore
.collection("users")
.where("firstname", "==", user.firstname)
.where("lastname", "==", user.lastname)
.get();
const index = querySnapshot.size;
// Create firestore document
await firestore.collection("users").doc(result.user.uid).set({
firstname: user.firstname,
lastname: user.lastname,
profilePictureURL: "fakebook-avatar.jpeg",
backgroundPictureURL: "background-server.jpg",
photos: [],
posts: [],
isOnline: false,
index: index,
});
// Sign out the user
await firebase.auth().signOut();
// Send Email Verification and redirect to my website.
await result.user.sendEmailVerification(FAKEBOOK_URL);
console.log("Verification email has been sent.");
} catch (error) {
// Update the error
store.dispatch(errorOccured(error.message));
console.log(error.message);
}
}
export async function signInUser(user) {
const EMAIL_VERIFICATION_ERROR =
"Please verify your email before to continue";
const NO_ERROR = "";
try {
const result = await auth.signInWithEmailAndPassword(
user.email,
user.password
);
// email has been verified?
if (!result.user.emailVerified) {
auth.signOut();
store.dispatch(errorOccured(EMAIL_VERIFICATION_ERROR));
} else {
store.dispatch(errorOccured(NO_ERROR));
}
} catch (error) {
// Update the error
store.dispatch(errorOccured(error.message));
} finally {
store.dispatch(loadingFinished());
}
}
export function sendPasswordReminder(email) {
return auth.sendPasswordResetEmail(email);
}
export async function upload(post) {
const refPosts = firestore.collection("posts");
const docRef = await refPosts.add({
...post,
timestamp: firebase.firestore.FieldValue.serverTimestamp(),
});
const postID = docRef.id;
updateUserPosts(postID);
return docRef;
}
function updateUserPosts(postID) {
const user = store.getState().currentUser;
let newPosts;
if (user.posts) newPosts = [...user.posts];
else newPosts = [];
newPosts.unshift(postID);
userDocRef.update({
posts: newPosts,
});
}
export function updatePost(post, postID) {
const postRef = firestore.collection("posts").doc(postID);
//We need to remove the timestamp, because it is stored in serializable format in the redux-store
//so we can't write it back to firestore
const { timestamp, ...restPost } = post;
postRef.update(restPost);
}
export function addFileToStorage(file) {
const ref = storage.ref(userID).child(file.name);
return ref.put(file);
}
export function updateProfile(profile) {
console.log(userDocRef);
console.log(profile);
return userDocRef.update(profile);
}
const refMessages = firestore.collection("messages");
export function uploadMessage(msg) {
return refMessages.add({
...msg,
timestamp: firebase.firestore.FieldValue.serverTimestamp(),
});
}
export function updateToBeRead(messageID) {
return refMessages.doc(messageID).update({ isRead: true });
}

View File

@ -0,0 +1,44 @@
import React from "react";
import StorageImage from "./StorageImage";
const CircularImage = (props) => {
const { size, url, isOnline, ...rest } = props;
const radius = Math.floor(size / 6);
const shift = Math.floor(0.8536 * size - radius / 2);
return (
<>
<StorageImage
style={{
width: `${size}px`,
height: `${size}px`,
objectFit: "cover",
borderRadius: `${size / 2}px`,
pointerEvents: "none",
position: "relative",
}}
storagePath={url}
alt=""
{...rest}
/>
{isOnline && (
<div
className="bg-success"
style={{
width: `${2 * radius}px`,
height: `${2 * radius}px`,
borderRadius: "50%",
position: "absolute",
top: `${shift}px`,
left: `${shift}px`,
border: "2px solid white",
}}
/>
)}
</>
);
};
export default CircularImage;

View File

@ -0,0 +1,13 @@
.comment-img-container {
position: relative;
width: 100%;
padding: 20px;
}
.img-to-comment {
width: 30%;
}
.comment-btn {
border-radius: 18px;
}

155
src/components/Comments.jsx Normal file
View File

@ -0,0 +1,155 @@
import React, { useState } from "react";
import { Col, Row, CloseButton, Button } from "react-bootstrap";
import StorageImage from "./StorageImage";
import CircularImage from "./CircularImage";
import UploadPhoto from "./UploadPhoto";
import DisplayComment from "./DisplayComment";
import StyledTextarea from "./StyledTextarea";
import { MdPhotoCamera } from "react-icons/md";
import { MdSend } from "react-icons/md";
import {
addPhoto,
handleTextareaChange,
delPhoto,
handleKeyPress,
} from "./helper";
import "./Comments.css";
import { useSelector } from "react-redux";
import { updatePost } from "../backend/backend";
const Comments = (props) => {
const { post } = props;
const user = useSelector((state) => state.currentUser);
const userID = useSelector((state) => state.user.id);
const WELCOME_TEXT = "Write a comment ...";
const INIT_COMMENT = {
userID: userID,
text: "",
isPhoto: false,
photoURL: "",
};
const [comment, setComment] = useState(INIT_COMMENT);
const [show, setShow] = useState(false);
function handleChange(e) {
handleTextareaChange({
e: e,
state: comment,
setState: setComment,
});
}
function addPhotoToComment(file) {
addPhoto({
state: comment,
setState: setComment,
file: file,
userID: userID,
});
}
function deletePhoto() {
delPhoto({
state: comment,
setState: setComment,
user: user,
userID: userID,
});
}
function saveComment() {
if (comment.text === "" && !comment.isPhoto) return;
const newPost = {
...post,
comments: [],
};
const postID = post.postID;
if (post.comments) newPost.comments = [...post.comments];
newPost.comments.push(comment);
updatePost(newPost, postID);
setComment(INIT_COMMENT);
}
return (
<Col>
<hr />
{post.comments &&
post.comments.map((comment, index) => (
<DisplayComment key={index} comment={comment} className="mb-2" />
))}
<Row>
<Col xs={1}>
<CircularImage size="36" url={user.profilePictureURL} />
</Col>
<Col xs={11}>
<Row
style={{
background: "#e9ecef",
borderRadius: "18px",
marginLeft: "5px",
}}>
<Col xs={9} className="align-self-center">
<StyledTextarea
onChange={handleChange}
onKeyPress={(e) => handleKeyPress(e, saveComment)}
welcomeText={WELCOME_TEXT}
value={comment.text}
className="w-100 mt-2"
/>
</Col>
<Col xs={3}>
<Row className="justify-content-end align-items-baseline">
<Button
variant="light"
size="sm"
className="comment-btn"
onClick={() => setShow(true)}
disabled={comment.isPhoto}>
<MdPhotoCamera
size="18px"
className="text-muted"
aria-label="photo"
/>
</Button>
<Button
variant="light"
size="sm"
className="comment-btn"
onClick={() => saveComment()}>
<MdSend
size="18px"
className="text-primary"
aria-label="send"
/>
</Button>
</Row>
</Col>
</Row>
{comment.isPhoto && (
<div className="comment-img-container">
<StorageImage
alt=""
storagePath={`/${comment.photoURL}`}
className="img-to-comment"
/>
<div id="close-btn-container">
<CloseButton onClick={deletePhoto} />
</div>
</div>
)}
</Col>
</Row>
<UploadPhoto
show={show}
setShow={setShow}
updatePost={addPhotoToComment}
/>
</Col>
);
};
export default Comments;

View File

@ -0,0 +1,33 @@
.container-choose-to:hover {
background-color: lightgray;
}
.nav-btn {
border: none;
text-align: left;
}
.nav-btn:focus {
background-color: lightgray;
}
.white {
background-color: white;
}
.msg-btn {
position: fixed;
background: white;
padding: 12px;
border-radius: 50%;
bottom: 20px;
right: calc((100vw - 100em) / 2 + 20px);
box-shadow: 0px 5px 5px 0px lightgray;
border: none;
}
@media (max-width: 100em) {
.msg-btn {
right: 20px;
}
}

354
src/components/Contacts.jsx Normal file
View File

@ -0,0 +1,354 @@
import React, { useState, useRef, useEffect, useCallback } from "react";
import {
Card,
CloseButton,
Nav,
OverlayTrigger,
Button,
Col,
Row,
} from "react-bootstrap";
import StorageImage from "./StorageImage";
import ProfileLink from "./ProfileLink";
import { FiEdit } from "react-icons/fi";
import { HiOutlinePhotograph } from "react-icons/hi";
import { MdSend } from "react-icons/md";
import StyledTextarea from "./StyledTextarea";
import UploadPhoto from "./UploadPhoto";
import Conversation from "./Conversation";
import {
addPhoto,
handleTextareaChange,
delPhoto,
handleKeyPress,
} from "./helper";
import "./Contacts.css";
import { useSelector } from "react-redux";
import {
subscribeMessages,
updateToBeRead,
uploadMessage,
} from "../backend/backend";
const Contacts = () => {
const [showOverlay, setShowOverlay] = useState(false);
const [recipient, setRecipient] = useState(null);
const [showPhotoDlg, setShowPhotoDlg] = useState(null);
const user = useSelector((state) => state.currentUser);
const userID = useSelector((state) => state.user.id);
const users = useSelector((state) => state.users);
const WELCOME_TEXT = "Aa";
const INIT_MESSAGE = {
sender: `${userID}`,
recipient: "",
text: "",
isPhoto: false,
photoURL: "",
isRead: false,
};
const [message, setMessage] = useState(INIT_MESSAGE);
function handleClick(user) {
if (!user && recipient) return;
if (recipient && user.userID !== recipient.userID) return;
setShowOverlay(true);
let id;
if (user) id = user.userID;
else id = "";
const newMessage = { ...message };
newMessage.recipient = id;
setMessage(newMessage);
setRecipient(user);
}
const handleClickCallback = useCallback(handleClick, [user]);
async function handleClose() {
await updateReadStatusOfMessages(recipient);
removeSender();
setShowOverlay(false);
setRecipient(null);
}
function handleChange(e) {
handleTextareaChange({
e: e,
state: message,
setState: setMessage,
});
}
function addPhotoToMessage(file) {
addPhoto({
state: message,
setState: setMessage,
file: file,
userID: userID,
});
}
function deletePhoto() {
delPhoto({
state: message,
setState: setMessage,
user: user,
userID: userID,
});
}
const convRowRef = useRef(null);
const [scrollHeight, setScrollHeight] = useState("");
function saveMessage() {
uploadMessage(message).then(() => {
setMessage(INIT_MESSAGE);
setScrollHeight(convRowRef.current.scrollHeight);
});
}
useEffect(() => {
if (convRowRef.current) convRowRef.current.scrollTop = scrollHeight;
}, [scrollHeight]);
useEffect(() => {
const unsubscribeIncomingMsg = subscribeMessages("incoming");
const unsubscribeOutgoingMsg = subscribeMessages("outgoing");
return () => {
unsubscribeIncomingMsg();
unsubscribeOutgoingMsg();
};
}, []);
const [senders, setSenders] = useState([]);
const incomingMessages = useSelector((state) => state.incomingMessages);
const unread = incomingMessages.filter((message) => !message.isRead);
useEffect(() => {
const sendersWithUnreadMsg = [];
unread.forEach((msg) => {
const sender = msg.sender;
if (sendersWithUnreadMsg.indexOf(sender) === -1)
sendersWithUnreadMsg.push(sender);
});
if (senders.length !== sendersWithUnreadMsg.length)
setSenders(sendersWithUnreadMsg);
}, [senders, unread]);
useEffect(() => {
if (senders.length === 0) return;
const last = senders.length - 1;
const sender = senders[last];
handleClickCallback(users.find((usr) => usr.userID === sender));
}, [senders, users, handleClickCallback]);
function updateReadStatusOfMessages(sender) {
const messagesToUpdate = unread.filter(
(msg) => msg.sender === sender.userID
);
const updates = [];
messagesToUpdate.forEach((msg) => {
const messageID = msg.id;
updates.push(updateToBeRead(messageID));
});
return Promise.all(updates);
}
function removeSender() {
const newSenders = [...senders];
newSenders.pop();
setSenders(newSenders);
}
//We open the overlay card programmatically again, otherwise the user is unable to send
//more than one message.
if (recipient)
if (showOverlay && message.recipient === "")
//We only do this if we set back the INIT_MESSAGE after previous message had sent.
handleClick(recipient);
return (
<>
<Nav className="flex-column">
<h5 className="text-muted ml-3">
<b>Contacts</b>
</h5>
{users.map((user, index) =>
user.userID === userID ? (
<div key={index}></div>
) : (
<button
type="button"
key={index}
className="navitem text-dark flex-row justify-content-center p-2 mb-1 nav-btn bg-200"
onClick={() => handleClick(user)}
>
<ProfileLink size="26" fullname="true" bold="false" user={user} />
</button>
)
)}
</Nav>
<OverlayTrigger
placement="left-start"
show={showOverlay}
overlay={
<Card
style={{
width: "350px",
height: "450px",
background: "white",
fontSize: "1.2rem",
maxHeight: `${window.innerHeight - 70}px`,
}}
>
<Card.Body className="overflow-none">
<Card.Title>
{!recipient && (
<>
<h6>New Message</h6>
<h6>To:</h6>
</>
)}
{recipient && (
<ProfileLink
size="26"
fullname="true"
bold="true"
user={recipient}
/>
)}
<div className="close-btn-container">
<CloseButton onClick={handleClose} className="text-primary" />
</div>
</Card.Title>
<hr />
{recipient && (
<Row
className="mh-100 overflow-auto flex-column-reverse"
ref={convRowRef}
>
<Conversation sender={userID} recipient={recipient.userID} />
</Row>
)}
{!recipient && (
<Col className="h-75 overflow-auto">
{users.map((user, index) =>
user.userID === userID ? (
<div key={index}></div>
) : (
<button
type="button"
key={index}
className="flex-row text-dark justify-content-start p-2 container-choose-to nav-btn w-100 mb-1 white"
onClick={() => handleClick(user)}
>
<ProfileLink
size="36"
fullname="true"
bold="false"
user={user}
/>
</button>
)
)}
</Col>
)}
</Card.Body>
{recipient && (
<Card.Footer className="mt-5">
<Row>
{message.text === "" && (
<Col xs={2}>
<Button
variant="light"
size="sm"
className="add-photo-btn"
onClick={() => setShowPhotoDlg(true)}
>
<HiOutlinePhotograph
size="21px"
className="text-primary"
/>
</Button>
</Col>
)}
<Col
xs={message.text === "" ? 8 : 10}
className="align-self-center"
style={{
background: "#e9ecef",
borderRadius: "18px",
}}
>
{message.isPhoto && (
<div className="comment-img-container">
<StorageImage
alt=""
storagePath={`/${message.photoURL}`}
className="img-to-comment"
/>
<div className="close-btn-container">
<CloseButton onClick={deletePhoto} />
</div>
</div>
)}
<StyledTextarea
onChange={handleChange}
onKeyPress={(e) => handleKeyPress(e, saveMessage)}
welcomeText={WELCOME_TEXT}
value={message.text}
className="w-100 mt-2"
/>
</Col>
<Col xs={2}>
<Button
variant="light"
size="sm"
className="add-photo-btn"
onClick={() => {
if (message.text !== "" || message.isPhoto)
saveMessage();
}}
>
<MdSend size="23px" className="text-primary" />
</Button>
</Col>
</Row>
</Card.Footer>
)}
<UploadPhoto
show={showPhotoDlg}
setShow={setShowPhotoDlg}
updatePost={addPhotoToMessage}
userID={userID}
/>
</Card>
}
>
<button
type="button"
/*style={{
position: "fixed",
background: "white",
padding: "12px",
borderRadius: "50%",
bottom: "20px",
right: "calc((100vw - 100em) / 2 + 20px)",
boxShadow: "0px 5px 5px 0px lightgray",
border: "none",
}}*/
className="msg-btn"
onClick={() => handleClick(null)}
aria-label="Message"
>
<FiEdit size="22px" aria-hidden="true" />
</button>
</OverlayTrigger>
</>
);
};
export default Contacts;

View File

@ -0,0 +1,37 @@
import React, { useEffect, useState } from "react";
import DisplayConversation from "./DisplayConversation";
import { useSelector } from "react-redux";
const Conversation = (props) => {
const { recipient } = props;
const [conversation, setConversation] = useState([]);
const incomingMessages = useSelector((state) => state.incomingMessages);
const outgoingMessages = useSelector((state) => state.outgoingMessages);
const incoming = incomingMessages.filter(
(message) => message.sender === recipient
);
const outgoing = outgoingMessages.filter(
(message) => message.recipient === recipient
);
function getConversation(incoming, outgoing) {
const conversation = [...incoming, ...outgoing];
const sorted = conversation.sort(
(msgA, msgB) => new Date(msgA.timestamp) - new Date(msgB.timestamp)
);
return sorted;
}
useEffect(() => {
const newConversation = getConversation(incoming, outgoing);
if (newConversation.length !== conversation.length)
setConversation(newConversation);
}, [incoming, outgoing, conversation]);
return <DisplayConversation conversation={conversation} />;
};
export default Conversation;

View File

@ -0,0 +1,28 @@
.text-btn {
background: #e9ecef;
border-radius: 20px;
position: absolute;
top: 20px;
left: 80px;
color: gray;
border: none;
text-align: left;
width: calc(100% - 100px);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.text-div:hover {
background: lightgray;
}
.add-btn {
display: block;
flex: 1;
max-width: 150px;
}
.card .d-flex {
justify-content: space-between;
}

View File

@ -0,0 +1,97 @@
import React, { useState } from "react";
import { Card, Button } from "react-bootstrap";
import CircularImage from "./CircularImage";
import PostModal from "./PostModal";
import "./CreatePost.css";
import { HiOutlinePhotograph } from "react-icons/hi";
import { AiFillYoutube } from "react-icons/ai";
import { useSelector } from "react-redux";
const CreatePost = (props) => {
const { firstname, isCurrentUser, ...rest } = props;
const user = useSelector((state) => state.currentUser);
const PLACEHOLDER_FOR_CURRENT_USER = `What's on your mind ${user.firstname}?`;
const PLACEHOLDER_FOR_ANOTHER_USER = `Write something to ${firstname}`;
const [showPostModal, setShowPostModal] = useState(false);
const [isYoutubeBtnPressed, setYoutubeBtnPressed] = useState(false);
const [text, setText] = useState("");
const handleClose = () => {
setShowPostModal(false);
setYoutubeBtnPressed(false);
};
const handleClick = () => setShowPostModal(true);
const handleYoutubeBtnClick = () => {
setYoutubeBtnPressed(true);
setShowPostModal(true);
};
function getPlaceholder() {
if (isCurrentUser) return PLACEHOLDER_FOR_CURRENT_USER;
else return PLACEHOLDER_FOR_ANOTHER_USER;
}
function getText() {
const MAX_LENGTH = 55;
const length = text.length;
if (length === 0) return getPlaceholder();
else {
let newText = text.slice(0, MAX_LENGTH);
if (length > MAX_LENGTH) newText += "...";
return newText;
}
}
return (
<>
<Card className="create-post-card" {...rest}>
<Card.Body>
<Card.Title>
<CircularImage size="40" url={user.profilePictureURL} />
<button
type="button"
className="p-2 text-dark text-btn"
onClick={handleClick}>
{getText()}
</button>
</Card.Title>
<hr></hr>
<div className="d-flex">
<Button
variant="light"
className="add-btn"
size="sm"
onClick={handleYoutubeBtnClick}>
<AiFillYoutube size="28px" className="text-danger" />
YouTube
</Button>
<Button
variant="light"
className="add-btn"
size="sm"
onClick={handleClick}>
<HiOutlinePhotograph size="28px" className="text-success" />
Photo
</Button>
</div>
</Card.Body>
</Card>
<PostModal
show={showPostModal}
onClose={handleClose}
setText={setText}
isYoutubeBtnPressed={isYoutubeBtnPressed}
placeholder={getPlaceholder()}
/>
</>
);
};
export default CreatePost;

View File

@ -0,0 +1,40 @@
import React from "react";
import { Row, Col } from "react-bootstrap";
import StorageImage from "./StorageImage";
import CircularImage from "./CircularImage";
import { useSelector } from "react-redux";
const DisplayComment = (props) => {
const { comment, ...rest } = props;
const userID = comment.userID;
const users = useSelector((state) => state.users);
const user = users.find((user) => user.userID === userID);
return (
<Row {...rest}>
<Col xs={1}>
<CircularImage size="26" url={user.profilePictureURL} />
</Col>
<Col xs={11}>
<p>
<b>{`${user.firstname} ${user.lastname} `}</b>
{comment.text}
</p>
{comment.isPhoto && (
<StorageImage
alt=""
storagePath={comment.photoURL}
style={{
width: "30%",
}}
/>
)}
</Col>
</Row>
);
};
export default DisplayComment;

View File

@ -0,0 +1,17 @@
import React from "react";
import { Col } from "react-bootstrap";
import Message from "./Message";
const DisplayConversation = (props) => {
const { conversation } = props;
return (
<Col>
{conversation.map((msg, index) => (
<Message key={index} message={msg} />
))}
</Col>
);
};
export default DisplayConversation;

View File

@ -0,0 +1,148 @@
import React, { useState } from "react";
import { Card, Button } from "react-bootstrap";
import StorageImage from "./StorageImage";
import ProfileLink from "./ProfileLink";
import LikesModal from "./LikesModal";
import Comments from "./Comments";
import { AiOutlineLike, AiFillLike } from "react-icons/ai";
import { GoComment } from "react-icons/go";
import { useSelector } from "react-redux";
import { updatePost } from "../backend/backend";
import ReactPlayer from "react-player/lazy";
const DisplayPost = (props) => {
const { post, ...rest } = props;
const userID = useSelector((state) => state.user.id);
const users = useSelector((state) => state.users);
const [show, setShow] = useState(false);
const [showComments, setShowComments] = useState(false);
function handleHide() {
setShow(false);
}
//We avoid error if post is undefind
if (!post) return <></>;
const postID = post.postID;
const user = users.find((user) => user.userID === post.userID);
//We avoid error if user is undefined
if (!user) return <></>;
function index() {
return post.likes.indexOf(userID);
}
function isLiked() {
return !(index() === -1);
}
function handleClick() {
const likes = [...post.likes];
const index = likes.indexOf(userID);
if (index === -1) likes.push(userID);
else likes.splice(index, 1);
updatePost({ likes: likes }, postID);
}
function handleCommentClick() {
setShowComments(true);
}
function getYouTubeURL(url) {
const index = url.lastIndexOf("/");
const videoID = url.slice(index + 1);
const newURL = `https://www.youtube.com/watch?v=${videoID}`;
return newURL;
}
return (
<Card {...rest}>
<Card.Header>
<ProfileLink user={user} size="40" fullname="true" bold="true" />
<span
style={{
fontSize: "12px",
}}
>
{post.timestamp}
</span>
</Card.Header>
<Card.Body>
<Card.Text>{post.text}</Card.Text>
{post.isPhoto && (
<StorageImage alt="" storagePath={post.photoURL} className="w-100" />
)}
{post.isYoutube && (
<div className="video-container">
<ReactPlayer
className="react-player"
url={getYouTubeURL(post.youtubeURL)}
light
width="100%"
height="100%"
playing
controls
/>
</div>
)}
</Card.Body>
<Card.Footer>
<Button
variant="link"
className="text-muted"
onClick={() => {
if (post.likes.length > 0) setShow(true);
}}
style={{ boxShadow: "none" }}
>
<AiFillLike
className="bg-primary text-light mr-2"
style={{ borderRadius: "50%" }}
aria-label="likes"
/>
{post.likes.length}
</Button>
<Button
variant="link"
className="text-muted float-right"
onClick={() => setShowComments(!showComments)}
style={{ boxShadow: "none" }}
>
comments({post.comments ? post.comments.length : 0})
</Button>
<hr />
<Button
variant="light"
className={`${isLiked() ? "text-primary" : "text-muted"} w-50`}
onClick={handleClick}
>
{isLiked() ? (
<AiFillLike size="22px" />
) : (
<AiOutlineLike size="22px" />
)}
<b> Like</b>
</Button>
<Button
variant="light"
className="text-muted w-50"
onClick={handleCommentClick}
>
<GoComment size="22px" />
<b> Comment</b>
</Button>
{showComments && <Comments post={post} />}
</Card.Footer>
<LikesModal show={show} onHide={handleHide} likes={post.likes} />
</Card>
);
};
export default DisplayPost;

View File

@ -0,0 +1,14 @@
import React from "react";
import DisplayPost from "./DisplayPost";
import { useSelector } from "react-redux";
const DisplayUserPost = (props) => {
const { postID } = props;
const posts = useSelector((state) => state.posts);
const post = posts.find((post) => post.postID === postID);
return <DisplayPost post={post} />;
};
export default DisplayUserPost;

View File

@ -0,0 +1,23 @@
.popup-card {
position: relative;
width: 400px;
height: 200px;
background: white;
border: 2px solid lightgray;
border-radius: 10px;
}
.name-tag {
position: absolute;
top: 20px;
left: 160px;
}
.profile-picture {
object-fit: cover;
}
.friend-btn {
border: none;
background: white;
}

View File

@ -0,0 +1,70 @@
import React, { useState } from "react";
import { Col, OverlayTrigger } from "react-bootstrap";
import { useHistory } from "react-router-dom";
import CircularImage from "./CircularImage";
import StorageImage from "./StorageImage";
import "./FriendCard.css";
const FriendCard = (props) => {
const { user } = props;
const userName = `${user.firstname} ${user.lastname}`;
const [showOverlay, setShowOverlay] = useState(false);
const history = useHistory();
function handleClick() {
history.push(
user.index && user.index > 0
? `/fakebook/${user.lastname}.${user.firstname}.${user.index}`
: `/fakebook/${user.lastname}.${user.firstname}`
);
}
return (
<Col xs={6} className="my-3">
<OverlayTrigger
placement="auto"
show={showOverlay}
overlay={
<div
className="popup-card"
onMouseEnter={() => setShowOverlay(true)}
onMouseLeave={() => setShowOverlay(false)}
onClick={handleClick}
>
<div className="m-3">
<CircularImage size="120" url={user.profilePictureURL} />
</div>
<h4 className="name-tag">
<b>{userName}</b>
</h4>
</div>
}
>
<button
type="button"
onClick={handleClick}
className="friend-btn"
tabIndex="-1"
>
<StorageImage
storagePath={user.profilePictureURL}
width="90px"
height="90px"
alt=""
className="profile-picture"
onMouseEnter={() => setShowOverlay(true)}
onMouseLeave={() => setShowOverlay(false)}
/>
</button>
</OverlayTrigger>
<button type="button" className="ml-3 friend-btn" onClick={handleClick}>
<b>{userName}</b>
</button>
</Col>
);
};
export default FriendCard;

View File

@ -0,0 +1,46 @@
import React from "react";
import ProfileLink from "./ProfileLink";
import { Col } from "react-bootstrap";
import { Link } from "react-router-dom";
import { useSelector } from "react-redux";
const FriendList = (props) => {
const { users, variant } = props;
const allUsers = useSelector((state) => state.users);
//Component is used with users prop in LikesModal, but without users prop in FriendsListPage
let usersToUse = allUsers;
if (users) usersToUse = users;
const isModal = variant === "modal";
return (
<Col xs="auto" className="overflow-auto mh-100">
<div id="col-1" className="m-2">
{usersToUse.map((user, index) => {
let profileLink = `/fakebook/${user.lastname}.${user.firstname}`;
if (user.index && user.index > 0)
profileLink = profileLink + `.${user.index}`;
return (
<Link
key={index}
to={profileLink}
className={isModal ? "p-1 text-dark" : "nav-link text-dark"}
>
<ProfileLink
user={user}
fullname="true"
size={isModal ? "40" : "60"}
bold="true"
className="pb-1"
/>
</Link>
);
})}
</div>
</Col>
);
};
export default FriendList;

View File

@ -0,0 +1,51 @@
import React, { useEffect } from "react";
import { Card, Row } from "react-bootstrap";
import { Link, useRouteMatch } from "react-router-dom";
import FriendCard from "./FriendCard";
import { handleClickLink } from "./helper";
import { useSelector } from "react-redux";
const Friends = (props) => {
const { url } = useRouteMatch();
const { linkHandling } = props;
const { linkRefs, linkState } = linkHandling;
const [activeLink, setActiveLink] = linkState;
const friendsLinkRef = linkRefs.friends;
const users = useSelector((state) => state.users);
useEffect(() => {
handleClickLink(
{ currentTarget: friendsLinkRef.current },
activeLink,
setActiveLink
);
}, [activeLink, friendsLinkRef, setActiveLink]);
//copyUsers never undefined to avoid error
let copyUsers;
if (!users) copyUsers = [];
else copyUsers = users;
return (
<Card>
<Card.Body>
<Card.Title>
<Link to={url} className="text-body">
<b>Friends</b>
</Link>
</Card.Title>
<Row>
{copyUsers.map((user, index) => (
<FriendCard user={user} key={index} />
))}
</Row>
</Card.Body>
</Card>
);
};
export default Friends;

View File

@ -0,0 +1,28 @@
.friends-list {
margin-top: 50px;
height: 92.5vh;
}
.col-two {
overflow-x: hidden;
}
.profile-container {
width: 98%;
min-width: 600px;
margin-top: -3.5%;
padding-right: 25px;
}
.nav-link:hover {
background-color: lightgray;
}
@media (max-width: 600) {
.profile-container {
width: auto;
min-width: none;
margin-top: 0;
padding-right: 0;
}
}

View File

@ -0,0 +1,53 @@
import React, { useEffect } from "react";
import Profile from "./Profile";
import FriendList from "./FriendList";
import { Row, Col } from "react-bootstrap";
import { useLocation } from "react-router-dom";
import "./FriendsListPage.css";
import imgFriends from "../images/friends.jpg";
import { useDispatch } from "react-redux";
import { linkUpdated } from "../features/link/linkSlice";
const FriendsListPage = (props) => {
const FRIENDS_LIST_PAGE_PATH = "/fakebook/friends/list";
const location = useLocation();
const dispatch = useDispatch();
const isNoUser = FRIENDS_LIST_PAGE_PATH === location.pathname;
//we set the active link to the friends link when it renders
useEffect(() => {
dispatch(linkUpdated("friends"));
}, [dispatch]);
return window.innerWidth > 600 || isNoUser ? (
<Row className="overflow-hidden friends-list">
<FriendList />
<Col className="overflow-auto mh-100 hide-scrollbar col-two">
{isNoUser ? (
<div className="h-100 w-100 d-flex flex-column align-items-center justify-content-center">
<img
width="200px"
src={imgFriends}
alt="cartoon of fakebook friends"
className="p-4"
/>
<h5 className="text-muted">
<b>Select people's names to preview their profile.</b>
</h5>
</div>
) : (
<div className="profile-container">
<Profile />
</div>
)}
</Col>
</Row>
) : (
<Profile />
);
};
export default FriendsListPage;

View File

@ -0,0 +1,22 @@
/* Hide scrollbar for Chrome, Safari and Opera */
.hide-scrollbar::-webkit-scrollbar {
display: none;
}
/* Hide scrollbar for IE, Edge and Firefox */
.hide-scrollbar {
-ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */
}
@media (max-width: 640px) {
.contacts-col {
display: none;
}
}
@media (max-width: 992px) {
.left-navbar-col {
display: none;
}
}

View File

@ -0,0 +1,47 @@
import React, { useEffect } from "react";
import { Row, Col } from "react-bootstrap";
import PostView from "./PostView";
import LeftNavbar from "./LeftNavbar";
import VideoView from "./VideoView";
import Contacts from "./Contacts";
import "./HomePage.css";
import { useSelector, useDispatch } from "react-redux";
import { linkUpdated } from "../features/link/linkSlice";
const HomePage = (props) => {
const { className } = props;
const accountPage = useSelector((state) => state.accountPage);
const { profileLink, isWatch } = accountPage;
const dispatch = useDispatch();
//we set the active link to the home link when it renders
useEffect(() => {
dispatch(linkUpdated(isWatch ? "watch" : "home"));
}, [isWatch, dispatch]);
return (
<Row className={`${className} overflow-hidden vh-100`}>
<Col className="mh-100 overflow-auto left-navbar-col">
<LeftNavbar profileLink={profileLink} />
</Col>
<Col
sm={9}
md={isWatch ? 12 : 9}
lg={isWatch ? 9 : 6}
className="mh-100 overflow-auto hide-scrollbar">
{!isWatch && <PostView />}
{isWatch && <VideoView />}
</Col>
{!isWatch && (
<Col
className="mh-100 overflow-auto contacts-col"
style={{ position: "relative" }}>
<Contacts />
</Col>
)}
</Row>
);
};
export default HomePage;

View File

@ -0,0 +1,8 @@
.navitem {
margin-left: 10px;
border-radius: 5px;
}
.navitem:hover {
background-color: lightgray;
}

View File

@ -0,0 +1,40 @@
import React from "react";
import { Nav } from "react-bootstrap";
import { Link } from "react-router-dom";
import ProfileLink from "./ProfileLink";
import { FaUserFriends } from "react-icons/fa";
import { MdOndemandVideo } from "react-icons/md";
import "./LeftNavbar.css";
import { useSelector } from "react-redux";
const LeftNavbar = (props) => {
const user = useSelector((state) => state.currentUser);
return (
<Nav className="flex-column mt-3" id="left-navbar">
<div className="navitem">
<Nav.Link
as={Link}
to={props.profileLink}
className="text-dark flex-column justify-content-center"
>
<ProfileLink user={user} size="26" fullname="true" />
</Nav.Link>
</div>
<div className="navitem">
<Nav.Link as={Link} to="/fakebook/friends/list" className="text-dark">
<FaUserFriends size="26px" className="text-info mr-2" />
<div className="d-inline">Friends</div>
</Nav.Link>
</div>
<div className="navitem">
<Nav.Link as={Link} to="/fakebook/watch" className="text-dark">
<MdOndemandVideo size="26px" className="text-info mr-2" />
<div className="d-inline">Watch</div>
</Nav.Link>
</div>
</Nav>
);
};
export default LeftNavbar;

View File

@ -0,0 +1,27 @@
import React from "react";
import { Modal } from "react-bootstrap";
import FriendList from "./FriendList";
import { useSelector } from "react-redux";
const LikesModal = (props) => {
const { show, onHide, likes } = props;
const users = useSelector((state) => state.users);
const usersWhoLike = likes.map((userID) =>
users.find((user) => user.userID === userID)
);
return (
<Modal show={show} onHide={onHide}>
<Modal.Header closeButton>
<Modal.Title className="text-primary">All</Modal.Title>
</Modal.Header>
<Modal.Body className="overflow-hidden">
<FriendList users={usersWhoLike} variant="modal" />
</Modal.Body>
</Modal>
);
};
export default LikesModal;

81
src/components/Login.jsx Normal file
View File

@ -0,0 +1,81 @@
import React, { useState } from "react";
import Form from "react-bootstrap/Form";
import Button from "react-bootstrap/Button";
import { useSelector, useDispatch } from "react-redux";
import { errorOccured, loadingStarted } from "../features/user/userSlice";
import { signInUser } from "../backend/backend";
const Login = (props) => {
const { onClickForgottenPswd } = props;
const [state, setState] = useState({ email: "", password: "" });
// onChange function
const handleChange = (e) => {
const newState = { ...state };
newState[e.target.name] = e.target.value;
setState(newState);
dispatch(errorOccured(""));
};
const errorMsg = useSelector((state) => state.user.error);
const dispatch = useDispatch();
// Submit function (Create account)
const handleSubmit = (e) => {
e.preventDefault();
e.stopPropagation();
if (state.email === "") {
dispatch(errorOccured("Email is required."));
return;
} else if (state.password === "") {
dispatch(errorOccured("Password is required."));
return;
}
dispatch(loadingStarted());
signInUser(state);
};
return (
<>
<Form noValidate onSubmit={handleSubmit} className="w-100">
<Form.Control
type="email"
placeholder="Email"
name="email"
size="lg"
className="mb-2 w-100"
onChange={handleChange}
isInvalid={
errorMsg.indexOf("mail") !== -1 ||
errorMsg.indexOf("identifier") !== -1
}
/>
<Form.Control
type="text"
placeholder="Password"
name="password"
size="lg"
className="mb-2 w-100"
onChange={handleChange}
isInvalid={errorMsg !== ""}
/>
<Form.Control.Feedback type="invalid">{errorMsg}</Form.Control.Feedback>
<Button variant="primary" type="submit" size="lg" className="w-100">
<b>Log In</b>
</Button>
</Form>
<Button
variant="link"
type="link"
className="w-100"
id="link-button"
onClick={onClickForgottenPswd}
>
Forgotten password?
</Button>
</>
);
};
export default Login;

View File

@ -0,0 +1,64 @@
import React from "react";
import { Row } from "react-bootstrap";
import StorageImage from "./StorageImage";
import { useSelector } from "react-redux";
const Message = (props) => {
const { message, ...rest } = props;
const userID = useSelector((state) => state.user.id);
const senderStyle = {
width: "75%",
backgroundColor: "dodgerblue",
color: "white",
padding: "16px",
borderRadius: "16px",
};
const receiverStyle = {
width: "75%",
backgroundColor: "lightgray",
color: "black",
padding: "16px",
borderRadius: "16px",
};
return (
<Row
{...rest}
className={
message.sender === userID
? "justify-content-end"
: "justify-content-start"
}
>
<div
style={{
fontSize: "12px",
margin: "auto",
}}
>
{new Date(message.timestamp).toLocaleString()}
</div>
{message.isPhoto && (
<div className="w-100 p-3">
<StorageImage
alt=""
storagePath={message.photoURL}
style={{
display: "block",
width: "50%",
margin: "auto",
}}
/>
</div>
)}
<p style={message.sender === userID ? senderStyle : receiverStyle}>
{message.text}
</p>
</Row>
);
};
export default Message;

View File

@ -0,0 +1,89 @@
import React from "react";
import { Card, Row, Col } from "react-bootstrap";
import { Link } from "react-router-dom";
import { handleClickLink } from "./helper";
import ResponsiveImage from "./ResponsiveImage";
import { useSelector } from "react-redux";
const MiniFriends = (props) => {
const { user, linkHandling, ...rest } = props;
const { linkRefs, linkState } = linkHandling;
const [activeLink, setActiveLink] = linkState;
const { friends: friendsLinkRef } = linkRefs;
const NUMBER_OF_FRIENDS = 9;
const friendsLink =
user.index && user.index > 0
? `/fakebook/${user.lastname}.${user.firstname}.${user.index}/Friends`
: `/fakebook/${user.lastname}.${user.firstname}/Friends`;
function handleClick() {
handleClickLink(
{ currentTarget: friendsLinkRef.current },
activeLink,
setActiveLink
);
}
const users = useSelector((state) => state.users);
return (
<Card {...rest}>
<Card.Body>
<Card.Title>
<Link to={friendsLink} className="text-body" onClick={handleClick}>
<b>Friends</b>
</Link>
</Card.Title>
<Card.Subtitle className="text-muted">
{users.length} friends
</Card.Subtitle>
<Row>
{users.map((user, index) => {
const userProfileURL =
user.index && user.index > 0
? `/fakebook/${user.lastname}.${user.firstname}.${user.index}`
: `/fakebook/${user.lastname}.${user.firstname}`;
const userName = `${user.firstname} ${user.lastname}`;
return (
//we render maximum 9 friends
index < NUMBER_OF_FRIENDS && (
<Col
key={index}
xs={4}
className="m-0"
style={{
paddingLeft: "3px",
paddingRight: "3px",
paddingTop: "0",
paddingBottom: "3px",
}}>
<ResponsiveImage
photo={user.profilePictureURL}
width="100%"
height="100%"
useStoragePath="true"
/>
<Link
to={userProfileURL}
className="text-body"
onClick={handleClick}>
<div className="w-100" style={{ height: "2.5em" }}>
<p style={{ fontSize: "0.9em" }}>
<b>{userName}</b>
</p>
</div>
</Link>
</Col>
)
);
})}
</Row>
</Card.Body>
</Card>
);
};
export default MiniFriends;

View File

@ -0,0 +1,74 @@
import React from "react";
import { Card, Row, Col } from "react-bootstrap";
import { Link } from "react-router-dom";
import ResponsiveImage from "./ResponsiveImage";
import { handleClickLink } from "./helper";
const MiniPhotos = (props) => {
const { user, userID, linkHandling, ...rest } = props;
const { linkRefs, linkState } = linkHandling;
const [activeLink, setActiveLink] = linkState;
const { photos: photosLinkRef } = linkRefs;
const photos = user.photos;
const NUMBER_OF_PHOTOS = 9;
const photosLink = `/fakebook/${user.lastname}.${user.firstname}/Photos`;
function handleClick() {
handleClickLink(
{ currentTarget: photosLinkRef.current },
activeLink,
setActiveLink
);
}
return (
<Card {...rest}>
<Card.Body>
<Card.Title>
<Link to={photosLink} className="text-body" onClick={handleClick}>
<b>Photos</b>
</Link>
</Card.Title>
<Row>
{photos.map((photo, index) => {
return (
//we only render maximum 9 photos
index < NUMBER_OF_PHOTOS && (
<Col
key={index}
xs={4}
className="m-0"
style={{
paddingLeft: "3px",
paddingRight: "3px",
paddingTop: "0",
paddingBottom: "0",
}}>
<Link
to={`/fakebook/photo/${userID}/${index}`}
className="text-body"
onClick={handleClick}
tabIndex="-1">
<ResponsiveImage
photo={photo}
userID={userID}
index={index}
width="100%"
height="100%"
/>
</Link>
</Col>
)
);
})}
</Row>
</Card.Body>
</Card>
);
};
export default MiniPhotos;

View File

@ -0,0 +1,38 @@
import React from "react";
import { useParams } from "react-router-dom";
import Photos from "./Photos";
import Friends from "./Friends";
import Posts from "./Posts";
const NestedRoute = (props) => {
const { itemId } = useParams();
const { userID, openFileInput, linkHandling } = props;
if (itemId === "Photos")
return (
<Photos
userID={userID}
openFileInput={openFileInput}
//the rendering of the component changes the activeLink
linkHandling={linkHandling}
/>
);
if (itemId === "Friends")
return (
<Friends
//the rendering of the component changes the activeLink
linkHandling={linkHandling}
/>
);
else
return (
<Posts
userID={userID}
//the rendering of the component changes the activeLink
linkHandling={linkHandling}
/>
);
};
export default NestedRoute;

View File

@ -0,0 +1,74 @@
import React, { useState } from "react";
import { Form, Button, Modal, Alert } from "react-bootstrap";
import { useSelector, useDispatch } from "react-redux";
import { sendPasswordReminder } from "../backend/backend";
import { errorOccured } from "../features/user/userSlice";
const PasswordReminderModal = (props) => {
const { show, onHide, onExit } = props;
const errorMsg = useSelector((state) => state.user.error);
const dispatch = useDispatch();
const [email, setEmail] = useState("");
function handleChange(e) {
e.preventDefault();
const input = e.target;
setEmail(input.value);
}
function handleClickSend() {
sendPasswordReminder(email)
.then(() => {
onHide();
})
.catch((error) => dispatch(errorOccured(error.message)));
}
return (
<Modal show={show} onHide={onHide} onExited={onExit}>
<Modal.Header>
<Modal.Title>
<strong className="fs-2">Password Reset Email</strong>
</Modal.Title>
</Modal.Header>
<Modal.Body>
{errorMsg !== "" && <Alert variant="danger">{errorMsg}</Alert>}
<Form.Group>
<Form.Label>
Please enter the email address associated to your account to receive
a password reset email.
</Form.Label>
<Form.Control
type="email"
placeholder="Email"
name="email"
size="lg"
onChange={handleChange}
value={email}
/>
</Form.Group>
</Modal.Body>
<Modal.Footer>
<hr />
<Button
variant="light"
onClick={() => {
onHide();
dispatch(errorOccured(""));
}}
>
<b>Cancel</b>
</Button>
<Button variant="primary" onClick={handleClickSend}>
<b className="px-2">Send</b>
</Button>
</Modal.Footer>
</Modal>
);
};
export default PasswordReminderModal;

View File

@ -0,0 +1,79 @@
import React, { useState, useEffect } from "react";
import StorageImage from "./StorageImage";
import { Row, Col, Carousel } from "react-bootstrap";
import { useHistory, useParams, useLocation } from "react-router-dom";
import { useSelector } from "react-redux";
const PhotoViewer = () => {
const { userID, n } = useParams();
const users = useSelector((state) => state.users);
const photos = users.find((user) => user.userID === userID).photos;
const [activeIndex, setActiveIndex] = useState(Number(n));
const history = useHistory();
const location = useLocation();
useEffect(() => {
const locArr = location.pathname.split("/");
const index = Number(locArr.pop());
setActiveIndex(index);
}, [location]);
const handleSelect = (selectedIndex, e) => {
history.push(`/fakebook/photo/${userID}/${selectedIndex}`);
};
return (
<Row
className="bg-200"
style={{
position: "relative",
top: "50px",
height: "89vh",
}}
>
<Col md={9} className="h-100" style={{ backgroundColor: "black" }}>
<Carousel
className="w-100 h-100"
interval={null}
indicators={false}
activeIndex={activeIndex}
onSelect={handleSelect}
>
{photos.map((photo, index) => {
return (
<Carousel.Item
key={index}
style={{
width: "100%",
height: "89vh",
}}
>
<StorageImage
storagePath={`/${userID}/${photo.fileName}`}
alt=""
style={{
position: "absolute",
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
width: "100%",
maxWidth: "600px",
height: "100%",
objectFit: "contain",
}}
></StorageImage>
</Carousel.Item>
);
})}
</Carousel>
</Col>
<Col></Col>
</Row>
);
};
export default PhotoViewer;

72
src/components/Photos.jsx Normal file
View File

@ -0,0 +1,72 @@
import React, { useEffect } from "react";
import { Row, Col, Card, Button } from "react-bootstrap";
import { Link, useRouteMatch } from "react-router-dom";
import ResponsiveImage from "./ResponsiveImage";
import { handleClickLink } from "./helper";
import { useSelector } from "react-redux";
const Photos = (props) => {
const { url } = useRouteMatch();
const { userID, openFileInput, linkHandling } = props;
const { linkRefs, linkState } = linkHandling;
const photosLinkRef = linkRefs.photos;
const [activeLink, setActiveLink] = linkState;
const users = useSelector((state) => state.users);
const currentUserID = useSelector((state) => state.user.id);
const isCurrentUser = userID === currentUserID;
const user = users.find((user) => user.userID === userID);
const photos = user.photos;
useEffect(() => {
handleClickLink(
{ currentTarget: photosLinkRef.current },
activeLink,
setActiveLink
);
}, [activeLink, photosLinkRef, setActiveLink]);
return (
<Card variant="light" className="w-100">
<Card.Body>
<Card.Title>
<Link to={url} className="text-body">
<b>Photos</b>
</Link>
{isCurrentUser && (
<Button
variant="link"
style={{
textDecoration: "none",
float: "right",
}}
onClick={openFileInput}
>
<b>Add Photos</b>
</Button>
)}
</Card.Title>
<Row className="w-100">
{photos.map((photo, index) => {
return (
<Col key={index} xs={6} sm={4} md={3} lg={2} className="p-1">
<Link to={`/fakebook/photo/${userID}/${index}`}>
<ResponsiveImage
width="100%"
height="100%"
userID={userID}
photo={photo}
index={index}
/>
</Link>
</Col>
);
})}
</Row>
</Card.Body>
</Card>
);
};
export default Photos;

View File

@ -0,0 +1,63 @@
.scrolling-container {
width: 102.5%;
max-height: 250px;
overflow-y: scroll;
}
.textarea {
outline: none;
border: none;
resize: none;
overflow-y: hidden;
}
.img-container {
width: 100%;
border: 2px solid lightgray;
border-radius: 10px;
padding: 10px;
position: relative;
}
.img-to-post {
border-radius: 10px;
}
.close-btn-container {
display: flex;
justify-content: center;
align-items: center;
background: white;
width: 26px;
height: 26px;
border-radius: 50%;
position: absolute;
top: 20px;
right: 20px;
}
.add-to-post {
border: 2px solid lightgray;
border-radius: 10px;
padding: 16px;
font-size: 14px;
}
.comment-btn {
background-color: #e9ecef;
}
.video-container {
width: 100%;
border: 2px solid lightgray;
border-radius: 10px;
padding: 10px;
position: relative;
padding-bottom: 56.25%; /* 16:9, for an aspect ratio of 1:1 change to this value to 100% */
}
.react-player {
position: absolute;
top: 0;
left: 0;
}

View File

@ -0,0 +1,223 @@
import React, { useState } from "react";
import { Modal, Button, CloseButton } from "react-bootstrap";
import StorageImage from "./StorageImage";
import ProfileLink from "./ProfileLink";
import UploadPhoto from "./UploadPhoto";
import { HiOutlinePhotograph } from "react-icons/hi";
import { AiFillYoutube } from "react-icons/ai";
import "./PostModal.css";
import { handleTextareaChange, addPhoto, delPhoto } from "./helper";
import { useSelector } from "react-redux";
import { upload } from "../backend/backend";
const PostModal = (props) => {
const { show, onClose, setText, isYoutubeBtnPressed, placeholder } = props;
const user = useSelector((state) => state.currentUser);
const userID = useSelector((state) => state.user.id);
const WELCOME_TEXT = `For adding YouTube video do the following:
1. copy link of the video from the addresse bar of your browser
2. press YouTube button again to upload the YouTube video to your post
3. add your text for the post
4. push the post button`;
const INIT_POST = {
userID: `${userID}`,
text: "",
isPhoto: false,
photoURL: "",
isYoutube: false,
youtubeURL: "",
likes: [],
};
const [post, setPost] = useState(INIT_POST);
function handleChange(e) {
const value = handleTextareaChange({
e: e,
state: post,
setState: setPost,
});
setPostBtnEnabled(value);
}
function setPostBtnEnabled(value) {
if (value === "") setBtnEnabled(false);
else setBtnEnabled(true);
setText(value);
}
const [isBtnEnabled, setBtnEnabled] = useState(false);
let variant, disabled;
if (isBtnEnabled) {
variant = "primary";
disabled = false;
} else {
variant = "secondary";
disabled = true;
}
const [showUploadPhotoDlg, setShowUploadPhotoDlg] = useState(false);
function addPhotoToPost(file) {
addPhoto({
state: post,
setState: setPost,
file: file,
userID: userID,
});
setBtnEnabled(true);
}
function deletePhoto() {
delPhoto({
state: post,
setState: setPost,
user: user,
userID: userID,
sideEffect: setPostBtnAsSideEffect,
});
}
function setPostBtnAsSideEffect() {
if (post.text === "" && !post.isYoutube) setBtnEnabled(false);
}
function uploadPost() {
upload(post).then(() => {
setPost(INIT_POST);
setText("");
onClose();
});
}
function addYoutubeVideo() {
const url = post.text;
const URL_PATTERN = "https://www.youtube.com/watch?v=";
const MOBILE_URL_PATTERN = "https://m.youtube.com/watch?v=";
if (!url.startsWith(URL_PATTERN) && !url.startsWith(MOBILE_URL_PATTERN))
return;
let patternLength;
if (url.startsWith(URL_PATTERN)) patternLength = URL_PATTERN.length;
else patternLength = MOBILE_URL_PATTERN.length;
const videoID = url.slice(patternLength);
const youtubeURL = `https://www.youtube.com/embed/${videoID}`;
const newPost = { ...post };
newPost.isYoutube = true;
newPost.youtubeURL = youtubeURL;
newPost.text = "";
setPost(newPost);
setText("");
setBtnEnabled(true);
}
function deleteYoutubeVideo() {
const newPost = { ...post };
newPost.isYoutube = false;
newPost.youtubeURL = "";
setPost(newPost);
if (post.text === "" && !post.isPhoto) setBtnEnabled(false);
}
function getPlaceholder() {
if (isYoutubeBtnPressed && !post.isYoutube) return WELCOME_TEXT;
else return placeholder;
}
function getRows() {
if (getPlaceholder() === WELCOME_TEXT && post.text === "") return 7;
else return 3;
}
return (
<>
<Modal show={show} onHide={onClose}>
<Modal.Header closeButton>
<div className="w-100 d-flex justify-content-center">
<Modal.Title>
<b>Create Post</b>
</Modal.Title>
</div>
</Modal.Header>
<Modal.Body>
<ProfileLink user={user} size="45" fullname="true" bold="true" />
<div className="mt-2 scrolling-container">
<textarea
type="text"
onChange={handleChange}
className="w-100 mt-2 textarea"
placeholder={getPlaceholder()}
rows={getRows()}
value={post.text}></textarea>
{post.isPhoto && (
<div className="mb-2 img-container">
<StorageImage
alt=""
storagePath={`/${post.photoURL}`}
className="w-100 img-to-post"
/>
<div className="close-btn-container">
<CloseButton onClick={deletePhoto} />
</div>
</div>
)}
{post.isYoutube && (
<div className="mb-2 video-container">
<iframe
src={post.youtubeURL}
title="YouTube video player"
frameBorder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope"
allowFullScreen></iframe>
<div className="close-btn-container">
<CloseButton onClick={deleteYoutubeVideo} />
</div>
</div>
)}
</div>
<div className="w-100 my-2 add-to-post">
<b>Add to your post</b>
<Button
className="ml-2 add-photo-btn"
variant="light"
size="sm"
onClick={addYoutubeVideo}
disabled={post.isPhoto || post.isYoutube}>
<AiFillYoutube
size="26px"
className="text-danger"
aria-label="YouTube"
/>
</Button>
<Button
className="ml-2 add-photo-btn"
variant="light"
size="sm"
onClick={() => setShowUploadPhotoDlg(true)}
disabled={post.isPhoto || post.isYoutube}>
<HiOutlinePhotograph
size="26px"
className="text-success"
aria-label="photo"
/>
</Button>
</div>
<Button
variant={variant}
className="w-100 mt-3"
disabled={disabled}
onClick={uploadPost}>
<b>Post</b>
</Button>
</Modal.Body>
</Modal>
<UploadPhoto
show={showUploadPhotoDlg}
setShow={setShowUploadPhotoDlg}
updateDatabase={addPhotoToPost}
/>
</>
);
};
export default PostModal;

View File

@ -0,0 +1,30 @@
import React from "react";
import CreatePost from "./CreatePost";
import DisplayPost from "./DisplayPost";
import { useSelector } from "react-redux";
const PostView = () => {
const posts = useSelector((state) => state.posts);
return (
<>
<CreatePost
isCurrentUser={true}
className="mw-100 m-auto p-0"
style={{ width: "450px" }}
/>
{posts.map((post, index) => {
return (
<DisplayPost
key={index}
post={post}
className="mw-100 mx-auto my-2"
style={{ width: "450px" }}
/>
);
})}
</>
);
};
export default PostView;

9
src/components/Posts.css Normal file
View File

@ -0,0 +1,9 @@
.posts {
height: 1000px;
}
@media (max-width: 600px) {
.posts {
height: auto;
}
}

64
src/components/Posts.jsx Normal file
View File

@ -0,0 +1,64 @@
import React, { useEffect } from "react";
import { Row, Col } from "react-bootstrap";
import CreatePost from "./CreatePost";
import MiniPhotos from "./MiniPhotos";
import MiniFriends from "./MiniFriends";
import DisplayUserPost from "./DisplayUserPost";
import { handleClickLink } from "./helper";
import { useSelector } from "react-redux";
import "./Posts.css";
const Posts = (props) => {
const { userID, linkHandling } = props;
const { linkRefs, linkState } = linkHandling;
const [activeLink, setActiveLink] = linkState;
const { posts: postsLinkRef } = linkRefs;
const users = useSelector((state) => state.users);
const currentUserID = useSelector((state) => state.user.id);
const isCurrentUser = userID === currentUserID;
const user = users.find((user) => user.userID === userID);
useEffect(() => {
handleClickLink(
{ currentTarget: postsLinkRef.current },
activeLink,
setActiveLink
);
}, [activeLink, postsLinkRef, setActiveLink]);
return (
<Row className="w-100 posts">
<Col sm={5} className="mh-100 overflow-hidden">
<MiniPhotos
user={user}
userID={userID}
linkHandling={linkHandling}
className="my-2"
/>
<MiniFriends user={user} linkHandling={linkHandling} className="my-2" />
</Col>
<Col sm={7} className="mh-100 overflow-auto hide-scrollbar bg-200">
<CreatePost
firstname={user.firstname}
isCurrentUser={isCurrentUser}
className="mt-2"
/>
{user.posts.map((postID, index) => {
return (
<DisplayUserPost
key={index}
postID={postID}
className="mx-auto my-2"
/>
);
})}
</Col>
</Row>
);
};
export default Posts;

View File

@ -0,0 +1,68 @@
.grad {
background-image: linear-gradient(rgb(48, 48, 48), white, white);
}
.profile-col {
max-width: 960px;
}
.background-pic-container {
position: relative;
height: 380px;
min-height: 250px;
}
.background-pic {
display: block;
width: 100%;
height: 105%;
min-height: 250px;
max-height: 750px;
object-fit: cover;
margin: auto;
margin-top: 3.5%;
border-radius: 13px;
pointer-events: none;
z-index: -1;
}
.background-pic-button {
position: absolute;
bottom: 15px;
right: 30px;
}
.profile-pic-container {
border: 5px solid white;
border-radius: 95px;
position: absolute;
bottom: -12%;
left: 50%;
margin-left: -95px;
}
.profile-pic-button {
border-radius: 22px;
height: 44px;
width: 44px;
position: absolute;
bottom: 8%;
left: 75%;
}
/*centers posts in profile component in vertical mobile view*/
.posts > div,
.profile-col {
padding-right: 0;
}
/*removes caret from background-pic-button*/
.background-pic-button::after {
content: none;
}
@media (max-width: 640px) {
.background-pic-button span {
display: none;
}
}

311
src/components/Profile.jsx Normal file
View File

@ -0,0 +1,311 @@
import React, { useState, useEffect, useRef } from "react";
import {
Row,
Col,
DropdownButton,
Dropdown,
Button,
Nav,
Navbar,
} from "react-bootstrap";
import {
Link,
Switch,
Route,
useRouteMatch,
useParams,
} from "react-router-dom";
import { MdPhotoCamera } from "react-icons/md";
import { IoTrashOutline } from "react-icons/io5";
import { ImUpload2 } from "react-icons/im";
import { HiOutlinePhotograph } from "react-icons/hi";
import CircularImage from "./CircularImage";
import NestedRoute from "./NestedRoute";
import RemoveCoverPhotoDlg from "./RemoveCoverPhotoDlg";
import SelectBgPhotoModal from "./SelectBgPhotoModal";
import UpdateProfilePicModal from "./UpdateProfilePicModal";
import UploadPhoto from "./UploadPhoto";
import Posts from "./Posts";
import StorageImage from "./StorageImage";
import "./Profile.css";
import { useDispatch, useSelector } from "react-redux";
import { updateProfile } from "../backend/backend";
import { linkUpdated } from "../features/link/linkSlice";
const Profile = (props) => {
const { userName } = useParams();
const userID = useSelector((state) => state.user.id);
const users = useSelector((state) => state.users);
const link = useSelector((state) => state.link);
const user = () => {
const userNames = users.map((user) => {
if (!user.index || user.index === 0)
return `${user.lastname}.${user.firstname}`;
else return `${user.lastname}.${user.firstname}.${user.index}`;
});
const index = userNames.indexOf(userName);
const user = users[index];
return user;
};
const userId = () => user().userID;
const isCurrentUser = userID === userId();
let { firstname, lastname, profilePictureURL, backgroundPictureURL, photos } =
user();
const [showRemoveCoverPhotoDlg, setShowRemoveCoverPhotoDlg] = useState(false);
const [showSelectPhoto, setShowSelectPhoto] = useState(false);
const [showUpdateProfilePic, setShowUpdateProfilePic] = useState(false);
const [showUploadPhotoDlg, setShowUploadPhotoDlg] = useState(false);
const [nameOfURL, setNameOfURL] = useState("backgroundPictureURL");
const [activeLink, setActiveLink] = useState(null);
//we need the refs to handle the activeLink changes
const photosLinkRef = useRef(null);
const friendsLinkRef = useRef(null);
const postsLinkRef = useRef(null);
const linkHandlingProps = {
linkRefs: {
photos: photosLinkRef,
friends: friendsLinkRef,
posts: postsLinkRef
},
linkState: [activeLink, setActiveLink]
}
const { url, path } = useRouteMatch();
function openFileInput(nameOfURL) {
setNameOfURL(nameOfURL);
setShowUploadPhotoDlg(true);
}
function handleSelect(key) {
switch (key) {
case "3":
setShowRemoveCoverPhotoDlg(true);
break;
case "2":
openFileInput("backgroundPictureURL");
break;
case "1":
setShowSelectPhoto(true);
break;
default:
return;
}
}
function closeDlg() {
setShowRemoveCoverPhotoDlg(false);
}
function removeCoverPhoto() {
closeDlg();
return updateProfile({ backgroundPictureURL: "background-server.jpg" });
}
function hideBgPhotoModal() {
setShowSelectPhoto(false);
}
function handleBgPhotoClick(event) {
hideBgPhotoModal();
handlePhotoClick(event, "backgroundPictureURL");
}
function hideProfilePicModal() {
setShowUpdateProfilePic(false);
}
function handleUploadProfilePicClick() {
hideProfilePicModal();
openFileInput("profilePictureURL");
}
function handleProfilePicClick(event) {
hideProfilePicModal();
handlePhotoClick(event, "profilePictureURL");
}
function updatePhotos(file) {
const newPhoto = { fileName: file.name };
const filenames = photos.map((photo) => photo.fileName);
const newPhotos = [...photos];
if (filenames.indexOf(file.name) === -1) {
newPhotos.push(newPhoto);
}
const newProfile = { photos: newPhotos };
if (nameOfURL !== "") newProfile[nameOfURL] = `${userID}/${file.name}`;
return updateProfile(newProfile);
}
function handlePhotoClick(e, name) {
const index = Number(e.target.id);
const photo = photos[index];
const storagePath = `${userID}/${photo.fileName}`;
return updateProfile({ [name]: storagePath });
}
const dispatch = useDispatch();
useEffect(() => {
//we set the active link to the profile link when it renders
//unless we are on the friends page and the window is wide
//enough to see the profile on that page
if (link.active !== "friends" || window.innerWidth < 600)
dispatch(linkUpdated("profile"));
}, [dispatch, link]);
return (
<>
<Row className="justify-content-center grad">
<Col className="m-0 p-0 profile-col">
<div className="background-pic-container">
<StorageImage
className="background-pic"
storagePath={backgroundPictureURL}
alt=""
/>
{isCurrentUser && (
<DropdownButton
variant="light"
className="background-pic-button"
title={
<b>
<MdPhotoCamera className="mr-1" size="20px" />
<span>Edit Cover Photo</span>
</b>
}
size="sm">
<Dropdown.Item eventKey="1" onSelect={handleSelect}>
<HiOutlinePhotograph size="20px" className="mr-2" />
Select Photo
</Dropdown.Item>
<Dropdown.Item eventKey="2" onSelect={handleSelect}>
<ImUpload2 size="20px" className="mr-2" />
Upload Photo
</Dropdown.Item>
<Dropdown.Divider />
<Dropdown.Item eventKey="3" onSelect={handleSelect}>
<IoTrashOutline size="20px" className="mr-2" /> Remove
</Dropdown.Item>
</DropdownButton>
)}
<div className="profile-pic-container">
<CircularImage size="180" url={profilePictureURL} />
{isCurrentUser && (
<Button
variant="light"
className="profile-pic-button"
onClick={() => setShowUpdateProfilePic(true)}>
<MdPhotoCamera size="19px" aria-label="photo" />
</Button>
)}
</div>
</div>
<h2 className="text-center mt-5">
<b>
{firstname} {lastname}
</b>
</h2>
<hr></hr>
<Navbar bg="light">
<Nav>
<Nav.Item>
<Link
key="1"
to={`${url}/Posts`}
className="nav-link mx-2"
ref={postsLinkRef}>
<b>Posts</b>
</Link>
</Nav.Item>
<Nav.Item>
<Link
key="2"
to={`${url}/Friends`}
className="nav-link mx-2"
ref={friendsLinkRef}>
<b>Friends</b> {users.length}
</Link>
</Nav.Item>
<Nav.Item>
<Link
key="3"
to={`${url}/Photos`}
className="nav-link mx-2"
ref={photosLinkRef}>
<b>Photos</b>
</Link>
</Nav.Item>
</Nav>
</Navbar>
</Col>
</Row>
<Row className="justify-content-center">
<Col className="profile-col">
<Switch>
<Route path={`${path}/:itemId`}>
<NestedRoute
userID={userId()}
openFileInput={() => openFileInput("")}
//we only need the rest to handle the changes of the activeLink
linkHandling = {linkHandlingProps}
/>
</Route>
<Route path={path}>
<Posts
userID={userId()}
//we only need the rest to handle the changes of the activeLink
linkHandling = {linkHandlingProps}
/>
</Route>
</Switch>
</Col>
</Row>
<RemoveCoverPhotoDlg
show={showRemoveCoverPhotoDlg}
removeCoverPhoto={removeCoverPhoto}
closeDlg={closeDlg}
/>
<SelectBgPhotoModal
show={showSelectPhoto}
onHide={hideBgPhotoModal}
onPhotoClick={handleBgPhotoClick}
userID={userID}
photos={photos}
/>
<UpdateProfilePicModal
show={showUpdateProfilePic}
onHide={hideProfilePicModal}
onBtnClick={handleUploadProfilePicClick}
onPhotoClick={handleProfilePicClick}
userID={userID}
photos={photos}
/>
<UploadPhoto
show={showUploadPhotoDlg}
setShow={setShowUploadPhotoDlg}
updateDatabase={updatePhotos}
userID={userID}
/>
</>
);
};
export default Profile;

View File

@ -0,0 +1,35 @@
import React from "react";
import CircularImage from "./CircularImage";
import { Row, Col } from "react-bootstrap";
const ProfileLink = (props) => {
const { size, fullname, bold, user } = props;
const { firstname, lastname, profilePictureURL, isOnline } = user;
let name;
if (fullname === "true") name = `${firstname} ${lastname}`;
else name = `${firstname}`;
return (
<Row
style={{
minWidth: "150px",
color: "inherited",
}}
>
<Col xs="auto" className="px-2 ml-2">
<CircularImage
size={size}
url={profilePictureURL}
isOnline={isOnline}
/>
</Col>
<Col className="align-self-center p-0" style={{ color: "inherited" }}>
{bold === "true" ? <b>{name}</b> : name}
</Col>
</Row>
);
};
export default ProfileLink;

View File

@ -0,0 +1,3 @@
.recent-logins {
width: 500px;
}

View File

@ -0,0 +1,19 @@
import React from "react";
import "./RecentLogins.css";
const RecentLogins = () => {
return (
<div className="recent-logins">
<h1 className="text-primary w-100">
<strong style={{ fontSize: "3.5rem" }}>fakebook</strong>
</h1>
<h3 className="w-100">
<p>
Fakebook helps you connect and share with the people in your life.
</p>
</h3>
</div>
);
};
export default RecentLogins;

View File

@ -0,0 +1,33 @@
import React from "react";
import { Modal, Button } from "react-bootstrap";
const RemoveCoverPhotoDlg = (props) => {
const { show, removeCoverPhoto, closeDlg } = props;
return (
<Modal show={show} onHide={closeDlg}>
<Modal.Header closeButton>
<Modal.Title>
<strong className="fs-2">Remove Cover Photo</strong>
</Modal.Title>
</Modal.Header>
<Modal.Body>
<div>Do you really want to remove the cover photo?</div>
</Modal.Body>
<Modal.Footer>
<Button
variant="link"
style={{ textDecoration: "none" }}
onClick={closeDlg}
>
<b>Cancel</b>
</Button>
<Button variant="primary" onClick={removeCoverPhoto}>
<b>Submit</b>
</Button>
</Modal.Footer>
</Modal>
);
};
export default RemoveCoverPhotoDlg;

View File

@ -0,0 +1,50 @@
import React from "react";
import StorageImage from "./StorageImage";
const ResponsiveImage = (props) => {
const {
width,
height,
index,
userID,
photo,
onClick,
className,
useStoragePath,
} = props;
let storagePath;
if (useStoragePath === "true") storagePath = photo;
else storagePath = `/${userID}/${photo.fileName}`;
return (
<div
key={index}
className={className}
style={{
display: "inline-block",
position: "relative",
width: `${width}`,
height: "0px",
paddingBottom: `${height}`,
}}
>
<StorageImage
alt=""
id={index}
storagePath={storagePath}
style={{
position: "absolute",
top: "0px",
left: "0px",
width: "100%",
height: "100%",
objectFit: "cover",
}}
onClick={onClick}
></StorageImage>
</div>
);
};
export default ResponsiveImage;

View File

@ -0,0 +1,38 @@
import React from "react";
import { Modal } from "react-bootstrap";
import StorageImage from "./StorageImage";
const SelectBgPhotoModal = (props) => {
const { show, onHide, onPhotoClick, userID, photos } = props;
return (
<Modal show={show} onHide={onHide} scrollable>
<Modal.Header closeButton>
<Modal.Title style={{ marginLeft: "35%" }}>
<strong>Select Photo</strong>
</Modal.Title>
</Modal.Header>
<Modal.Body>
{photos.map((photo, index) => {
return (
<StorageImage
className="m-1"
width="31%"
height="90px"
alt=""
key={index}
id={index}
storagePath={`/${userID}/${photo.fileName}`}
style={{
objectFit: "cover",
}}
onClick={onPhotoClick}
></StorageImage>
);
})}
</Modal.Body>
</Modal>
);
};
export default SelectBgPhotoModal;

View File

@ -0,0 +1,3 @@
.text-legal {
font-size: small;
}

View File

@ -0,0 +1,131 @@
import React, { useEffect, useState } from "react";
import Form from "react-bootstrap/Form";
import Row from "react-bootstrap/Row";
import Col from "react-bootstrap/Col";
import Button from "react-bootstrap/Button";
import Modal from "react-bootstrap/Modal";
import "./Signup.css";
import { createUserAccount } from "../backend/backend";
const SignupModal = (props) => {
const { show, onHide, onExit } = props;
const [user, setUser] = useState({
firstname: "",
lastname: "",
email: "",
password: "",
});
const handleChange = (e) =>
setUser({
...user,
[e.target.name]: e.target.value,
});
const [validated, setValidated] = useState(false);
const [formIsValid, setFormIsValid] = useState(false);
// Submit function (Create account)
const handleSubmit = (e) => {
e.preventDefault();
const form = e.currentTarget;
if (form.checkValidity() === true) {
createUserAccount({
firstname: user.firstname,
lastname: user.lastname,
email: user.email,
password: user.password,
});
setFormIsValid(true);
}
setValidated(true);
};
useEffect(() => {
if (formIsValid) onHide();
}, [formIsValid, onHide]);
return (
<Modal show={show} onHide={onHide} onExited={onExit}>
<Modal.Header closeButton>
<Modal.Title>
<strong className="fs-2">Sign Up</strong>
<div className="title-footer">It's quick and easy.</div>
</Modal.Title>
</Modal.Header>
<Modal.Body>
<Form noValidate validated={validated} onSubmit={handleSubmit}>
<Form.Row>
<Form.Group as={Col}>
<Form.Control
type="text"
placeholder="First name"
name="firstname"
onChange={handleChange}
required
/>
<Form.Control.Feedback type="invalid">
First name required.
</Form.Control.Feedback>
</Form.Group>
<Form.Group as={Col}>
<Form.Control
type="text"
placeholder="Surename"
name="lastname"
onChange={handleChange}
required
/>
<Form.Control.Feedback type="invalid">
Last name required.
</Form.Control.Feedback>
</Form.Group>
</Form.Row>
<Form.Row>
<Form.Group as={Col}>
<Form.Control
type="email"
placeholder="Email address"
name="email"
onChange={handleChange}
required
/>
<Form.Control.Feedback type="invalid">
Email required in the right format.
</Form.Control.Feedback>
</Form.Group>
</Form.Row>
<Form.Row>
<Form.Group as={Col}>
<Form.Control
type="text"
placeholder="New password"
name="password"
onChange={handleChange}
required
/>
<Form.Control.Feedback type="invalid">
Password required.
</Form.Control.Feedback>
</Form.Group>
</Form.Row>
<Form.Row>
<p className="text-muted p-1 text-legal">
Signing Up for fakebook you agree to share the uploaded details
with any other user. Please do not upload any sensitive data to
the app, which is built strictly for demonstration purposes.
</p>
</Form.Row>
<Row>
<Button type="submit" variant="success" className="w-50 mx-auto">
<strong>Sign Up</strong>
</Button>
</Row>
</Form>
</Modal.Body>
</Modal>
);
};
export default SignupModal;

View File

@ -0,0 +1,102 @@
import React, { useState } from "react";
import { getImageURL } from "../backend/backend";
import { useSelector, useDispatch } from "react-redux";
import placeholderImage from "../images/placeholder-image.jpg";
import fakebookAvatar from "../images/fakebook-avatar.jpeg";
import backgroundServer from "../images/background-server.jpg";
import { imageAdded, imageUrlFound } from "../features/images/imagesSlice";
import { useEffect } from "react";
const StorageImage = (props) => {
const { storagePath, alt, ...rest } = props;
const PLACEHOLDER_AVATAR_STORAGE_PATH = "fakebook-avatar.jpeg";
const PLACEHOLDER_BACKGROUND_STORAGE_PATH = "background-server.jpg";
//We use the images slice as a buffer. Fetching the actual url of the image
//in the storage takes relatively long time and uses Firebase. We render the same
//image in the app several times. Our goal to fetch the url only once for each image
//to save resources.
const images = useSelector((state) => state.images);
const dispatch = useDispatch();
const [url, setUrl] = useState(placeholderImage);
function changeStoragePath(storagePath) {
const words = storagePath.split(".");
words[words.length - 2] += "_400x400";
return words.join(".");
}
useEffect(() => {
let shouldUpdate = true;
const cleanup = () => (shouldUpdate = false);
//We filter out placeholder pictures
if (storagePath === PLACEHOLDER_AVATAR_STORAGE_PATH) {
setUrl(fakebookAvatar);
return cleanup;
}
if (storagePath === PLACEHOLDER_BACKGROUND_STORAGE_PATH) {
setUrl(backgroundServer);
return cleanup;
}
//We look for the url in images slice first
let imageIndex = images
.map((image) => image.storagePath)
.indexOf(storagePath);
if (imageIndex === -1) {
imageIndex = images
.map((image) => image.storagePath)
.indexOf(changeStoragePath(storagePath));
//If we are unable to find it anyway we add the image to the slice
if (imageIndex === -1) {
dispatch(
imageAdded({
storagePath,
url,
})
);
}
//We add the url for the image to the images slice when we have actually got it
//We also update the local state to show the right image
getImageURL(storagePath)
.then((url) => {
setUrl(url);
dispatch(
imageUrlFound({
storagePath,
url,
})
);
})
.catch((_error) => {
getImageURL(changeStoragePath(storagePath)).then((url) => {
setUrl(url);
dispatch(
imageUrlFound({
storagePath,
url,
})
);
});
});
} else {
//If we are able to find the url in the images slice we just use it instead of fetching
const newUrl = images[imageIndex].url;
//We only update the state if it contains different value and we should update because
//the component has not been unmounted by the time the promise resolves
if (newUrl !== url && shouldUpdate) setUrl(newUrl);
}
//We return a cleanup function which runs when the component unmounts. We set
//the shouldUpdate to false, so after this the state cannot be updated. If
//we don't do this React gives us error messages about state update on our
//unmounted component
return cleanup;
}, [images, storagePath, url, dispatch]);
return <img src={url} alt={alt} {...rest} />;
};
export default StorageImage;

View File

@ -0,0 +1,66 @@
import React, { useState, useEffect, useRef } from "react";
const StyledTextarea = (props) => {
const { onChange, onKeyPress, value, welcomeText, ...rest } = props;
const TEXTAREA_STYLE_INIT = {
outline: "none",
border: "none",
resize: "none",
overflow: "hidden",
background: "#e9ecef",
padding: "0",
lineHeight: "0.8em",
};
const styleInitRef = useRef(TEXTAREA_STYLE_INIT);
const [style, setStyle] = useState(TEXTAREA_STYLE_INIT);
const [textarea, setTextarea] = useState(null); //We save the textarea in the state, so the effect hook can use it
//When content changes we first change the height to auto,
//which changes back the scrollHeight property of the textarea
//to a low value and the component rerenders
function restyleTextarea(textarea) {
const newStyle = { ...style };
newStyle.height = "auto";
setStyle(newStyle);
}
//When the component has rerendered and the height is auto
//we set the height to the scrollHeight property of textarea
//This way when the height of the content decreses the textarea
//can follow it down too. Without this trick the textarea can
//grow but unable to shrink back
useEffect(() => {
if (style.height !== "auto") return;
if (!textarea) return;
const newStyle = { ...style };
newStyle.height = textarea.scrollHeight + "px";
setStyle(newStyle);
}, [style, textarea]);
//This restyles the text area, when we send the message, so the
//value goes back to "" without executing the onChange handler
useEffect(() => {
if (value === "" && textarea) setStyle(styleInitRef.current);
}, [value, textarea, styleInitRef]);
return (
<textarea
type="text"
onChange={(e) => {
onChange(e);
const textarea = e.target;
setTextarea(textarea);
restyleTextarea(textarea);
}}
onKeyPress={onKeyPress}
placeholder={welcomeText}
rows="1"
style={style}
value={value}
{...rest}
></textarea>
);
};
export default StyledTextarea;

View File

@ -0,0 +1,53 @@
.custom-drop-down-btn button {
height: 0.1em;
width: 1.7em;
border-radius: 1.7em;
border-color: #e9ecef !important;
color: black !important;
background: #e9ecef !important;
box-shadow: none;
font-size: 1.5em;
padding-bottom: 1.5em;
}
.custom-drop-down-btn button:hover {
background: lightgray !important;
}
.custom-drop-down-btn button:focus {
color: dodgerblue !important;
background-color: lightblue !important;
border-color: lightblue !important;
}
.profile {
width: 100px;
height: 46px;
background: #e9ecef;
border-radius: 23px;
caret-color: transparent;
}
.profile:hover {
background: lightgray;
}
.nav-container {
max-width: 100em;
margin: auto;
}
.titlebar {
position: fixed;
width: 100vw;
z-index: 1;
top: 0;
left: 0;
}
@media (max-width: 640px) {
.spaceing,
.first {
display: none;
}
}

137
src/components/Titlebar.jsx Normal file
View File

@ -0,0 +1,137 @@
import React, { useRef } from "react";
import { Navbar, Nav, Dropdown, DropdownButton } from "react-bootstrap";
import { Link } from "react-router-dom";
import { VscHome } from "react-icons/vsc";
import { FaFacebook } from "react-icons/fa";
import { FaUserFriends } from "react-icons/fa";
import { MdOndemandVideo } from "react-icons/md";
import { ImExit } from "react-icons/im";
import "./Titlebar.css";
import ProfileLink from "./ProfileLink";
import { signUserOut } from "../backend/backend";
import { useSelector, useDispatch } from "react-redux";
import { useEffect } from "react";
import { handleClickLink } from "./helper";
import { linkUpdated } from "../features/link/linkSlice";
import { friendsListPageSet } from "../features/accountPage/accountPageSlice";
const TitleBar = (props) => {
const refs = {
home: useRef(null),
friends: useRef(null),
watch: useRef(null),
profile: useRef(null),
};
//Get the signed in user, active link and the profileLink
const user = useSelector((state) => state.currentUser);
const link = useSelector((state) => state.link);
const profileLink = useSelector((state) => state.accountPage.profileLink);
const dispatch = useDispatch();
useEffect(() => {
handleClickLink(
{ currentTarget: refs[link.active].current },
refs[link.previous].current
);
}, [link, refs]);
// Log out function
const handleClick = () => {
signUserOut();
};
function closeFriendsListPage() {
dispatch(friendsListPageSet(false));
dispatch(linkUpdated("profile"));
}
return (
<div className="titlebar bg-light">
<Navbar bg="light" className="p-0 nav-container">
<Navbar.Brand as={Link} to="/fakebook">
<FaFacebook color="dodgerblue" fontSize="2em" className="mx-3" />
</Navbar.Brand>
<div style={{ width: "450px" }} className="spaceing" />
<Nav className="w-75 justify-content-start mr-5">
<Nav.Item className="first">
<Link to="/fakebook" className="nav-link" ref={refs.home}>
<VscHome
fontSize="2rem"
className="mx-4"
style={{ pointerEvents: "none" }}
/>
</Link>
</Nav.Item>
<Nav.Item>
<Link
to="/fakebook/friends/list"
className="nav-link"
ref={refs.friends}>
<FaUserFriends
fontSize="2rem"
className="mx-4"
style={{ pointerEvents: "none" }}
/>
</Link>
</Nav.Item>
<Nav.Item>
<Link to="/fakebook/watch" className="nav-link" ref={refs.watch}>
<MdOndemandVideo
fontSize="2rem"
className="mx-4"
style={{ pointerEvents: "none" }}
/>
</Link>
</Nav.Item>
</Nav>
<Nav className="w-25 justify-content-end align-self-center">
<Nav.Item className="align-self-center first">
<Link
to={profileLink}
className="nav-link profile"
id="profile"
onClick={closeFriendsListPage}
ref={refs.profile}>
<ProfileLink user={user} size="30" fullname="false" bold="true" />
</Link>
</Nav.Item>
<Nav.Item className="align-self-center">
<DropdownButton
title=""
className="mr-4 custom-drop-down-btn"
menuAlign="right">
<Dropdown.Item
as={Link}
to={profileLink}
onClick={closeFriendsListPage}>
<ProfileLink
user={user}
size="60"
fullname="true"
bold="true"
/>
</Dropdown.Item>
<Dropdown.Divider />
<Dropdown.Item
as={Link}
to="/fakebook/"
onClick={handleClick}
className="p-0">
<ImExit fontSize="1.5em" className="mx-4" />
<span>Log Out</span>
<div style={{ width: "20em" }}></div>
</Dropdown.Item>
</DropdownButton>
</Nav.Item>
</Nav>
</Navbar>
</div>
);
};
export default TitleBar;

View File

@ -0,0 +1,47 @@
import React from "react";
import { Modal, Button, Row } from "react-bootstrap";
import ResponsiveImage from "./ResponsiveImage";
const UpdateProfilePicModal = (props) => {
const { show, onHide, onBtnClick, onPhotoClick, userID, photos } = props;
return (
<Modal show={show} onHide={onHide} size="lg" scrollable>
<Modal.Header closeButton>
<Modal.Title style={{ margin: "auto" }}>
<strong>Update Profile Picture</strong>
</Modal.Title>
</Modal.Header>
<Modal.Body>
<Button
size="sm"
variant="outline-primary"
className="w-50 m-2 mb-3"
onClick={onBtnClick}
>
<b>+ Upload Photo</b>
</Button>
<br />
<b>Suggested Photos</b>
<Row className="m-1">
{photos.map((photo, index) => {
return (
<ResponsiveImage
key={index}
width="15%"
height="15%"
userID={userID}
photo={photo}
index={index}
onClick={onPhotoClick}
className="m-1"
/>
);
})}
</Row>
</Modal.Body>
</Modal>
);
};
export default UpdateProfilePicModal;

View File

@ -0,0 +1,34 @@
import React, { useRef, useEffect } from "react";
import { addFileToStorage } from "../backend/backend";
const UploadPhoto = (props) => {
const { show, setShow, updateDatabase } = props;
const fileInputRef = useRef(null);
useEffect(() => {
if (!show) return;
fileInputRef.current.click();
setShow(false);
}, [show, setShow]);
function onChange(event) {
event.preventDefault();
const file = event.target.files[0];
addFileToStorage(file).then(() => {
return updateDatabase(file);
});
}
return (
<input
type="file"
accept="image/*"
ref={fileInputRef}
style={{ display: "none" }}
onChange={onChange}
/>
);
};
export default UploadPhoto;

View File

@ -0,0 +1,121 @@
import React, { useEffect } from "react";
import TitleBar from "./Titlebar";
import Profile from "./Profile";
import PhotoViewer from "./PhotoViewer";
import HomePage from "./HomePage";
import FriendsListPage from "./FriendsListPage";
import { BrowserRouter, Switch, Route } from "react-router-dom";
import Container from "react-bootstrap/Container";
import { useDispatch, useSelector } from "react-redux";
import {
currentUserOffline,
currentUserOnline,
subscribeCurrentUser,
subscribeUsers,
subscribePosts,
} from "../backend/backend";
import {
friendsListPageSet,
profileLinkSet,
watchSet,
} from "../features/accountPage/accountPageSlice";
const UserAccount = (props) => {
const profileLink = useSelector((state) => state.accountPage.profileLink);
const currentUser = useSelector((state) => state.currentUser);
const users = useSelector((state) => state.users);
const isFriendsListPage = useSelector(
(state) => state.accountPage.isFriendsListPage
);
const dispatch = useDispatch();
useEffect(() => {
const unsubscribeCurrentUser = subscribeCurrentUser();
const unsubscribeUsers = subscribeUsers();
const unsubscribePosts = subscribePosts();
//We make currentUser online
currentUserOnline();
//We add event listener for the event when the user closes the browser window
const beforeunloadListener = (e) => {
//We put the user offline
currentUserOffline();
};
window.addEventListener("beforeunload", beforeunloadListener);
//we add event listener for the event when the browser window change visibility
const visibilitychangeListener = (e) => {
if (document.visibilityState === "visible") currentUserOnline();
else currentUserOffline();
};
document.addEventListener("visibilitychange", visibilitychangeListener);
return () => {
unsubscribeCurrentUser();
unsubscribeUsers();
unsubscribePosts();
};
}, []);
//We add the index of user to the profileLink if there are more users with the exact same userName
const addIndexToProfileLink = () => {
if (currentUser && currentUser.index && currentUser.index > 0) {
return `${profileLink}.${currentUser.index}`;
} else return profileLink;
};
const newProfileLink = addIndexToProfileLink();
useEffect(() => dispatch(profileLinkSet(newProfileLink)), [dispatch, newProfileLink]);
if (users.length === 0 || !currentUser) {
return <div>...Loading</div>;
}
return (
<div className="bg-200 vw-100 main-container overflow-hidden">
<Container className="w-100 p-0" fluid>
<BrowserRouter>
<TitleBar />
<Switch>
<Route
path="/fakebook/friends/list"
render={() => {
dispatch(friendsListPageSet(true));
return <FriendsListPage />;
}}
/>
<Route
path={`/fakebook/photo/:userID/:n`}
render={() => <PhotoViewer />}
/>
<Route
path="/fakebook/watch"
render={() => {
dispatch(friendsListPageSet(false));
dispatch(watchSet(true));
return <HomePage className="pt-5" />;
}}
/>
<Route
path={`/fakebook/:userName`}
render={() => {
if (isFriendsListPage) return <FriendsListPage />;
else {
return <Profile />;
}
}}
/>
<Route
path="/fakebook"
render={() => {
dispatch(friendsListPageSet(false));
dispatch(watchSet(false));
return <HomePage className="pt-5" />;
}}
/>
</Switch>
</BrowserRouter>
</Container>
</div>
);
};
export default UserAccount;

View File

@ -0,0 +1,24 @@
import React from "react";
import DisplayPost from "./DisplayPost";
import { useSelector } from "react-redux";
const VideoView = () => {
const posts = useSelector((state) => state.posts);
const videos = posts.filter((post) => post.isYoutube);
return (
<>
{videos.map((video, index) => (
<DisplayPost
key={index}
post={video}
className="mw-100 mx-auto my-2"
style={{ width: "550px" }}
/>
))}
</>
);
};
export default VideoView;

57
src/components/helper.js Normal file
View File

@ -0,0 +1,57 @@
export function handleClickLink(e, linkState, setLinkState) {
const current = e.currentTarget;
const previous = linkState;
if (previous) {
if (previous.id === "profile") previous.style.backgroundColor = "#e9ecef";
previous.style.borderBottom = "3px solid transparent";
previous.style.color = "";
}
if (current) {
if (setLinkState) setLinkState(current);
current.style.color = "dodgerblue";
if (current.id === "profile") {
current.style.backgroundColor = "lightblue";
return;
}
current.style.borderBottom = "3px solid dodgerblue";
}
}
export function handleTextareaChange(input) {
let value = input.e.target.value;
const newState = { ...input.state };
newState.text = value;
input.setState(newState);
return value;
}
export function addPhoto(input) {
const newState = { ...input.state };
newState.isPhoto = true;
newState.photoURL = `${input.userID}/${input.file.name}`;
input.setState(newState);
}
export function delPhoto(input) {
//We do not remove the photo from the storage, because in the case of multiple occurances, the removal
//of the photo causes error. If we want to take into account the storage efficiency, we have to store
//the number of occurences of each photo and only delete those from the storage, which only occure once.
//This would cause unnecessary extra logic in a demonstration app like this.
removePhotoFromPost(input.state, input.setState, input.sideEffect);
}
function removePhotoFromPost(state, setState, sideEffect) {
const newState = { ...state };
newState.isPhoto = false;
newState.photoURL = "";
setState(newState);
if (sideEffect) sideEffect();
}
export function handleKeyPress(e, save) {
if (e.shiftKey) return;
const code = e.code;
if (code !== "Enter") return;
e.preventDefault();
save();
}

View File

@ -0,0 +1,26 @@
import { createSlice } from "@reduxjs/toolkit";
export const accountPageSlice = createSlice({
name: "accountPage",
initialState: {
profileLink: "",
isWatch: false,
isFriendsListPage: false,
},
reducers: {
profileLinkSet: (state, action) => {
state.profileLink = action.payload;
},
friendsListPageSet: (state, action) => {
state.isFriendsListPage = action.payload;
},
watchSet: (state, action) => {
state.isWatch = action.payload;
},
},
});
export const { profileLinkSet, friendsListPageSet, watchSet } =
accountPageSlice.actions;
export default accountPageSlice.reducer;

View File

@ -0,0 +1,42 @@
import { createSlice } from "@reduxjs/toolkit";
export const currentUserSlice = createSlice({
name: "currentUser",
initialState: {
firstname: "",
lastname: "",
profilePictureURL: "fakebook-avatar.jpeg",
backgroundPictureURL: "background-server.jpg",
photos: [],
posts: [],
isOnline: false,
},
reducers: {
currentUserUpdated: (state, action) => {
const {
firstname,
lastname,
profilePictureURL,
backgroundPictureURL,
photos,
posts,
isOnline,
index,
} = action.payload;
state.firstname = firstname;
state.lastname = lastname;
state.profilePictureURL = profilePictureURL;
state.backgroundPictureURL = backgroundPictureURL;
state.isOnline = isOnline;
if (index) state.index = index;
state.photos = [];
state.posts = [];
photos.forEach((photo) => state.photos.push(photo));
posts.forEach((post) => state.posts.push(post));
},
},
});
export const { currentUserUpdated } = currentUserSlice.actions;
export default currentUserSlice.reducer;

View File

@ -0,0 +1,24 @@
import { createSlice } from "@reduxjs/toolkit";
export const imagesSlice = createSlice({
name: "images",
initialState: [],
reducers: {
imageAdded: (state, action) => {
const index = state
.map((image) => image.storagePath)
.indexOf(action.payload.storagePath);
if (index === -1) state.push(action.payload);
},
imageUrlFound: (state, action) => {
const index = state
.map((image) => image.storagePath)
.indexOf(action.payload.storagePath);
if (index > -1) state[index].url = action.payload.url;
},
},
});
export const { imageAdded, imageUrlFound } = imagesSlice.actions;
export default imagesSlice.reducer;

View File

@ -0,0 +1,17 @@
import { createSlice } from "@reduxjs/toolkit";
export const incomingMessagesSlice = createSlice({
name: "incomingMessages",
initialState: [],
reducers: {
incomingMessagesUpdated: (state, action) => {
const updatedState = [];
action.payload.forEach((message) => updatedState.push(message));
return updatedState;
},
},
});
export const { incomingMessagesUpdated } = incomingMessagesSlice.actions;
export default incomingMessagesSlice.reducer;

View File

@ -0,0 +1,19 @@
import { createSlice } from "@reduxjs/toolkit";
export const linkSlice = createSlice({
name: "link",
initialState: {
active: "home",
previous: "home"
},
reducers: {
linkUpdated: (state, action) => {
state.previous = state.active;
state.active = action.payload;
},
},
});
export const { linkUpdated } = linkSlice.actions;
export default linkSlice.reducer;

View File

@ -0,0 +1,17 @@
import { createSlice } from "@reduxjs/toolkit";
export const outgoingMessagesSlice = createSlice({
name: "outgoingMessages",
initialState: [],
reducers: {
outgoingMessagesUpdated: (state, action) => {
const updatedState = [];
action.payload.forEach((message) => updatedState.push(message));
return updatedState;
},
},
});
export const { outgoingMessagesUpdated } = outgoingMessagesSlice.actions;
export default outgoingMessagesSlice.reducer;

View File

@ -0,0 +1,17 @@
import { createSlice } from "@reduxjs/toolkit";
export const postsSlice = createSlice({
name: "posts",
initialState: [],
reducers: {
postsUpdated: (state, action) => {
const updatedState = [];
action.payload.forEach((post) => updatedState.push(post));
return updatedState;
},
},
});
export const { postsUpdated } = postsSlice.actions;
export default postsSlice.reducer;

View File

@ -0,0 +1,46 @@
import { createSlice } from "@reduxjs/toolkit";
export const userSlice = createSlice({
name: "user",
initialState: {
id: "",
isSignedIn: false,
isEmailVerified: false,
error: "",
isLoading: true,
},
reducers: {
signIn: (state, action) => {
// Redux Toolkit allows us to write "mutating" logic in reducers. It
// doesn't actually mutate the state because it uses the immer library,
// which detects changes to a "draft state" and produces a brand new
// immutable state based off those changes
state.id = action.payload.id;
state.displayName = action.payload.displayName;
state.isEmailVerified = action.payload.isEmailVerified;
state.isSignedIn = true;
},
signOut: (state) => {
state.isSignedIn = false;
},
errorOccured: (state, action) => {
state.error = action.payload;
},
loadingStarted: (state) => {
state.isLoading = true;
},
loadingFinished: (state) => {
state.isLoading = false;
},
},
});
export const {
signIn,
signOut,
errorOccured,
loadingStarted,
loadingFinished,
} = userSlice.actions;
export default userSlice.reducer;

View File

@ -0,0 +1,17 @@
import { createSlice } from "@reduxjs/toolkit";
export const usersSlice = createSlice({
name: "users",
initialState: [],
reducers: {
usersUpdated: (state, action) => {
const updatedState = [];
action.payload.forEach((user) => updatedState.push(user));
return updatedState;
},
},
});
export const { usersUpdated } = usersSlice.actions;
export default usersSlice.reducer;

11
src/firebaseConfig.js Normal file
View File

@ -0,0 +1,11 @@
const firebaseConfig = {
apiKey: "AIzaSyA6giH2VCl9pBrO86uGH3gcwbmeM-dYMPM",
authDomain: "fakebook-2df7b.firebaseapp.com",
projectId: "fakebook-2df7b",
storageBucket: "fakebook-2df7b.appspot.com",
messagingSenderId: "1030600439600",
appId: "1:1030600439600:web:98617c1d503260115cfaf5",
measurementId: "G-YN35E2SN2E",
};
export default firebaseConfig;

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

BIN
src/images/friends.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

@ -1,68 +1,19 @@
:root {
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
font-weight: 400;
color-scheme: light dark;
color: rgba(255, 255, 255, 0.87);
background-color: #242424;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
a {
font-weight: 500;
color: #646cff;
text-decoration: inherit;
}
a:hover {
color: #535bf2;
font-size: 16px;
}
body {
margin: 0;
display: flex;
place-items: center;
min-width: 320px;
min-height: 100vh;
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
"Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
overflow-x: hidden;
background-color: #e9ecef;
}
h1 {
font-size: 3.2em;
line-height: 1.1;
}
button {
border-radius: 8px;
border: 1px solid transparent;
padding: 0.6em 1.2em;
font-size: 1em;
font-weight: 500;
font-family: inherit;
background-color: #1a1a1a;
cursor: pointer;
transition: border-color 0.25s;
}
button:hover {
border-color: #646cff;
}
button:focus,
button:focus-visible {
outline: 4px auto -webkit-focus-ring-color;
}
@media (prefers-color-scheme: light) {
:root {
color: #213547;
background-color: #ffffff;
}
a:hover {
color: #747bff;
}
button {
background-color: #f9f9f9;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New",
monospace;
}

View File

@ -1,5 +1,12 @@
import { render } from 'preact'
import './index.css'
import { App } from './app.jsx'
import App from './app.jsx'
import store from './app/store'
import { Provider } from 'react-redux'
render(<App />, document.getElementById('app'))
render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById("app")
);