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
41
package.json
41
package.json
|
@ -1,18 +1,27 @@
|
||||||
{
|
{
|
||||||
"name": "fakebook-ainiro",
|
"name": "fakebook-ainiro",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"preact": "^10.25.3"
|
"preact": "^10.25.3",
|
||||||
},
|
"@reduxjs/toolkit": "^1.8.3",
|
||||||
"devDependencies": {
|
"bootstrap": "^4.6.0",
|
||||||
"@preact/preset-vite": "^2.9.3",
|
"firebase": "^9.9.4",
|
||||||
"vite": "^6.0.5"
|
"react-bootstrap": "^1.5.2",
|
||||||
}
|
"react-dom": "^17.0.2",
|
||||||
|
"react-icons": "^4.2.0",
|
||||||
|
"react-player": "^2.12.0",
|
||||||
|
"react-redux": "^8.0.2",
|
||||||
|
"react-router-dom": "^5.2.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@preact/preset-vite": "^2.9.3",
|
||||||
|
"vite": "^6.0.5"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
},
|
||||||
|
});
|
146
src/app.jsx
146
src/app.jsx
|
@ -1,43 +1,109 @@
|
||||||
import { useState } from 'preact/hooks'
|
import React, { useState, useCallback, useEffect } from "react";
|
||||||
import preactLogo from './assets/preact.svg'
|
import "./App.css";
|
||||||
import viteLogo from '/vite.svg'
|
import SignupModal from "./components/SignupModal.jsx";
|
||||||
import './app.css'
|
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() {
|
function App() {
|
||||||
const [count, setCount] = useState(0)
|
const user = useSelector((state) => state.user);
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
return (
|
useEffect(() => {
|
||||||
<>
|
const unsubscribe = subscribeAuth();
|
||||||
<div>
|
return unsubscribe;
|
||||||
<a href="https://vite.dev" target="_blank">
|
}, []);
|
||||||
<img src={viteLogo} class="logo" alt="Vite logo" />
|
|
||||||
</a>
|
//Handle the modal
|
||||||
<a href="https://preactjs.com" target="_blank">
|
const [show, setShow] = useState(false);
|
||||||
<img src={preactLogo} class="logo preact" alt="Preact logo" />
|
|
||||||
</a>
|
function handleClose() {
|
||||||
</div>
|
setShow(false);
|
||||||
<h1>Vite + Preact</h1>
|
}
|
||||||
<div class="card">
|
|
||||||
<button onClick={() => setCount((count) => count + 1)}>
|
function handleShow() {
|
||||||
count is {count}
|
setShow(true);
|
||||||
</button>
|
}
|
||||||
<p>
|
|
||||||
Edit <code>src/app.jsx</code> and save to test HMR
|
const handleCloseCallback = useCallback(handleClose, []);
|
||||||
</p>
|
|
||||||
</div>
|
//get the first and lastName for the route of the profile
|
||||||
<p>
|
const name =
|
||||||
Check out{' '}
|
(user && user.displayName && user.displayName.trim().split(" ")) || [];
|
||||||
<a
|
|
||||||
href="https://preactjs.com/guide/v10/getting-started#create-a-vite-powered-preact-app"
|
const lastName = name.pop();
|
||||||
target="_blank"
|
|
||||||
>
|
const firstName = name.join(" ");
|
||||||
create-preact
|
|
||||||
</a>
|
const profileLink = `/fakebook/${lastName}.${firstName}`;
|
||||||
, the official Preact + Vite starter
|
|
||||||
</p>
|
|
||||||
<p class="read-the-docs">
|
useEffect(() => dispatch(profileLinkSet(profileLink)), [profileLink, dispatch]);
|
||||||
Click on the Vite and Preact logos to learn more
|
|
||||||
</p>
|
//handling the password reminder button
|
||||||
</>
|
const [isModalSignup, setModalSignup] = useState(true);
|
||||||
)
|
|
||||||
|
function handleClickPasswordReminderBtn() {
|
||||||
|
setModalSignup(false);
|
||||||
|
handleShow();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user.isLoading) {
|
||||||
|
return <div>...Loading</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user.isSignedIn && !user.error) {
|
||||||
|
if (user.isEmailVerified) return <UserAccount />;
|
||||||
|
else return <></>;
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<Col className="bg-200 vh-100">
|
||||||
|
<Row className="h-100 align-items-center">
|
||||||
|
<Col
|
||||||
|
lg={{ span: 5, offset: 1 }}
|
||||||
|
className="d-flex justify-content-center">
|
||||||
|
<RecentLogins />
|
||||||
|
</Col>
|
||||||
|
<Col lg={5} className="bg-200 d-flex justify-content-center">
|
||||||
|
<div className="login p-3 bg-light">
|
||||||
|
<Login
|
||||||
|
onClickForgottenPswd={handleClickPasswordReminderBtn}
|
||||||
|
></Login>
|
||||||
|
|
||||||
|
<hr />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="success"
|
||||||
|
size="lg"
|
||||||
|
className="d-block w-60 mx-auto mt-4"
|
||||||
|
onClick={handleShow}>
|
||||||
|
<b>Create New Account</b>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Col>
|
||||||
|
|
||||||
|
<SignupModal
|
||||||
|
show={show && isModalSignup}
|
||||||
|
onHide={handleCloseCallback}
|
||||||
|
onExit={() => setModalSignup(true)}></SignupModal>
|
||||||
|
|
||||||
|
<PasswordReminderModal
|
||||||
|
show={show && !isModalSignup}
|
||||||
|
onHide={handleClose}
|
||||||
|
onExit={() => setModalSignup(true)}
|
||||||
|
/>
|
||||||
|
</Row>
|
||||||
|
</Col>
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default App;
|
||||||
|
|
|
@ -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 {
|
:root {
|
||||||
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
|
font-size: 16px;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
display: flex;
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
|
||||||
place-items: center;
|
"Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
|
||||||
min-width: 320px;
|
sans-serif;
|
||||||
min-height: 100vh;
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
overflow-x: hidden;
|
||||||
|
background-color: #e9ecef;
|
||||||
}
|
}
|
||||||
|
|
||||||
h1 {
|
code {
|
||||||
font-size: 3.2em;
|
font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New",
|
||||||
line-height: 1.1;
|
monospace;
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
11
src/main.jsx
11
src/main.jsx
|
@ -1,5 +1,12 @@
|
||||||
import { render } from 'preact'
|
import { render } from 'preact'
|
||||||
import './index.css'
|
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