Final Flatiron Project
Wow, after almost a year I can't believe I’m finishing up. This project came with its own set of challenges, the biggest being working with picture files which were something I had not had any experience with. I had a lot of trouble initially with rails ActiveStorage, and after being held up for a few days with no progress I decided to use my own workaround using an external API.
In this post, I will give a brief overview of the entire project, and dive slightly deeper into some of the concepts I implemented.
I created this website for my wonderful wife, Kinsley. She is a photographer and needed something for an online portfolio and a way for people to contact her about booking requests and I could think of no better way to spend my final project than to help her grow and prosper in her new career.
Overview
I have a rails API backend, with a React frontend, and I am using Thunk as my state manager.
Here you can see my index.js, where I am creating my redux store and connecting the middleware.
import React from ‘react’;import ReactDOM from ‘react-dom’;import ‘./index.css’;import App from ‘./containers/App’;import reportWebVitals from ‘./reportWebVitals’;import { createStore, applyMiddleware } from ‘redux’import { Provider } from ‘react-redux’;import thunk from ‘redux-thunk’;import rootReducer from ‘./reducers/rootReducer’let store = createStore(rootReducer, applyMiddleware(thunk))ReactDOM.render((<Provider store={store}>< App /></Provider>),document.getElementById(‘root’))’
And in the rendered App component is my react-router setup:
import ‘../App.css’;import React, { Component } from ‘react’;import HomePage from ‘./HomePage’import GalleriesPage from ‘./GalleriesPage’import BookingPage from ‘./BookingPage’import AboutMePage from ‘./AboutMePage’import Navigation from ‘../components/Navigation’import { BrowserRouter as Router, Route } from ‘react-router-dom’;class App extends Component {render() {return (<Router><Navigation /><Route exact path=’/’ component={HomePage} /><Route exact path=’/galleries’ component={GalleriesPage} /><Route exact path=’/booking’ component={BookingPage} /><Route exact path=’/about’ component={AboutMePage} /></Router>);}}export default App;
As you can see I am importing all of the different pages, all from the “containers” directory.
They are nested in the <Router> tag, which is what we named BrowserRouter upon importing.
The Navbar sits directly on top, and from this point, everything is contained within the routes.
One of the more logic-intensive routes is the gallery page.
Let's take a look, and then dive in.
import React, { Component } from ‘react’import Col from ‘react-bootstrap/Col’import Row from ‘react-bootstrap/Row’import GalleryBox from ‘../components/GalleryBox’import { connect } from ‘react-redux’;import { selectGallery, getGalleries } from ‘../actions/galleries’import { getPictures } from ‘../actions/pictures’import Jumbotron from ‘react-bootstrap/Jumbotron’import GalleryRender from ‘../components/GalleryRender’import AddPictureForm from ‘../components/AddPictureForm’class GalleriesPage extends Component {componentDidMount(){this.props.getPictures()}render() {const getGalleries = this.props.galleries.galleries.map((gallery, index) => <Col>< GalleryBox id={index} gal={gallery} selectGal={this.props.selectGallery} /></Col>)if (this.props.galleries.selectedGallery) {return (<Jumbotron className=”gallery-pics text-center”><h1>{this.props.galleries.selectedGallery.title}</h1><AddPictureForm gallery={this.props.galleries.selectedGallery} /><br/><GalleryRender galId={this.props.galleries.selectedGallery.id} /></Jumbotron>)} else {return (<div className=”text-center”><Row className=”justify-content-md-center galleries-container”>{getGalleries}</Row></div>)}}}const mapStateToProps = state => {return {galleries: state.galleries,selectedGallery: state.selectedGallery,pictures: state.pictures,photoshoots: state.photoshoots}}export default connect(mapStateToProps, {selectGallery, getPictures, getGalleries })(GalleriesPage)
When the GalleryContainer is rendered, the getPictures dispatch is immediately hit. This sends a post request to the backend to ensure the state is always updated with the current pictures. Then there is a conditional render, at first, it calls on the getGalleries variable, which is set to pull the galleries from the global state, and then use a .map to render each out passing down selectGallery as a prop. This way, when you click on one of the galleries, the condition switches to show the user the gallery that was selected.
class GalleryRender extends Component {componentDidMount(){this.props.getPictures()}onSelectImage = (event) => {console.log(event)}render() {let images = []this.props.pictures.filter(picture => picture.gallery_id === this.props.galId).map(pic => {return images.push({src: pic.src, thumbnail: pic.thumbnail, thumbnailWidth: undefined, thumbnailHeight: undefined})})return (<div><Gallery images={images} rowHeight={250} onSelectImage={ event => this.onSelectImage(event)}/></div>)}}
One thing I struggled with was deleting the pictures since I used react-grid-gallery to render my images, this is an issue I will continue to look into and fix! As far as rendering them in the first place, I am setting the variable “images” to an empty array, this component is connected to the redux store, so I am taking the pictures from the state, filtering them against the selected gallery_id, then mapping over the remaining images to push them into our array from before.
That array is what is passed into the Gallery component which is provided by react-grid-gallery.
I will also show the BookingPage, to show how I am capturing the photoshoot data, saving, and rendering it.
Without all of the imports, this is what this container looks like:
export default class BookingPage extends Component {state = {buttonToggle: false}toggleButtonOn = () => {console.log(“On”)this.setState({buttonToggle: true})}render() {if (this.state.buttonToggle === true) {return (<Jumbotron className=”jumbo-booking booking-background”><Container className=”position-absolute top-50 start-50 translate-middle top-padding”><div className=”row”><div className=”col”><SchedulingInfo /></div><div className=”col-5"><BookingList /></div><div className=”col”><SchedulingExamples /></div></div></Container><Button variant=”outline-dark position-absolute top-100 start-50 translate-middle top-padding” onClick={this.toggleButtonOn}>List bookings</Button></Jumbotron>)} else {return (<Jumbotron className=”jumbo-booking booking-background”><Container className=”position-absolute top-50 start-50 translate-middle top-padding”><div className=”row”><div className=”col”><SchedulingInfo /></div><div className=”col-5"><Scheduler /></div><div className=”col”><SchedulingExamples /></div></div></Container><Button variant=”outline-dark” className=”position-absolute top-100 start-50 translate-middle top-padding” onClick={this.toggleButtonOn}>List bookings</Button></Jumbotron>)}}}
The booking page has a local state with the attribute buttonToggle, initially set to false. There is a button on the middle/bottom of the page that when clicked will toggle that attribute to true. This is the deciding factor on which version of the booking page is rendered, if unselected it will render the scheduler component, which is a calendar with an event listener so that when you click on a day it extracts the date and populated a form for the user to fill out with all of the information about the photoshoot. If it is selected, it will show a list of all of the current bookings in the database, with an option to delete them.
Summary
This was by far the most challenging project, and while I have hit my minimum viable product, there is definitely still some work to be done. One major feature that is needed is some sort of permission library so that one owner can change data, upload/delete pictures, and view/delete photoshoots while users viewing the site cannot.
Another future feature will be an email list, for notifications when new pictures are added, or a short newsletter including info about shoots, locations, or special deals!
Thank you for your time, I cannot wait to improve my skills with these languages, and to improve this application.
The next step of the journey is the job search, which I am very excited to begin.