diff --git a/README.md b/README.md index ecb5343..3c7d570 100644 --- a/README.md +++ b/README.md @@ -53,11 +53,6 @@ path='/single-board' component={() => } /> - } - /> - + {/* TODO: make sure the user has been evaluated on each route load */} + { user !== null && } ); diff --git a/src/components/Cards/BoardsCard.js b/src/components/Cards/BoardsCard.js index f92bb5e..3d66b2b 100644 --- a/src/components/Cards/BoardsCard.js +++ b/src/components/Cards/BoardsCard.js @@ -3,17 +3,13 @@ import { Link } from 'react-router-dom'; export default function BoardsCard({ board }) { return ( -
- Card cap -
-
{board.name}
-

- {board.description} -

- - View Pins - + +
+
+
{board.name}
+

{board.description}

+
-
+ ); } diff --git a/src/components/Cards/PinsCard.js b/src/components/Cards/PinsCard.js index ce7aa77..b49eccd 100644 --- a/src/components/Cards/PinsCard.js +++ b/src/components/Cards/PinsCard.js @@ -3,17 +3,16 @@ import { Link } from 'react-router-dom'; export default function PinsCard({ pin }) { return ( -
- Card cap -
-
{pin.name}
-

- {pin.description} -

- - Edit Pin - + +
+
+
{pin.name}
+

{pin.description}

+
-
+ ); } diff --git a/src/components/Forms/BoardForm.js b/src/components/Forms/BoardForm.js new file mode 100644 index 0000000..ec813d4 --- /dev/null +++ b/src/components/Forms/BoardForm.js @@ -0,0 +1,105 @@ +import React, { Component } from 'react'; +import firebase from 'firebase/app'; +import 'firebase/storage'; +import getUser from '../../helpers/data/authData'; +import { createBoard, updateBoard } from '../../helpers/data/boardData'; + +export default class BoardForm extends Component { + state = { + firebaseKey: this.props.board?.firebaseKey || '', + name: this.props.board?.name || '', + imageUrl: this.props.board?.imageUrl || '', + userId: this.props.board?.userId || '', + description: this.props.board?.description || '', + } + + componentDidMount() { + const userId = getUser(); + this.setState({ userId }); + } + + handleChange = (e) => { + if (e.target.name === 'filename') { + this.setState({ imageUrl: '' }); + const storageRef = firebase.storage().ref(); + const imagesRef = storageRef.child(`pinterest/${this.state.userId}/${Date.now()}${e.target.files[0].name}`); + + imagesRef.put(e.target.files[0]).then((snapshot) => { + snapshot.ref.getDownloadURL().then((imageUrl) => { + this.setState({ imageUrl }); + }); + }); + } else { + this.setState({ + [e.target.name]: e.target.value, + }); + } + } + + handleSubmit = (e) => { + e.preventDefault(); + this.btn.setAttribute('disabled', 'disabled'); + if (this.state.firebaseKey === '') { + createBoard(this.state) + .then(() => { + this.props.onUpdate?.(); + this.setState({ success: true }); + }); + } else { + updateBoard(this.state) + .then(() => { + this.props.onUpdate?.(this.props.board.firebaseKey); + this.setState({ success: true }); + }); + } + } + + render() { + const { success } = this.state; + return ( + <> + { success && (
Your Board was Updated/Created
) + } +
+
+ +
+
+ +
+
+ +
+
+ +
+ +
+ + ); + } +} diff --git a/src/components/Modal/index.js b/src/components/Modal/index.js new file mode 100644 index 0000000..6204767 --- /dev/null +++ b/src/components/Modal/index.js @@ -0,0 +1,28 @@ +/* eslint react/no-multi-comp: 0, react/prop-types: 0 */ + +import React, { useState } from 'react'; +import { + Button, Modal, ModalHeader, ModalBody, +} from 'reactstrap'; + +const AppModal = (props) => { + const { className } = props; + + const [modal, setModal] = useState(false); + + const toggle = () => setModal(!modal); + + return ( +
+ + + {props.title} + {props.children} + +
+ ); +}; + +export default AppModal; diff --git a/src/components/MyNavbar/index.js b/src/components/MyNavbar/index.js index f85cc15..4f5d133 100644 --- a/src/components/MyNavbar/index.js +++ b/src/components/MyNavbar/index.js @@ -1,7 +1,7 @@ import React, { useState } from 'react'; import firebase from 'firebase/app'; import 'firebase/auth'; -import { Link } from 'react-router-dom'; +import { Link, useHistory } from 'react-router-dom'; import { Collapse, Navbar, @@ -16,54 +16,62 @@ import { import SearchInput from '../SearchInput'; export default function MyNavbar(props) { + const history = useHistory(); const logMeOut = (e) => { e.preventDefault(); + history.push('/'); firebase.auth().signOut(); }; const { user } = props; const [isOpen, setIsOpen] = useState(false); const toggle = () => setIsOpen(!isOpen); - return ( + return user && (
- - Pinterest + + + Pinterest + -
diff --git a/src/components/PageHeader/index.js b/src/components/PageHeader/index.js new file mode 100644 index 0000000..4cbddba --- /dev/null +++ b/src/components/PageHeader/index.js @@ -0,0 +1,9 @@ +const PageHeader = (props) => ( +
+ {props.user?.displayName} +

{props.user?.displayName}

+
{props.user?.providerData[0].email}
+
+); + +export default PageHeader; diff --git a/src/components/SearchInput/index.js b/src/components/SearchInput/index.js index 2a6ddfd..5799e8e 100644 --- a/src/components/SearchInput/index.js +++ b/src/components/SearchInput/index.js @@ -25,9 +25,9 @@ class SearchInput extends Component { render() { return ( -
- - + diff --git a/src/helpers/Routes.js b/src/helpers/Routes.js index c025c4c..4c776dd 100644 --- a/src/helpers/Routes.js +++ b/src/helpers/Routes.js @@ -1,7 +1,6 @@ import React from 'react'; -import { Switch, Route } from 'react-router-dom'; +import { Switch, Route, Redirect } from 'react-router-dom'; import Home from '../views/Home'; -import BoardForm from '../views/BoardForm'; import Boards from '../views/Boards'; import PinDetails from '../views/PinDetails'; import PinForm from '../views/PinForm'; @@ -10,6 +9,20 @@ import SingleBoard from '../views/SingleBoard'; import NotFound from '../views/NotFound'; import SearchResults from '../views/SearchResults'; +// The PrivateRoute function is creating a private route and returing the specified route based on the props + +// We specify the specific props we want to use in the routeChecker and pass the rest with the spread +const PrivateRoute = ({ component: Component, user, ...rest }) => { + // when we call this function in the return, it is looking for an argument. `props` here is taco. + const routeChecker = (taco) => (user + ? () + : ()); + // this render method is one we can use instead of component. Since the components are being dynamically created, we use render. Read the docs for more info: https://reactrouter.com/web/api/Route/render-func + + // Just like in the routes if we want the dynamically rendered component to have access to the Router props, we have to pass `props` as an argument. + return routeChecker(props)} />; +}; + export default function Routes({ user }) { return ( @@ -18,40 +31,42 @@ export default function Routes({ user }) { path='/' component={() => } /> - } + component={PinDetails} + // since we are checking if a user is authed, we have to pass the user as a props to Private Route so that it can determine if the route should be rendered or redirected. We do this in every route that uses Private Route + user={user} /> - } + component={Pins} + user={user} /> - } + component={PinForm} + user={user} /> - } + component={SingleBoard} + user={user} /> - } - /> - } + component={SearchResults} + user={user} /> - } + component={Boards} + user={user} /> diff --git a/src/helpers/data/authData.js b/src/helpers/data/authData.js index 908dc8c..85308f3 100644 --- a/src/helpers/data/authData.js +++ b/src/helpers/data/authData.js @@ -1,4 +1,4 @@ -import firebase from 'firebase'; +import firebase from 'firebase/app'; import 'firebase/auth'; const getUid = () => firebase.auth().currentUser?.uid; diff --git a/src/helpers/data/boardData.js b/src/helpers/data/boardData.js index 853a3a7..bbc7e1e 100644 --- a/src/helpers/data/boardData.js +++ b/src/helpers/data/boardData.js @@ -10,8 +10,36 @@ const getAllUserBoards = (uid) => new Promise((resolve, reject) => { const getSingleBoard = (boardId) => new Promise((resolve, reject) => { axios.get(`${baseUrl}/boards/${boardId}.json`).then((response) => { + // FIXME: Set up the call to only resolve the boards that belong to the user so that if a user types the ID in the URL, it does not show the board unless it belongs to them. + + // update function to take in userId and compare with response.data.userId. If they match, resolve the board. If they do not match, send an empty object or an error message. resolve(response.data); }).catch((error) => reject(error)); }); -export { getAllUserBoards, getSingleBoard }; +const createBoard = (object) => new Promise((resolve, reject) => { + axios.post(`${baseUrl}/boards.json`, object) + .then((response) => { + axios.patch(`${baseUrl}/boards/${response.data.name}.json`, { firebaseKey: response.data.name }).then(resolve); + }).catch((error) => reject(error)); +}); + +const updateBoard = (object) => new Promise((resolve, reject) => { + axios.patch(`${baseUrl}/boards/${object.firebaseKey}.json`, object) + .then(resolve).catch((error) => reject(error)); +}); + +const searchBoards = (uid, term) => new Promise((resolve, reject) => { + getAllUserBoards(uid).then((response) => { + const searchResults = response.filter((r) => r.name.toLowerCase().includes(term) || r.description.toLowerCase().includes(term)); + resolve(searchResults); + }).catch((error) => reject(error)); +}); + +export { + getAllUserBoards, + getSingleBoard, + createBoard, + updateBoard, + searchBoards, +}; diff --git a/src/helpers/data/pinData.js b/src/helpers/data/pinData.js index 1894c9c..29c7233 100644 --- a/src/helpers/data/pinData.js +++ b/src/helpers/data/pinData.js @@ -14,4 +14,27 @@ const getPin = (pinId) => new Promise((resolve, reject) => { }).catch((error) => reject(error)); }); -export { getBoardPins, getPin }; +const getAllUserPins = (userId) => new Promise((resolve, reject) => { + axios.get(`${baseUrl}/pins.json?orderBy="userId"&equalTo="${userId}"`).then((response) => { + resolve(Object.values(response.data)); + }).catch((error) => reject(error)); +}); + +const getAllPins = (userId) => new Promise((resolve, reject) => { + axios.get(`${baseUrl}/pins.json`).then((response) => { + // Need to make sure that the pin either belongs to the user or is not private. + const filteredArray = Object.values(response.data).filter((r) => r.userId === userId || r.private === false); + resolve(filteredArray); + }).catch((error) => reject(error)); +}); + +const searchPins = (userId, term) => new Promise((resolve, reject) => { + getAllPins(userId).then((response) => { + const searchResults = response.filter((r) => r.name.toLowerCase().includes(term) || r.description.toLowerCase().includes(term)); + resolve(searchResults); + }).catch((error) => reject(error)); +}); + +export { + getBoardPins, getPin, getAllUserPins, searchPins, getAllPins, +}; diff --git a/src/styles/_variables.scss b/src/styles/_variables.scss index defcbde..00dbca0 100644 --- a/src/styles/_variables.scss +++ b/src/styles/_variables.scss @@ -1 +1 @@ -$favColor: white; +$favColor: rgb(223, 223, 223);; diff --git a/src/styles/components/_app.scss b/src/styles/components/_app.scss index ee341ab..06a592b 100644 --- a/src/styles/components/_app.scss +++ b/src/styles/components/_app.scss @@ -1,4 +1,10 @@ // ADD STYLES +body { + background-color: rgb(223, 223, 223); +} + .App { text-align: center; + padding-top: 50px; + padding-bottom: 100px; } \ No newline at end of file diff --git a/src/styles/components/_boards.scss b/src/styles/components/_boards.scss index e69de29..30a667a 100644 --- a/src/styles/components/_boards.scss +++ b/src/styles/components/_boards.scss @@ -0,0 +1,51 @@ +a.whole-card { + text-decoration: none; + color: white; + border-radius: 10px; + + .board-card { + background-position: center center; + background-size: cover; + background-repeat: no-repeat; + width: 300px; + height: 400px; + border-top-right-radius: 10px; + border-top-left-radius: 10px; + + &:before { + content: ''; + background: rgba(0,0,0,.4); + width: 300px; + height: 400px; + border-top-right-radius: 10px; + border-top-left-radius: 10px; + } + + .card-body { + content: ''; + background: rgba(0,0,0,.7); + } + + } +} + +.user-board-info { + padding: 20px 0; + margin-bottom: 30px; + border-bottom: 1px solid rgb(209, 209, 209); + background-color: rgb(200, 204, 255); + + img { + width: 100px; + border-radius: 50px; + } +} + +.app-modal { + padding: 20px; + background-color: rgb(200, 204, 255); + + &.align-right { + text-align: right; + } +} diff --git a/src/styles/components/_navbar.scss b/src/styles/components/_navbar.scss index cf881bd..07ba1e8 100644 --- a/src/styles/components/_navbar.scss +++ b/src/styles/components/_navbar.scss @@ -1,5 +1,5 @@ img.userInfo { - width: 40px; + width: 30px; border-radius: 50px; } diff --git a/src/styles/index.scss b/src/styles/index.scss index 53859ef..f633fed 100644 --- a/src/styles/index.scss +++ b/src/styles/index.scss @@ -3,8 +3,4 @@ @import 'variables'; // COMPONENTS -@import './components/index.scss'; - -body { - background-color: $favColor; -} +@import './components/index.scss'; \ No newline at end of file diff --git a/src/views/BoardForm.js b/src/views/BoardForm.js deleted file mode 100644 index 44dc9a3..0000000 --- a/src/views/BoardForm.js +++ /dev/null @@ -1,9 +0,0 @@ -import React from 'react'; - -export default function BoardForm() { - return ( -
-

Board Form

-
- ); -} diff --git a/src/views/Boards.js b/src/views/Boards.js index cae400a..ee63054 100644 --- a/src/views/Boards.js +++ b/src/views/Boards.js @@ -3,6 +3,9 @@ import { getAllUserBoards } from '../helpers/data/boardData'; import BoardsCard from '../components/Cards/BoardsCard'; import Loader from '../components/Loader'; import getUid from '../helpers/data/authData'; +import AppModal from '../components/Modal'; +import BoardForm from '../components/Forms/BoardForm'; +import PageHeader from '../components/PageHeader'; export default class Boards extends React.Component { state = { @@ -11,6 +14,10 @@ export default class Boards extends React.Component { } componentDidMount() { + this.getBoards(); + } + + getBoards = () => { const currentUserId = getUid(); getAllUserBoards(currentUserId).then((response) => { this.setState({ @@ -31,6 +38,7 @@ export default class Boards extends React.Component { render() { const { boards, loading } = this.state; + const { user } = this.props; const showBoards = () => ( boards.map((board) => ) ); @@ -40,8 +48,12 @@ export default class Boards extends React.Component { ) : ( <> -

Here are all of your boards

-
{showBoards()}
+ + + + +

Boards

+
{showBoards()}
)} diff --git a/src/views/Home.js b/src/views/Home.js index 9eb3383..c451e41 100644 --- a/src/views/Home.js +++ b/src/views/Home.js @@ -1,24 +1,55 @@ import React from 'react'; import Auth from '../components/Auth'; import Loader from '../components/Loader'; +import { getAllPins } from '../helpers/data/pinData'; +import getUid from '../helpers/data/authData'; +import PinsCard from '../components/Cards/PinsCard'; -export default function Home({ user }) { - const loadComponent = () => { +export default class Home extends React.Component { + state = { + pins: [], + loading: true, + } + + componentDidMount() { + this.getPins(); + } + + getPins = () => { + const userId = getUid(); + getAllPins(userId) + .then((pins) => { + this.setState({ + pins, + loading: false, + }); + }); + } + + loadComponent = () => { + const { user } = this.props; let component = ''; if (user === null) { component = ; } else if (user) { - component = 'Load all non-private pins here'; + component = this.state.pins.length + && this.state.pins.map((pin) => ( + + )); } else { component = ; } return component; }; - return ( -
-

Welcome to React-Pinterest

- {loadComponent()} -
- ); + render() { + return ( +
+

Welcome to React Pinterest

+
+ {!this.state.loading && this.loadComponent()} +
+
+ ); + } } diff --git a/src/views/Pins.js b/src/views/Pins.js index ea4becf..02ee4f7 100644 --- a/src/views/Pins.js +++ b/src/views/Pins.js @@ -1,9 +1,36 @@ import React from 'react'; +import getUid from '../helpers/data/authData'; +import { getAllUserPins } from '../helpers/data/pinData'; +import PinsCard from '../components/Cards/PinsCard'; +import Loader from '../components/Loader'; -export default function Pins() { - return ( -
-

Pins

-
- ); +export default class Pins extends React.Component { + state = { + pins: [], + } + + componentDidMount() { + const userId = getUid(); + getAllUserPins(userId).then((pins) => this.setState({ pins })); + } + + render() { + const { pins } = this.state; + const renderPins = () => ( + pins.length + ? pins.map((pin) => ( + + )) : ( + + ) + ); + return ( +
+

My Pins

+
+ {renderPins()} +
+
+ ); + } } diff --git a/src/views/SearchResults.js b/src/views/SearchResults.js index 6fb8898..7a0f104 100644 --- a/src/views/SearchResults.js +++ b/src/views/SearchResults.js @@ -1,10 +1,68 @@ import React, { Component } from 'react'; +import getUid from '../helpers/data/authData'; +import { searchBoards } from '../helpers/data/boardData'; +import { searchPins } from '../helpers/data/pinData'; +import BoardsCard from '../components/Cards/BoardsCard'; +import PinsCard from '../components/Cards/PinsCard'; export default class SearchResults extends Component { + state = { + results: [], + searchTerm: '', + searchType: '', + } + + componentDidMount() { + this.performSearch(); + } + + performSearch = () => { + const searchType = this.props.match.params.type; + const searchTerm = this.props.match.params.term.toLowerCase(); + const userId = getUid(); + if (searchType === 'boards') { + this.getResults = searchBoards(userId, searchTerm) + .then((results) => { + this.setState({ + results, + searchTerm, + searchType, + }); + }); + } else { + searchPins(userId, searchTerm).then((results) => { + this.setState({ + results, + searchTerm, + searchType, + }); + }); + } + } + + // allow searches to take place when the search results component is already mounted and it rerenders based on the changes identified in this function + componentDidUpdate(prevProps, prevState) { + // only update/rerender component if the params have changed + if (prevState.searchTerm !== this.props.match.params.term.toLowerCase() || prevState.searchType !== this.props.match.params.type) { + this.performSearch(); + } + } + render() { + const { results, searchType } = this.state; + const showResults = () => ( + results.length + ? results.map((result) => ( + searchType === 'boards' ? : + )) : ( + 'No Results' + ) + ); return (
-

Search Results

+

Search Results

+
+ {showResults()}
); } diff --git a/src/views/SingleBoard.js b/src/views/SingleBoard.js index bd6ba8d..1369892 100644 --- a/src/views/SingleBoard.js +++ b/src/views/SingleBoard.js @@ -2,6 +2,9 @@ import React from 'react'; import { getBoardPins, getPin } from '../helpers/data/pinData'; import { getSingleBoard } from '../helpers/data/boardData'; import PinsCard from '../components/Cards/PinsCard'; +import AppModal from '../components/Modal'; +import BoardForm from '../components/Forms/BoardForm'; +import PageHeader from '../components/PageHeader'; export default class SingleBoard extends React.Component { state = { @@ -13,11 +16,7 @@ export default class SingleBoard extends React.Component { // 1. Pull boardId from URL params const boardId = this.props.match.params.id; // 2. Make a call to the API that gets the board info - getSingleBoard(boardId).then((response) => { - this.setState({ - board: response, - }); - }); + this.getBoard(boardId); // 3. Make a call to the API that returns the pins associated with this board and set to state. this.getPins(boardId) @@ -27,6 +26,14 @@ export default class SingleBoard extends React.Component { )); } + getBoard = (boardId) => { + getSingleBoard(boardId).then((response) => { + this.setState({ + board: response, + }); + }); + } + getPins = (boardId) => ( getBoardPins(boardId).then((response) => { // an array that holds all of the calls to get the pin information @@ -42,6 +49,7 @@ export default class SingleBoard extends React.Component { render() { const { pins, board } = this.state; + const { user } = this.props; const renderPins = () => ( // 4. map over the pins in state pins.map((pin) => ( @@ -52,6 +60,10 @@ export default class SingleBoard extends React.Component { // 5. Render the pins on the DOM return (
+ + + +

{board.name}

{renderPins()}