<file_sep>import React from 'react';
import { Link } from 'react-router-dom';
import LoginForm from '../../components/LoginForm/LoginForm'
import './LoginPage.css'
import HeaderContext from '../../contexts/HeaderContext';


class LoginPage extends React.Component {
   static defaultProps = {
     history: {
       push: () => {},
     },
   }

   static contextType = HeaderContext

   handleLoginSuccess = (token) => {
     const { history } = this.props;
     this.context.setAuthToken(token)
     history.push('/posts')
   }

  render(){
    return (
      <>
      <Link to={'/posts'}>
        <h1 className='title-header'>BandBridge</h1>
        </Link>
      <section className='login-page'> 
      <h1 className='login-header'>Login</h1>
        <LoginForm 
        onLoginSuccess={this.handleLoginSuccess}
        />
        </section>
      </>
    )
  }  
}

export default LoginPage;<file_sep>import React from 'react';
import { Link } from 'react-router-dom';
import TokenService from '../../services/TokenService';
import HeaderContext from '../../contexts/HeaderContext';
import './Header.css'


class Header extends React.Component{
  

  static contextType = HeaderContext;

  
  
  handleLogoutClick = () => {
    TokenService.clearAuthToken()
    this.context.setAuthToken(TokenService.getAuthToken())
  }

  renderUserName = () => {
    if(this.context.authToken){
      return <h3 className='user-name'>{TokenService.getUserName()}</h3>
    }
    else {
      return <h3 className='user-name'>Guest</h3>
    }
  }

  renderLogoutLink() {

    return (
      <div className='header-login'>
       {this.renderUserName()}
        <Link to='/' onClick={this.handleLogoutClick}><button className='login-button'>Logout</button></Link>
      </div>
      )
  }

  renderLoginLink(){

  return  (
      <div className='header-login'>
       {this.renderUserName()}
        <Link to='/login'><button className='login-button'>Login</button></Link>
        {'  '}
        <Link to='signup'><button className='login-button'>Sign up</button></Link>
      </div>
    )
  }


  render(){
    return (
      <>
      <Link to={'/posts'}>
        <h1 className='title-header'>BandBridge</h1>
        </Link>
      <header className='app-header'>
        {this.context.authToken
        ? this.renderLogoutLink()
        : this.renderLoginLink()}
      </header>
      </>
    )
  }


}

export default Header;

<file_sep>import React from 'react';
import Lists from '../../Utils/Lists';
import AuthApiService from '../../services/AuthApiService';

class RegistrationForm extends React.Component {

  handleSubmit = (ev) => {
    ev.preventDefault();
    const {user_name, password, location, instrument, styles, commitment} = ev.target;
    
    
    console.log(styles.value)

    this.setState({error: null});
    AuthApiService.postUser({
      user_name: user_name.value,
      password: <PASSWORD>,
      location: location.value,
      instrument: instrument.value,
      styles: styles.value,
      commitment: commitment.value,
    })
    .then(() => {
      user_name.value = ''
      password.value = ''
      this.props.onRegistrationSuccess()
    })
    .catch(res => {
      this.setState({error: res.error})
    })

  }

  handleInputOptions = inputs => {
      return inputs.map((input, key )=> <option key={key}value={input}>{input}</option>)
  }

  render() {
    let instruments = this.handleInputOptions(Lists.instrumentOptions);
    let locations = this.handleInputOptions(Lists.locationOptions);
    let styles = this.handleInputOptions(Lists.styles.sort());
    let commitment = this.handleInputOptions(Lists.commitment);
    return (
      <form className='registraion-form' onSubmit={this.handleSubmit}>
        <div className='reg-form-row'>
          <label htmlFor='user_name'>Username: </label>
          <input name='user_name' id='user_name' type='text' placeholder='username...' required />
        </div>
        <div className='reg-form-row'>
          <label htmlFor='password'>Password: </label>
          <input name='password' id='password' type='password' placeholder='password...' required />
        </div>
        <div className='reg-form-row'>
          <label htmlFor='location'>Location: </label>
          <select name='location' id='location' required>
          {locations} 
          </select>
        </div>
        <div className='reg-form-row'>
          <label htmlFor='instrument'>Instrument: </label>
          <select name='instrument' id='instrument' required>
          {instruments} 
          </select>
        </div>
        <div className='reg-form-row'>
          <label htmlFor='styles'>Styles (Genres): </label>
          <select name='styles' className='reg-form-multi' id='styles' multiple size='5' required>
            {styles}
          </select>
        </div>
        <div className='reg-form-row'>
          <label htmlFor='commitment'>Hours Per Week (Availability): </label>
          <select name='commitment' id='commitment' required>
            {commitment}
          </select>
        </div>
        <button type='submit' className='reg-form-button'>Register</button>
      </form>
    )
  }
}

export default RegistrationForm;

<file_sep>import React from 'react';
import TokenService from '../services/TokenService';


const HeaderContext = React.createContext({
  setAuthToken: () => {}
})


export default HeaderContext;

export class HeaderProvider extends React.Component {


  state = {
    authToken: TokenService.getAuthToken()
  }

  setAuthToken = authToken => {
    this.setState({
      authToken
    })
  }


  render(){
    const value = {
      authToken: this.state.authToken,
      setAuthToken: this.setAuthToken,
    }
    return (
      <HeaderContext.Provider value={value}>
        {this.props.children}
      </HeaderContext.Provider>
    )
  }
}

<file_sep>import React from 'react';


const Lists = {
  instrumentOptions: [
    'Guitar(6)',
    'Guitar(7)',
    'Guiter(8)',
    'Acoustic-Guitar',
    'Bass(4)',
    'Bass(5)',
    'Acoustic-Bass',
    'Double-Bass',
    'Drums',
    'Vocals',
    'Piano',
    'Keyboard',
    'Violin',
    'Saxophone',
    'Cello',
    'Flute'
  ],
  locationOptions: [
    'Montgomery (AL)',
    'Juneau (AK)',
    'Phoenix (AZ)',
    'Little Rock (AR)',
    'Sacramento (CA)',
    'Denver (CO)',
    'Hartford (CT)',
    'Dover (DE)',
    'Tallahassee (FL)',
    'Atlanta (GA)',
    'Honolulu (HI)',
    'Boise (ID)',
    'Springfield (IL)',
    'Indianapolis (IN)',
    'Des Moines (IA)',
    'Topeka (KS)',
    'Frankfort (KY)',
    'Baton Rouge (LA)',
    'Augusta (ME)',
    'Annapolis (MD)',
    'Boston (MA)',
    'Lansing (MI)',
    'Saint Paul (MN)',
    'Jackson (MS)',
    'Jefferson City (MO)',
    'Helena (MT)',
    'Lincoln (NE)',
    'Carson City (NV)',
    'Concord (NH)',
    'Trenton (NJ)',
    'Santa Fe (NM)',
    'Albany (NY)',
    'Raleigh (NC)',
    'Bismarck (ND)',
    'Columbus (OH)',
    'Oklahoma City (OK)',
    'Salem (OR)',
    'Harrisburg (PA)',
    'Providence (RI)',
    'Columbia (SC)',
    'Pierre (SD)',
    'Nashville (TN)',
    'Austin (TX)',
    'Salt Lake City (UT)',
    'Montpelier (VT)',
    'Richmond (VA)',
    'Olympia (WA)',
    'Charleston (WV)',
    'Madison (WI)',
    'Cheyenne (WY)'
  ],
  skillLvls: [
    'Beginner',
    'Hobbyist',
    'Intermediate',
    'Advanced',
    'Professional'
  ],
  styles: [
    'Blues','Classic-Rock','Country','Dance','Disco','Funk','Grunge',
 'Hip-Hop','Jazz','Metal','New-Age','Oldies','Other','Pop','R&B',
 'Rap','Reggae','Rock','Techno','Industrial','Alternative','Ska',
 'Death-Metal','Soundtrack','Euro-Techno','Ambient',
 'Trip-Hop','Vocal','Fusion','Trance','Classical',
 'Instrumental','Acid','House','Gospel',
 'Noise','Alt-Rock','Bass','Soul','Punk','Space','Meditative',
 'Instrumental-Pop','Instrumental Rock','Gothic',
 'Darkwave','Techno-Industrial','Electronic','Pop-Folk',
 'Eurodance','Dream','Southern-Rock','Comedy','Cult','Pop/Funk','Jungle',
 'Cabaret','New-Wave','Psychadelic','Rave','Showtunes',
 'Lo-Fi','Tribal','Acid-Punk','Acid Jazz','Polka','Retro',
 'Musical','Rock','Hard-Rock','Folk','Folk-Rock',
 'National Folk','Swing','Fast-Fusion','Bebob','Latin','Revival',
 'Celtic','Bluegrass','Avantgarde','Gothic-Rock','Progressive-Rock',
 'Psychedelic Rock','Symphonic Rock','Slow-Rock','Big-Band',
 'Chorus','Easy-Listening','Acoustic','Chanson',
 'Opera','Chamber-Music','Sonata','Symphony','Satire','Slow-Jam','Club','Tango','Samba',
 'Folklore','Ballad','Power Ballad','Rhythmic Soul','Freestyle',
 'Duet','Punk-Rock','Acapella','Euro-House','Dance-Hall', 'Pop-Punk',
  'Alt-Metal', 'Post-Hardcore', 'Hardcore', 'Math-Rock'
  ],
  commitment: [
    'Low(1-3)',
    'Mid-low(3-6)',
    'Mid(6-10)',
    'Mid-High(10-14)',
    'High(14+)'
  ],
  post_type: [
    'Band',
    'Side-Project',
    'Recording',
    'One-off (gig)',
    'Shed'
  ],
  makeOptions(arr){
    return arr.map((item, key) => <option key={key} value={item}>{item}</option>)
  },

}

export default Lists;<file_sep>import React from 'react';
import { Link } from 'react-router-dom';
import './PostListItem.css';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'

class PostListItem extends React.Component {


  state = {
    isCollapsed: true
  }

  toggleCollapse = (ev) => {
    ev.preventDefault();
    this.setState({
      isCollapsed: !this.state.isCollapsed
    })
  }

  classByIndex = index => {
    if (index === 0 || index % 2 === 0) {
        return 'odd-item';
      } else {
        return 'even-item';
      }
  }

 

  determineContent = post => { 
    let buttonText = this.buttonText();
    let indexClass = this.classByIndex(this.props.index);
    if(this.state.isCollapsed){ 
      return (
        <div className={indexClass}>
          <Link to={`/posts/${post.id}`}>
            <h2 className='item-header'>{post.style}</h2>
          </Link>
          <p><strong>Location: </strong>{post.location}</p>
          <p><strong>Need: </strong>{post.instruments_need.split(' ').join(', ')}</p>
          <p>{this.truncDesc(post.description)}</p>
        <button onClick={this.toggleCollapse} className='toggle'>{buttonText}</button>
        </div>
      )
    }
    else return (
      <div className={indexClass}>
      <Link to={`/posts/${post.id}`}>
        <h2 className='item-header'>{post.style}</h2>
      </Link>
      <p>Posted by: <em>{post.author.user_name}</em> on {new Date(post.author.date_created).toLocaleDateString('en-US')}</p>
      <ul className='post-item-attr'>
        <li><strong>Location: </strong>{post.location}</li>
        <li><strong>Need: </strong>{post.instruments_need.split(' ').join(', ')}</li>
        <li><strong>Style:</strong> {post.style}</li>
        <li><strong>Availability:</strong> {post.commitment}</li>
        <li><strong>Desired Skill:</strong> {post.skill_lvl}</li>
      </ul>
      <p>{post.description}</p>
      <button onClick={this.toggleCollapse} className='toggle'>{buttonText}</button>
      </div>
    )
  } 

  buttonText = () => {
    if(this.state.isCollapsed){
      return <FontAwesomeIcon size='lg' icon='arrow-alt-circle-down' />
    }
    else return <FontAwesomeIcon size='lg' icon='arrow-alt-circle-up' />
  }

  truncDesc = description => {
    const newDesc = description.split(' ').splice(0, 8).join(' ').concat('...')
    return newDesc;
  }

  render(){
    const { post } = this.props;
    let content = this.determineContent(post);
    return (
      <>
      {content}
      </>
    )
  }

}

export default PostListItem;


<file_sep>import React from 'react';
import ReactDOM from 'react-dom';
import { BrowserRouter } from 'react-router-dom';
import './index.css';
import { PostProvider } from './contexts/PostContext';
import { PostListProvider } from './contexts/PostListContext';
import {HeaderProvider} from './contexts/HeaderContext';
import App from './components/App/App';
import { library } from '@fortawesome/fontawesome-svg-core';
import {faArrowAltCircleDown, faArrowAltCircleUp} from '@fortawesome/free-solid-svg-icons';

library.add(faArrowAltCircleDown, faArrowAltCircleUp);


ReactDOM.render(
    <BrowserRouter>
      <PostListProvider>
        <PostProvider>
          <HeaderProvider>
          <App />
          </HeaderProvider>
        </PostProvider>
      </PostListProvider>
    </BrowserRouter>
  , 
  document.getElementById('root'));


<file_sep>import React from 'react';
import Header from '../../components/Header/Header';
import PostForm from '../../components/PostForm/PostForm';


class PostFormPage extends React.Component {


  static defaultProps = {
    histroy: {
      push: () => {},
    }
  }
  handlePostSuccess = () => {
    const { history } = this.props
    history.push('/posts')
  }

  render(){
    return (
      <>
      <Header />
      <div>
        <PostForm onPostSuccess={this.handlePostSuccess} />
      </div>
      </>
    )
  }

}

export default PostFormPage;<file_sep>import React from 'react';
import ReactDOM from 'react-dom';
import CommentForm from '../components/CommentForm/CommentForm';
import renderer from 'react-test-renderer';



it('renders without crashing', () => {
  const div = document.createElement('div');

  ReactDOM.render(<CommentForm />, div);

  ReactDOM.unmountComponentAtNode(div);
});

it('renders UI as expected', () => {
  const tree = renderer
      .create(<CommentForm />)
      .toJSON();
      expect(tree).toMatchSnapshot();
})<file_sep>import React from 'react';
import { Switch, Route } from 'react-router-dom';
import PrivateRoute from '../../Utils/PrivateRoute';
import PublicOnlyRoute from '../../Utils/PublicOnlyRoute';
import LandingPage from '../../routes/LandingPage/LandingPage';
import SignupPage from '../../routes/SignupPage/SignupPage'
import PostListPage from '../../routes/PostListPage/PostListPage'
import LoginPage from '../../routes/LoginPage/LoginPage';
import PostFormPage from '../../routes/PostFormPage/PostFormPage';
import PostPage from '../../routes/PostPage/PostPage';
import './App.css';



class App extends React.Component {
  
  state = {
    hasError: false
  }
  


  render(){
    
    
    return (

      <div className="App">
        
          
        
        <Switch>
          <PublicOnlyRoute 
          exact path={'/'}
          component={LandingPage}
          />
          <Route
          exact path={'/posts'}
          component={PostListPage}
          />
          <PublicOnlyRoute
          exact path={'/login'}
          component={LoginPage}
          />
          <PublicOnlyRoute
          exact path={'/signup'}
          component={SignupPage}
          />
          <PrivateRoute
            exact path={'/postform'}
            component={PostFormPage}
          />
          <PrivateRoute
            path={'/posts/:postId'}
            component={PostPage}
            />
        </Switch>
      </div>
    );
  }
}

export default App;
<file_sep>import React from 'react';
import AuthApiService from '../../services/AuthApiService';



class LoginForm extends React.Component {

  static defaultProps = {
    onLoginSuccess: () => {}
  }

  state = {error: null}

  handleSubmitJwtAuth = ev => {
    ev.preventDefault();
    this.setState({error: null});
    const {user_name, password} = ev.target;

    AuthApiService.postLogin({
      user_name: user_name.value,
      password: <PASSWORD>,
    })
    .then(res => {
      user_name.value = '';
      password.value = '';

      this.props.onLoginSuccess(res.authToken);
    })
    .catch(res => {
      this.setState({error: res.error})
    })
  }


  render(){

    const {error} = this.state;
    return (
      <form className='login-form' onSubmit={this.handleSubmitJwtAuth}>
        <div role='alert'>
          {error && <p style={{color: 'red'}}>{error}</p>}
        </div>       
        <div className='user_name form-row'>
          <label htmlFor='login-user'>User name: </label>
          <input className='input' type='text' name='user_name' id='login-user' required/>
        </div>
        <div className='password form-row'>
          <label htmlFor='login-password'>Password: </label>
          <input className='input' type='password' name='password' id='login-password' required/>
        </div>
        <button type='submit' className='login-page-button'>Login</button>
      </form>
    )
  }
}

export default LoginForm;<file_sep>import React from 'react';
import ReactDOM from 'react-dom';
import PostListItem from '../components/PostListItem/PostListItem';
import {MemoryRouter} from 'react-router-dom';
import renderer from 'react-test-renderer';




const dummyPost = {
  id: 1,
  style: 'Rock',
  location: 'Atlanta (GA)',
  instruments_need: 'Drums Piano',
  commitment: 'Low(1-3)',
  skill_lvl: 'Hobbyist',
  description: 'hello'
}

it('renders without crashing', () => {
  const div = document.createElement('div');

  ReactDOM.render(<MemoryRouter><PostListItem post={dummyPost} index={1} /></MemoryRouter>, div);

  ReactDOM.unmountComponentAtNode(div);
});

it('renders UI as expected', () => {
  const tree = renderer
      .create(<MemoryRouter><PostListItem post={dummyPost} index={1}/></MemoryRouter>)
      .toJSON();
      expect(tree).toMatchSnapshot();
})<file_sep>import React from 'react';
import './LandingPage.css';
import { Link } from 'react-router-dom';


function LandingPage() {



  return (
    <div className='landing-page'>

      <h1 className='lp-header'>BandBridge</h1>

      <section className='lp-info'>
        <h2>Find your band</h2>
        <p className='bb-desc'>BandBridge helps connect you with artists that share your musical interests.
         Make an account to join the conversation or continue as a guest to check it out.</p>
      </section>

      <section className='lp-links-container'>
        <div className='lp-links'>
          <Link to='/signup'><button className='lp-button'>Sign up</button></Link>
          <Link to='/login'><button className='lp-button'>Login</button></Link>
          <Link to='/posts'><button className='lp-button'>Guest</button></Link>
        </div>
      </section>
    </div>
  )

}

export default LandingPage;<file_sep>export default {
  API_ENDPOINT: process.env.REACT_APP_API_ENDPOINT || 'http://localhost:8000/api',
  TOKEN_KEY: 'bandbridge-key',
  USER_KEY: 'bandbridge-user',
}



<file_sep>import config from '../config';
import TokenService from './TokenService';

const PostApiService = {
  getAllPosts(){
    return fetch(`${config.API_ENDPOINT}/posts`)
      .then(res => 
        (!res.ok)
         ? res.json().then(e => Promise.reject(e))
         : res.json()
        ) 
  },
  getPost(postId){
    return fetch(`${config.API_ENDPOINT}/posts/${postId}`, {
      headers: {
        'authorization': `bearer ${TokenService.getAuthToken()}`
      },
    })
    .then(res => 
      (!res.ok) 
      ? res.json().then(e => Promise.reject(e))
      : res.json()
    )
  },
  makePost(post){
    return fetch(`${config.API_ENDPOINT}/posts`, {
      method: 'POST',
      headers: {
        'content-type': 'application/json',
        'authorization': `bearer ${TokenService.getAuthToken()}`
      },
      body: JSON.stringify(post),
    })
      .then(res => 
        (!res.ok)
        ? res.json().then(e => Promise.reject(e))
        : res.json()
        )
  },
  getPostComments(postId){
    return fetch(`${config.API_ENDPOINT}/posts/${postId}/comments`, {
      headers: {
        'authorization': `bearer ${TokenService.getAuthToken()}`
      },
    })
    .then(res =>
      (!res.ok)
      ? res.json().then(e => Promise.reject(e))
      : res.json()
      )
  },
  getPostsByLocation(location){
    return fetch(`${config.API_ENDPOINT}/posts?location=${location}`, {
      header: {
        'authorization': `bearer ${TokenService.getAuthToken()}`
      },
    }) 
    .then(res => 
      (!res.ok)
      ? res.json().then(e => Promise.reject(e))
      : res.json())
  },
  getFilteredPosts(...args){
    console.log('args', args)
    let instrument = args[0];
    let location = args[1];
    let queryString= `${config.API_ENDPOINT}/posts?`;
    if(instrument){
      queryString += `instrument=` + instrument;
    } 
    if (location){
      queryString += `&location=` + location;
    }
    return fetch(queryString, {
      header: {
        'authorization': `bearer ${TokenService.getAuthToken()}`
      },
    })
    .then(res =>
      (!res.ok)
      ? res.json().then(e => Promise.reject(e))
      : res.json())
  },
  getPostsByInstrument(instrument){
    return fetch(`${config.API_ENDPOINT}/posts?instrument=${instrument}`, {
      header: {
        'authorization': `bearer ${TokenService.getAuthToken()}`
      },
    }) 
    .then(res => 
      (!res.ok)
      ? res.json().then(e => Promise.reject(e))
      : res.json())
  },
  postComment(postId, text){
    return fetch(`${config.API_ENDPOINT}/posts/${postId}/comments`, {
      method: 'POST',
      headers: {
        'content-type': 'application/json',
        'authorization': `bearer ${TokenService.getAuthToken()}`
      },
      body: JSON.stringify({
        post_id: postId,
        text,
      }),
    })
    .then(res => 
      (!res.ok)
      ? res.json().then(e => Promise.reject(e))
      : res.json()
      )
  },
}

export default PostApiService;<file_sep>import React from 'react';
import { Link } from 'react-router-dom';
import Header from '../../components/Header/Header';
import PostListItem from '../../components/PostListItem/PostListItem';
import PostListContext from '../../contexts/PostListContext';
import PostApiService from '../../services/PostApiService';
import Lists from '../../Utils/Lists';
import './PostListPage.css';

class PostListPage extends React.Component {

  static contextType = PostListContext;

  

  componentDidMount(){
    this.context.clearError()
    PostApiService.getAllPosts()
      .then(this.context.setPostList)
      .catch(this.context.setError)
  }
  

  handleClearFilter = ev => {
    ev.preventDefault();
    PostApiService.getAllPosts()
      .then(this.context.setPostList)
      .catch(this.context.setError)
  }

  handleFilter = ev => {
    ev.preventDefault();
    const {instrument_filter, location_filter } = ev.target;
    PostApiService.getFilteredPosts(instrument_filter.value, location_filter.value)
      .then(this.context.setPostList)
      .catch(this.context.setError)
  }


  renderPosts() {
    const { postList = []} =  this.context;
    return postList.map((post, index) => 
      <PostListItem 
      index={index}
      key={post.id}
      post={post}/>
      )
  }

  render() {
    const {error} = this.context;
    let locations = Lists.makeOptions(Lists.locationOptions);
    let instruments = Lists.makeOptions(Lists.instrumentOptions)
    return (
      <>
      <Header />
      <div className='post-list-filters'>
        <form onSubmit={this.handleFilter} className='filter-form'>
        <select name='location_filter' defaultValue='' id='location_filter' className='filter-select'>
          <option value='' disabled >Choose location</option>
           {locations}
          </select>
          <select name='instrument_filter' defaultValue='' id='instrument_filter' className='filter-select'>
          <option value='' disabled >Choose instrument</option>
           {instruments}
          </select>
          <button type='submit' className='filter-button'>Filter</button>
        <button type='button' onClick={this.handleClearFilter} className='filter-button'>Clear</button>
        </form>
        <Link to={'/postform'}><button className='create-post-button'>Create Post</button></Link>
      </div>
      {error && <div className='error'><p>Something Went Wrong</p></div>}
      <div className='post-list'>
      {this.renderPosts()}
      </div>
      </>
    )
  }
}

export default PostListPage;<file_sep>BandBridge Client
=================

> ## Summary

BandBridge connects musicians based on location, style, skill and availability. Users are able to make posts specifying their musical needs or aspirations and can filter their feed of posts to find projects and bands that they might be interested in. Users can also make comments on posts to start a conversation and hopefully start a band.

[Live App](https://band-bridge.ygnick.now.sh/)


Landing Page

![Landing page image](./screenshots/LandingPage.png "Landing page")

Registration Page

![Registration Page](./screenshots/RegistrationPage.png "Registration page")

Post List Page

![Post list page image](./screenshots/PostListPage.png "Post list page")

Post Form Page

![Post form page image](./screenshots/PostFormPage.png "Post form page")

Post Page

![Post page image](./screenshots/PostPage.png "Post page")

Technologies used
* React
* react-dom
* react-router-dom
* jwt-decode

To run development(local) server - npm start

To deploy 
1. npm run build 
2. now ./build



