diff --git a/digital-course-file/public/index.html b/digital-course-file/public/index.html index 381448a5ba8e3c209ad6a2fa686815a425428d3c..a273dc46530f1fb9c00a1913fc3ff56fb4ab0be0 100644 --- a/digital-course-file/public/index.html +++ b/digital-course-file/public/index.html @@ -27,7 +27,7 @@ work correctly both with client-side routing and a non-root public URL. Learn how to configure a non-root public URL by running `npm run build`. --> - <title>React App</title> + <title>Digital Course File System</title> </head> <body> <noscript>You need to enable JavaScript to run this app.</noscript> diff --git a/digital-course-file/src/fire.js b/digital-course-file/src/fire.js index 5f1b626f00ae3559b1aad5f460cb15a687aee9e8..f39ab456cb97b9abd7d3f9f7ba887f3b39a94f6a 100644 --- a/digital-course-file/src/fire.js +++ b/digital-course-file/src/fire.js @@ -1,5 +1,7 @@ import firebase from 'firebase' -// import "firebase/firestore" +import "firebase/auth" +import "firebase/firestore" +import "firebase/storage" // Your web app's Firebase configuration var firebaseConfig = { @@ -23,5 +25,6 @@ import firebase from 'firebase' getTime : firebase.firestore.FieldValue.serverTimestamp, formatDoc : doc => { return {id : doc.id, ...doc.data()} }, } - - export default fire; \ No newline at end of file + export const storage = fire.storage() + export const auth = fire.auth() + export default fire; diff --git a/digital-course-file/src/hooks/useFolder.js b/digital-course-file/src/hooks/useFolder.js index bc8aba70810f96137a9d29a83072e11d149bd445..ab56f941ee81f1ac37f20b79a2d008d514a592cd 100644 --- a/digital-course-file/src/hooks/useFolder.js +++ b/digital-course-file/src/hooks/useFolder.js @@ -1,8 +1,6 @@ import { useState,useReducer, useEffect } from "react"; import { database } from '../fire.js' import firebase from 'firebase' -import Loader from "react-loader-spinner"; - export const ROOT_FOLDER = {name: 'Root', id : null , path : [] , parents : []}; @@ -14,6 +12,7 @@ export function useFolder( folderId = null, folder= null) { SELECT_FOLDER : 'select-folder', UPDATE_FOLDER : 'update-folder', SET_CHILD_FOLDERS : 'set_child_folders', + SET_CHILD_FILES: "set-child-files", } function reducer( state, { type,payload } ){ @@ -37,7 +36,12 @@ export function useFolder( folderId = null, folder= null) { return{ ...state, childFolders : payload.childFolders, - }; + }; + case ACTIONS.SET_CHILD_FILES: + return { + ...state, + childFiles: payload.childFiles, + }; default: return state; @@ -85,15 +89,6 @@ export function useFolder( folderId = null, folder= null) { useEffect( () => { - if (!firebase.auth().currentUser) { - return <><div className='centered'><Loader - type="Puff" - color="#00BFFF" - height={100} - width={100} - timeout={3000} //3 secs - /></div></> - } return database.folders .where("parentId", "==" ,folderId) .where("userId","==", firebase.auth().currentUser.uid) @@ -105,6 +100,18 @@ export function useFolder( folderId = null, folder= null) { }) }) },[folderId]) - + useEffect(() => { + return ( + database.files + .where("folderId", "==", folderId) + .where("userId", "==", firebase.auth().currentUser.uid) + .onSnapshot(snapshot => { + dispatch({ + type: ACTIONS.SET_CHILD_FILES, + payload: { childFiles: snapshot.docs.map(database.formatDoc) }, + }) + }) + ) + }, [folderId]) return state; -} \ No newline at end of file +} diff --git a/digital-course-file/src/user/AddFile.js b/digital-course-file/src/user/AddFile.js new file mode 100644 index 0000000000000000000000000000000000000000..b3ca63699b3972b4febdac9a8e79fad93a85a239 --- /dev/null +++ b/digital-course-file/src/user/AddFile.js @@ -0,0 +1,145 @@ +import React, { useState } from "react" +import ReactDOM from "react-dom" +import { faFileUpload } from "@fortawesome/free-solid-svg-icons" +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome" +import { storage, database } from '../fire.js' +import { ROOT_FOLDER } from "../hooks/useFolder" +import { v4 as uuidV4 } from "uuid" +import { ProgressBar, Toast } from "react-bootstrap" +import firebase from 'firebase' + +export default function AddFile({ currentFolder }) { + const [uploadingFiles, setUploadingFiles] = useState([]) + + + function handleUpload(e) { + const file = e.target.files[0] + if (currentFolder == null || file == null) return + + const id = uuidV4() + setUploadingFiles(prevUploadingFiles => [ + ...prevUploadingFiles, + { id: id, name: file.name, progress: 0, error: false }, + ]) + const filePath = + currentFolder === ROOT_FOLDER + ? `${currentFolder.path.join("/")}/${file.name}` + : `${currentFolder.path.join("/")}/${currentFolder.name}/${file.name}` + + const uploadTask = storage + .ref(`/files/${firebase.auth().currentUser.uid}/${filePath}`) + .put(file) + + uploadTask.on( + "state_changed", + snapshot => { + const progress = snapshot.bytesTransferred / snapshot.totalBytes + setUploadingFiles(prevUploadingFiles => { + return prevUploadingFiles.map(uploadFile => { + if (uploadFile.id === id) { + return { ...uploadFile, progress: progress } + } + + return uploadFile + }) + }) + }, + () => { + setUploadingFiles(prevUploadingFiles => { + return prevUploadingFiles.map(uploadFile => { + if (uploadFile.id === id) { + return { ...uploadFile, error: true } + } + return uploadFile + }) + }) + }, + () => { + setUploadingFiles(prevUploadingFiles => { + return prevUploadingFiles.filter(uploadFile => { + return uploadFile.id !== id + }) + }) + + uploadTask.snapshot.ref.getDownloadURL().then(url => { + database.files + .where("name", "==", file.name) + .where("userId", "==", firebase.auth().currentUser.uid) + .where("folderId", "==", currentFolder.id) + .get() + .then(existingFiles => { + const existingFile = existingFiles.docs[0] + if (existingFile) { + existingFile.ref.update({ url: url }) + } else { + database.files.add({ + url: url, + name: file.name, + createdAt: database.getTime(), + folderId: currentFolder.id, + userId: firebase.auth().currentUser.uid, + }) + } + }) + }) + } + ) + } + + return ( + <> + <label className="btn btn-outline-success btn-sm m-0 mr-2"> + <FontAwesomeIcon icon={faFileUpload} /> + <input + type="file" + onChange={handleUpload} + style={{ opacity: 0, position: "absolute", left: "-9999px" }} + /> + </label> + {uploadingFiles.length > 0 && + ReactDOM.createPortal( + <div + style={{ + position: "absolute", + bottom: "1rem", + right: "1rem", + maxWidth: "250px", + }} + > + {uploadingFiles.map(file => ( + <Toast + key={file.id} + onClose={() => { + setUploadingFiles(prevUploadingFiles => { + return prevUploadingFiles.filter(uploadFile => { + return uploadFile.id !== file.id + }) + }) + }} + > + <Toast.Header + closeButton={file.error} + className="text-truncate w-100 d-block" + > + {file.name} + </Toast.Header> + <Toast.Body> + <ProgressBar + animated={!file.error} + variant={file.error ? "danger" : "primary"} + now={file.error ? 100 : file.progress * 100} + label={ + file.error + ? "Error" + : `${Math.round(file.progress * 100)}%` + } + /> + </Toast.Body> + </Toast> + ))} + </div>, + document.body + )} + </> + ) +} diff --git a/digital-course-file/src/user/File.js b/digital-course-file/src/user/File.js new file mode 100644 index 0000000000000000000000000000000000000000..4a5ce33f758962f8d0f52a2364b5bec35998902f --- /dev/null +++ b/digital-course-file/src/user/File.js @@ -0,0 +1,16 @@ +import { faFile } from "@fortawesome/free-solid-svg-icons" +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome" +import React from "react" + +export default function File({ file }) { + return ( + <a + href={file.url} + target="_blank" + className="btn btn-outline-dark text-truncate w-100" + > + <FontAwesomeIcon icon={faFile} className="mr-2" /> + {file.name} + </a> + ) +} \ No newline at end of file diff --git a/digital-course-file/src/user/Hero.js b/digital-course-file/src/user/Hero.js index 434126d0c53b7f1847b0d3c20c03da9573f8888f..a8e9a37c96de0d8c9ae410785538f7bd2e054a4e 100644 --- a/digital-course-file/src/user/Hero.js +++ b/digital-course-file/src/user/Hero.js @@ -1,5 +1,6 @@ -import React, { useEffect, useState } from 'react' +import React from 'react' import AddFolder from './AddFolder' +import AddFile from './AddFile' import { Container, Button, Navbar, Nav } from 'react-bootstrap' import { ROOT_FOLDER, useFolder } from '.././hooks/useFolder' import Folder from './Folder' @@ -9,31 +10,29 @@ import DeleteFolder from './DeleteFolder' import { useParams } from 'react-router-dom' import copyright from './copyright' import { Link } from 'react-router-dom' -import Loader from 'react-loader-spinner' +import File from './File' +import FontAwesomeIcon from '@fortawesome/react-fontawesome'; +import firebase from "../fire"; const Hero = ({ handleLogout }) => { const { folderId } = useParams() - const { folder, childFolders } = useFolder(folderId) - console.log(folder); - - if (!folder) { - return ( - <> - <div className='centered'> - <Loader - type='Puff' - color='#00BFFF' - height={100} - width={100} - timeout={3000} //3 secs - /> - </div> - </> - ) - } - + const { state = {} } = useLocation() + const { folder, childFolders, childFiles } = useFolder(folderId, state.folder) + return ( <> + <section className='hero'> + <nav> + <Navbar.Brand as={Link} to='/'> + <h2>Course File System</h2> + </Navbar.Brand> + + <button className='logoutbutton' onClick={handleLogout}> + Logout + </button> + </nav> + </section> + <Container fluid> <div className='d-flex align-items-center'> <FolderNav currentFolder={folder} /> @@ -57,7 +56,29 @@ const Hero = ({ handleLogout }) => { ))} </div> )} + {childFolders.length > 0 && childFiles.length > 0 && <hr />} + {childFiles.length > 0 && ( + <div className="d-flex flex-wrap"> + {childFiles.map(childFile => ( + <div + key={childFile.id} + style={{ maxWidth: "250px" }} + className="p-2" + > + <File file={childFile} /> + + </div> + ))} + </div> + )} </Container> + <Navbar fixed='bottom' variant='light' bg='light'> + <Container className='ml-sm-2'> + <Nav.Link eventKey={2} href='copyright'> + © Digital Course File Group 2 + </Nav.Link> + </Container> + </Navbar> </> ) }