Commit 47420959 authored by Zac Wood's avatar Zac Wood
Browse files

Merge branch 'v2-ui' into 'dev-v2'

V2 ui

See merge request !25
parents 894575fb 9a163119
Pipeline #2947 passed with stage
in 2 minutes and 14 seconds
import * as React from 'react';
import SearchBar from '../components/SearchBar';
import CourseSection from '../util/CourseSection';
import CourseSectionList from './CourseSectionList';
import { Row, Col, Alert } from 'reactstrap';
import { SearchState } from '../reducers/search.reducer';
interface SearchRootProps {
search: SearchState;
searchCourseSections: (crn: string) => void;
addCourseSection: (courseSectionToAdd: CourseSection) => void;
}
/**
* Renders the SearchBar and a list of CourseSections returned from the Search.
* Also renders an error if there is one.
*/
const SearchRoot = ({ search, searchCourseSections, addCourseSection }: SearchRootProps) => (
<div>
<SearchBar onSearch={searchCourseSections} />
{search.error !== '' ? (
<Error />
) : (
<CourseSectionList
courseSectionActionButtonText="Add to schedule"
courseSections={search.courseSections}
courseSectionAction={addCourseSection}
/>
)}
</div>
);
/**
* Renders a basic error message.
*/
const Error = () => (
<Row className="justify-content-center">
<Col md="8">
<Alert color="danger">Could not find course section with the given CRN.</Alert>
</Col>
</Row>
);
export default SearchRoot;
import { connect } from 'react-redux';
import { removeCourseSection } from '../actions/schedule/schedule.actions';
import ScheduleRoot from '../components/ScheduleRoot';
import { State } from '../reducers';
import CourseSection from '../util/CourseSection';
import ApiService from '../util/ApiService';
import { downloadFile } from '../util/utilities';
// Takes the current Redux state and returns objects which will be
// passed to the component as Props
const mapStateToProps = (state: State) => {
const crns = state.schedule ? state.schedule.map(section => section.crn) : [];
return {
schedule: state.schedule,
generateCalendarUrl: () => ApiService.generateCalendarUrl(crns),
openCalendarAsWebcal: () => ApiService.openCalendarAsWebcal(crns),
downloadIcs: async () => {
const icsText = await ApiService.fetchICal(crns);
downloadFile(icsText, 'GMU Fall 2018.ics');
},
};
};
// Pass mapStateToProps and other values to the component's props
export default connect(
mapStateToProps,
{ removeCourseSection }
)(ScheduleRoot);
import { connect } from 'react-redux';
import { addCourseSection } from '../actions/schedule/schedule.actions';
import { searchCourseSections } from '../actions/search/search.actions';
import SearchRoot from '../components/SearchRoot';
import { State } from '../reducers';
// Takes the current Redux state and returns objects which will be
// passed to the component as Props
const mapStateToProps = (state: State) => ({
search: state.search,
});
// Pass mapStateToProps and other values to the component's props
export default connect(
mapStateToProps,
{ searchCourseSections, addCourseSection }
)(SearchRoot);
body {
background-color: #E4E4E4;
}
.fa-stack[data-count]:after {
position: absolute;
right: 0%;
top: 1%;
content: attr(data-count);
font-size: 30%;
padding: 0.6em;
border-radius: 999px;
line-height: 0.75em;
color: black;
background: rgba(164, 164, 164, 0.85);
text-align: center;
min-width: 2em;
font-weight: bold;
}
#cart {
color: black;
background: initial;
border-color: #e4e4e4;
box-shadow: initial;
}
/**
* index.tsx
*
* The entry point for the application. Apply global styling, configure high
* level (global application) settings and render <App />. Simple.
*/
// Apply Global Masonstrap styling
import 'masonstrap/build/css/masonstrap.min.css';
import 'masonstrap/build/js/masonstrap.min.js';
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import { applyMiddleware, compose, createStore } from 'redux';
import ReduxThunk from 'redux-thunk';
import App from './components/App';
import { allReducers } from './reducers';
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 addOns;
if (isProduction || !extension) {
addOns = compose(applyMiddleware(ReduxThunk));
} else {
addOns = compose(
applyMiddleware(ReduxThunk),
extension
);
}
// Attach all reducers + addOns to the Redux store
const store = createStore(allReducers, addOns);
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);
/**
* reducers/index.ts
*
* Wrap all reducers in a common object to be returned to the store.
*/
import { schedule, ScheduleState } from './schedule.reducer';
import { search, SearchState } from './search.reducer';
import { combineReducers } from 'redux';
// The global state
export interface State {
schedule: ScheduleState;
search: SearchState;
}
/**
* Combine all reducers into one object to attach to the store
*/
export const allReducers = combineReducers({ search, schedule });
/**
* reducers/schedule.reducer.ts
*
* Perform operations on the current state of the "Schedule" list in the store
* and return a new definition of the state.
*/
import { ADD_COURSE_SECTION, REMOVE_COURSE_SECTION } from '../actions/schedule/schedule.action-types';
import { ScheduleAction } from '../actions/schedule/schedule.actions';
import CourseSection from '../util/CourseSection';
export type ScheduleState = CourseSection[];
export const schedule = (state: ScheduleState = [], action: ScheduleAction) => {
switch (action.type) {
case ADD_COURSE_SECTION:
return state.findIndex(s => s.crn === action.courseSection.crn) === -1
? [...state, action.courseSection]
: state;
case REMOVE_COURSE_SECTION:
return state.filter(s => s.crn !== action.courseSection.crn);
default:
return state;
}
};
/**
* reducers/search.reducer.ts
*
* Perform operations on the current state of the "search.searchedSections"
* list in the store and return a new definition of the state.
*/
import { SET_SEARCH_RESULTS } from '../actions/search/search.action-types';
import { SearchAction } from '../actions/search/search.actions';
import CourseSection from '../util/CourseSection';
export interface SearchState {
courseSections: CourseSection[];
error: string;
}
const initialState: SearchState = {
courseSections: [],
error: '',
};
export const search = (state: SearchState = initialState, action: SearchAction): SearchState => {
switch (action.type) {
case SET_SEARCH_RESULTS:
return { courseSections: action.searchResults, error: action.error };
default:
return state;
}
};
class ApiService {
private apiRoot: string;
private webcalUrl: string;
public constructor(apiRoot: string, webcalUrl: string) {
this.apiRoot = apiRoot;
this.webcalUrl = webcalUrl;
}
searchCourseSections = async (crn: string): Promise<any[]> =>
fetchJson(`${this.apiRoot}/course_sections?crn=${crn}`);
generateCalendarUrl = (crns: string[]): string => `${this.apiRoot}/schedules?crns=${crns.join(',')}`;
openCalendarAsWebcal = (crns: string[]) => {
window.open(`${this.webcalUrl}/schedules?crns=${crns.join(',')}`, '_self');
};
fetchICal = async (crns: string[]): Promise<string> =>
fetch(this.generateCalendarUrl(crns)).then(response => response.text());
}
const fetchJson = async (url: string): Promise<any> => fetch(url).then(response => response.json());
const postJson = (endpoint: string, data: any): Promise<Response> =>
fetch(endpoint, {
method: 'POST',
body: JSON.stringify(data),
headers: {
'Content-Type': 'application/json',
},
});
const local = 'localhost:3000/api';
const remote = `${window.location.hostname}/api`;
const apiUrl = process.env.NODE_ENV === 'development' ? `http://${local}` : `https://${remote}`;
const webcalUrl = process.env.NODE_ENV === 'development' ? `webcal://${local}` : `webcal://${remote}`;
export default new ApiService(apiUrl, webcalUrl);
/**
* util/CourseSection.ts
*
* Common object interface for all "Section"s.
*/
interface CourseSection {
id: number;
name: string;
title: string;
crn: string;
instructor: string;
location: string;
days: string;
startTime: string;
endTime: string;
}
export default CourseSection;
/**
* util/utilities.ts
*
* Reusable deterministic functions.
*/
import * as FileSaver from 'file-saver';
export const downloadFile = (text: string, fileName: string) =>
FileSaver.saveAs(new Blob([text], { type: 'text/calendar;charset=utf-8' }), fileName);
{
"compilerOptions": {
"outDir": "./dist/",
"sourceMap": true,
"noImplicitAny": true,
"module": "commonjs",
"target": "es6",
"jsx": "react"
},
"include": ["./src/**/*"]
}
const path = require('path');
const webpack = require('webpack');
const CleanWebpackPlugin = require('clean-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const HtmlWebpackPluginConfig = new HtmlWebpackPlugin({
template: './index.html',
filename: 'index.html',
inject: 'body',
favicon: 'favicon.ico',
});
module.exports = {
entry: './src/index.tsx',
output: {
filename: '[name].[hash].js',
path: path.resolve(__dirname, 'dist'),
},
resolve: {
extensions: ['.js', '.json', '.ts', '.tsx'],
},
module: {
rules: [
{
test: /\.tsx?$/,
loader: 'awesome-typescript-loader',
exclude: /node_modules/,
},
{
enforce: 'pre',
test: /\.js$/,
loader: 'source-map-loader',
},
{
test: /\.css$/,
use: [
{
loader: 'style-loader',
},
{
loader: 'css-loader',
},
],
},
{
test: /\.woff(\?.+)?$/,
use: 'url-loader?limit=10000&mimetype=application/font-woff',
},
{
test: /\.woff2(\?.+)?$/,
use: 'url-loader?limit=10000&mimetype=application/font-woff',
},
{
test: /\.ttf(\?.+)?$/,
use: 'file-loader',
},
{
test: /\.eot(\?.+)?$/,
use: 'file-loader',
},
{
test: /\.svg(\?.+)?$/,
use: 'file-loader',
},
{
test: /\.(js)$/,
exclude: /(node_modules)/,
loader: 'babel-loader',
},
],
},
plugins: [new webpack.HotModuleReplacementPlugin(), new CleanWebpackPlugin(['dist']), HtmlWebpackPluginConfig],
};
const path = require('path');
const merge = require('webpack-merge');
const common = require('./webpack.common.js');
module.exports = merge(common, {
mode: 'development',
devtool: 'source-map',
devServer: {
contentBase: path.resolve(__dirname, 'dist'),
compress: true,
host: '0.0.0.0',
port: 8080,
hot: true,
publicPath: '/',
historyApiFallback: true,
},
});
const merge = require('webpack-merge');
const common = require('./webpack.common.js');
module.exports = merge(common, {
mode: 'production',
});
This source diff could not be displayed because it is too large. You can view the blob instead.
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