Commit f3699b03 authored by Andrew Hrdy's avatar Andrew Hrdy

Added redux

parent 7d3267e4
......@@ -22,6 +22,7 @@
"dependencies": {
"@types/react": "^16.3.12",
"@types/react-dom": "^16.0.5",
"@types/react-redux": "^6.0.2",
"babel-loader": "^7.1.4",
"clean-webpack-plugin": "^0.1.19",
"css-loader": "^0.28.11",
......@@ -31,6 +32,9 @@
"masonstrap": "https://git.gmu.edu/srct/masonstrap.git",
"react": "^16.3.2",
"react-dom": "^16.3.2",
"react-redux": "^5.0.7",
"redux": "^4.0.0",
"redux-thunk": "^2.3.0",
"style-loader": "^0.21.0",
"url-loader": "^1.0.1"
},
......
export const ADD_SECTION = '[Schedule] ADD_SECTION';
export const REMOVE_SECTION = '[Schedule] REMOVE_SECTION';
\ No newline at end of file
import { ADD_SECTION, REMOVE_SECTION } from "./schedule.action-types";
import { Section } from "../../ts/section";
export interface ScheduleAction {
type: string;
section: Section;
}
export const addSection = (section: Section): ScheduleAction => {
return {
type: ADD_SECTION,
section: section
};
}
export const removeSection = (section: Section): ScheduleAction => {
return {
type: REMOVE_SECTION,
section: section
};
}
\ No newline at end of file
export const SET_SEARCH_SECTIONS = '[Search] SET_SECTIONS';
\ No newline at end of file
import { SET_SEARCH_SECTIONS } from './search.action-types';
import { Section } from '../../ts/section';
export interface SearchAction {
type: string;
sections: Section[];
}
export const searchSections = (crn: string) => async (dispatch: any) => {
const response = await fetch(`http://localhost:3000/api/search?crn=${crn}`);
const object = await response.json();
const section: Section = {
id: object.id,
name: object.name,
title: object.title,
crn: object.crn,
instructor: object.instructor,
location: object.location,
days: object.days,
startTime: object.start_time,
endTime: object.end_time,
};
dispatch({
type: SET_SEARCH_SECTIONS,
sections: [section]
});
}
\ No newline at end of file
import * as React from 'react';
import { fetchSectionWithCRN, Section } from '../ts/section';
import SearchBar from './SearchBar';
import SectionList from './SectionList';
interface Props {
addSearchResultCallback?: (section: Section) => void;
}
interface State {
sections: Section[];
}
export default class Search extends React.Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = { sections: [] };
}
render() {
return (
<div>
<SearchBar onSearch={this.searchForSections} />
<SectionList
sections={this.state.sections}
buttonText="Add to schedule"
selectSectionCallback={this.props.addSearchResultCallback}
/>
</div>
);
}
searchForSections = (crn: string) => {
fetchSectionWithCRN(crn).then(section => this.setState({ sections: [section] }));
};
}
......@@ -2,16 +2,19 @@ import * as React from 'react';
import { Section } from '../ts/section';
import { downloadCalendar, ENDPOINTS, postData } from '../ts/utilities';
import Search from './Search';
import SectionList from './SectionList';
interface State {
currentSchedule: Section[];
import SectionList from '../components/SectionList';
import { connect } from 'react-redux';
import {State} from '../reducers';
import { removeSection } from '../actions/schedule/schedule.actions';
interface AppProps {
schedule: Section[];
removeSection: (section: Section) => any;
}
export default class App extends React.Component<any, State> {
constructor(props: any) {
class App extends React.Component<AppProps> {
constructor(props: AppProps) {
super(props);
this.state = { currentSchedule: [] };
}
render() {
......@@ -19,38 +22,32 @@ export default class App extends React.Component<any, State> {
<div>
<h1>Schedules</h1>
<h2>Search</h2>
<Search addSearchResultCallback={this.addSectionToCurrentScheduleIfUnique} />
<Search />
<h2>Your schedule</h2>
<SectionList
sections={this.state.currentSchedule}
sections={this.props.schedule}
buttonText="Remove from schedule"
selectSectionCallback={this.removeFromSchedule}
selectSectionCallback={this.props.removeSection}
/>
<button onClick={this.generateSchedule}>Generate Schedule</button>
</div>
);
}
addSectionToCurrentScheduleIfUnique = (section: Section) => {
if (!this.isSectionInSchedule(section)) {
this.setState({ currentSchedule: [...this.state.currentSchedule, section] });
}
};
isSectionInSchedule = (section: Section) =>
this.state.currentSchedule.find(sectionInSchedule => section == sectionInSchedule);
// TODO: Only view logic should be in the component.
generateSchedule = () => {
const crns = this.state.currentSchedule.map(section => section.crn);
const crns = this.props.schedule.map(section => section.crn);
postData(ENDPOINTS.generateCalendar, crns)
.then(response => response.text())
.then(icalText => downloadCalendar(icalText));
};
removeFromSchedule = (section: Section) => {
this.setState({
currentSchedule: this.state.currentSchedule.filter(other => section !== other),
});
};
}
const mapStateToProps = (state: State) => ({
schedule: state.schedule
});
export default connect(mapStateToProps, {
removeSection
})(App);
\ No newline at end of file
import * as React from 'react';
import { Section } from '../ts/section';
import SearchBar from '../components/SearchBar';
import SectionList from '../components/SectionList';
import { connect } from 'react-redux';
import { State } from '../reducers';
import { searchSections } from '../actions/search/search.actions';
import { addSection } from '../actions/schedule/schedule.actions';
interface SearchProps {
searchedSections: Section[];
searchSections: (crn: string) => any;
addSection: (section: Section) => any;
}
class Search extends React.Component<SearchProps> {
constructor(props: SearchProps) {
super(props);
}
render() {
return (
<div>
<SearchBar onSearch={this.props.searchSections} />
<SectionList
sections={this.props.searchedSections}
buttonText="Add to schedule"
selectSectionCallback={this.props.addSection}
/>
</div>
);
}
}
const mapStateToProps = (state: State) => ({
searchedSections: state.search.searchedSections
});
export default connect(mapStateToProps, {
searchSections,
addSection
})(Search);
\ No newline at end of file
import 'masonstrap/build/css/masonstrap.min.css';
import 'masonstrap/build/js/masonstrap.min.js';
import {applyMiddleware, compose, createStore} from 'redux';
import ReduxThunk from 'redux-thunk';
import {Provider} from 'react-redux';
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import App from './components/App';
import App from './containers/App';
import { reducers } from './reducers';
ReactDOM.render(<App />, document.getElementById('root'));
declare global {
interface Window {
__REDUX_DEVTOOLS_EXTENSION__?: () => any;
}
}
const extension = window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__();
const isProduction = process.env.NODE_ENV === 'production';
let enhance;
if (isProduction || !extension) {
enhance = compose(applyMiddleware(ReduxThunk));
} else {
enhance = compose(applyMiddleware(ReduxThunk), extension);
}
const store = createStore(reducers, enhance);
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>, document.getElementById('root'));
\ No newline at end of file
import { schedule, ScheduleState } from './schedule.reducer';
import { search, SearchState } from './search.reducer';
export interface State {
schedule: ScheduleState
search: SearchState
}
const defaultState: State = {
schedule: [],
search: {
searchedSections: []
}
};
export const reducers = (state: State = defaultState, action: any) => ({
schedule: schedule(state.schedule, action),
search: search(state.search, action)
})
\ No newline at end of file
import { Section } from '../ts/section';
import { ADD_SECTION, REMOVE_SECTION } from '../actions/schedule/schedule.action-types';
import { ScheduleAction } from '../actions/schedule/schedule.actions';
export type ScheduleState = Section[];
export const schedule = (state: ScheduleState = [], action: ScheduleAction) => {
switch (action.type) {
case ADD_SECTION:
return state.findIndex(s => s.crn === action.section.crn) === -1 ?
[...state, action.section] :
state;
case REMOVE_SECTION:
return state.filter(s => s.crn !== action.section.crn);
default:
return state;
}
};
\ No newline at end of file
import { SearchAction } from './../actions/search/search.actions';
import { Section } from './../ts/section';
import { SET_SEARCH_SECTIONS } from '../actions/search/search.action-types';
export interface SearchState {
searchedSections: Section[];
}
export const search = (state: SearchState = {searchedSections: []}, action: SearchAction) => {
switch (action.type) {
case SET_SEARCH_SECTIONS:
return Object.assign({}, state, {
searchedSections: [...action.sections]
});
default:
return state;
}
}
\ No newline at end of file
......@@ -9,20 +9,3 @@ export interface Section {
startTime: string;
endTime: string;
}
export async function fetchSectionWithCRN(crn: string): Promise<Section> {
const response = await fetch(`http://localhost:3000/api/search?crn=${crn}`);
const object = await response.json();
return {
id: object.id,
name: object.name,
title: object.title,
crn: object.crn,
instructor: object.instructor,
location: object.location,
days: object.days,
startTime: object.start_time,
endTime: object.end_time,
};
}
......@@ -91,6 +91,13 @@
"@types/node" "*"
"@types/react" "*"
"@types/react-redux@^6.0.2":
version "6.0.2"
resolved "https://registry.yarnpkg.com/@types/react-redux/-/react-redux-6.0.2.tgz#10069b53db8e0920fd8656e068dcf10c53c9ad2a"
dependencies:
"@types/react" "*"
redux "^4.0.0"
"@types/react@*", "@types/react@^16.3.12":
version "16.3.17"
resolved "https://registry.yarnpkg.com/@types/react/-/react-16.3.17.tgz#d59d1a632570b0713946ed9c2949d994773633c5"
......@@ -3968,6 +3975,10 @@ hoek@2.x.x:
version "2.16.3"
resolved "https://registry.yarnpkg.com/hoek/-/hoek-2.16.3.tgz#20bb7403d3cea398e91dc4710a8ff1b8274a25ed"
hoist-non-react-statics@^2.5.0:
version "2.5.0"
resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-2.5.0.tgz#d2ca2dfc19c5a91c5a6615ce8e564ef0347e2a40"
home-or-tmp@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/home-or-tmp/-/home-or-tmp-2.0.0.tgz#e36c3f2d2cae7d746a857e38d18d5f32a7882db8"
......@@ -4273,7 +4284,7 @@ into-stream@^3.1.0:
from2 "^2.1.1"
p-is-promise "^1.1.0"
invariant@^2.2.2:
invariant@^2.0.0, invariant@^2.2.2:
version "2.2.4"
resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6"
dependencies:
......@@ -4977,6 +4988,10 @@ locate-path@^2.0.0:
p-locate "^2.0.0"
path-exists "^3.0.0"
lodash-es@^4.17.5:
version "4.17.10"
resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.10.tgz#62cd7104cdf5dd87f235a837f0ede0e8e5117e05"
lodash._basecopy@^3.0.0:
version "3.0.1"
resolved "https://registry.yarnpkg.com/lodash._basecopy/-/lodash._basecopy-3.0.1.tgz#8da0e6a876cf344c0ad8a54882111dd3c5c7ca36"
......@@ -6828,6 +6843,17 @@ react-dom@^16.3.2:
object-assign "^4.1.1"
prop-types "^15.6.0"
react-redux@^5.0.7:
version "5.0.7"
resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-5.0.7.tgz#0dc1076d9afb4670f993ffaef44b8f8c1155a4c8"
dependencies:
hoist-non-react-statics "^2.5.0"
invariant "^2.0.0"
lodash "^4.17.5"
lodash-es "^4.17.5"
loose-envify "^1.1.0"
prop-types "^15.6.0"
react@^16.3.2:
version "16.4.0"
resolved "https://registry.yarnpkg.com/react/-/react-16.4.0.tgz#402c2db83335336fba1962c08b98c6272617d585"
......@@ -6966,6 +6992,17 @@ reduce-function-call@^1.0.1:
dependencies:
balanced-match "^0.4.2"
redux-thunk@^2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-2.3.0.tgz#51c2c19a185ed5187aaa9a2d08b666d0d6467622"
redux@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/redux/-/redux-4.0.0.tgz#aa698a92b729315d22b34a0553d7e6533555cc03"
dependencies:
loose-envify "^1.1.0"
symbol-observable "^1.2.0"
regenerate@^1.2.1:
version "1.4.0"
resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.0.tgz#4a856ec4b56e4077c557589cae85e7a4c8869a11"
......@@ -7927,7 +7964,7 @@ symbol-observable@1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.0.1.tgz#8340fc4702c3122df5d22288f88283f513d3fdd4"
symbol-observable@^1.1.0:
symbol-observable@^1.1.0, symbol-observable@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.2.0.tgz#c22688aed4eab3cdc2dfeacbb561660560a00804"
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment