Make app work with Preact with Firebase in dev
This commit is contained in:
parent
c4e85898ba
commit
a943e53ed0
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"projects": {
|
||||
"default": "fakebook-2df7b"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"functions": [
|
||||
{
|
||||
"source": "functions",
|
||||
"codebase": "default",
|
||||
"ignore": [
|
||||
"node_modules",
|
||||
".git",
|
||||
"firebase-debug.log",
|
||||
"firebase-debug.*.log"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
File diff suppressed because it is too large
Load Diff
11
package.json
11
package.json
|
@ -9,7 +9,16 @@
|
|||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"preact": "^10.25.3"
|
||||
"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",
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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,
|
||||
},
|
||||
});
|
142
src/app.jsx
142
src/app.jsx
|
@ -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();
|
||||
|
||||
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 (
|
||||
<>
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
</>
|
||||
)
|
||||
</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;
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
});
|
|
@ -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 });
|
||||
}
|
|
@ -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;
|
|
@ -0,0 +1,13 @@
|
|||
.comment-img-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.img-to-comment {
|
||||
width: 30%;
|
||||
}
|
||||
|
||||
.comment-btn {
|
||||
border-radius: 18px;
|
||||
}
|
|
@ -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;
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
|
@ -0,0 +1,8 @@
|
|||
.navitem {
|
||||
margin-left: 10px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.navitem:hover {
|
||||
background-color: lightgray;
|
||||
}
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
|
@ -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;
|
|
@ -0,0 +1,9 @@
|
|||
.posts {
|
||||
height: 1000px;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.posts {
|
||||
height: auto;
|
||||
}
|
||||
}
|
|
@ -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;
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
|
@ -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;
|
|
@ -0,0 +1,3 @@
|
|||
.recent-logins {
|
||||
width: 500px;
|
||||
}
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -0,0 +1,3 @@
|
|||
.text-legal {
|
||||
font-size: small;
|
||||
}
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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();
|
||||
}
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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 |
Binary file not shown.
After Width: | Height: | Size: 13 KiB |
Binary file not shown.
After Width: | Height: | Size: 14 KiB |
|
@ -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;
|
||||
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;
|
||||
}
|
||||
|
|
11
src/main.jsx
11
src/main.jsx
|
@ -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")
|
||||
);
|
||||
|
|
Loading…
Reference in New Issue