diff --git a/components/Header.jsx b/components/Header.jsx
index 7b68583..880c93e 100644
--- a/components/Header.jsx
+++ b/components/Header.jsx
@@ -1,15 +1,14 @@
import React from 'react';
import NextHead from 'next/head';
-// import NProgress from 'nprogress';
-// import Router from 'next/router';
+import NProgress from 'nprogress';
+import Router from 'next/router';
-// Router.onRouteChangeStart = (url) => {
-// console.log(`Loading: ${url}`);
-// NProgress.start();
-// };
-//
-// Router.onRouteChangeComplete = () => NProgress.done();
-// Router.onRouteChangeError = () => NProgress.done();
+Router.onRouteChangeStart = (url) => {
+ NProgress.start();
+};
+
+Router.onRouteChangeComplete = () => NProgress.done();
+Router.onRouteChangeError = () => NProgress.done();
/**
* A header Component that provide Progress bar
diff --git a/components/Login.jsx b/components/Login.jsx
deleted file mode 100644
index 9cb825c..0000000
--- a/components/Login.jsx
+++ /dev/null
@@ -1,58 +0,0 @@
-import React from 'react';
-import TextField from '@material-ui/core/TextField';
-import Button from '@material-ui/core/Button';
-
-/**
- * Login Component that provide text fields and submit button.
- * @inheritDoc
- */
-class Login extends React.Component {
- static propTypes = {
- // classes: PropTypes.object.isRequired,
- };
-
- state = {
- email: 'email here',
- password: '',
- };
-
- handleChange = (email, password) => (event) => {
- this.setState({
- [email]: event.target.value,
- [password]: event.target.value,
- });
- };
-
- /**
- * @return {Element}
- */
- render() {
- return (
-
-
);
- }
-}
-
-export default Login;
diff --git a/components/PopMenu.jsx b/components/PopMenu.jsx
index 267badb..095c16e 100644
--- a/components/PopMenu.jsx
+++ b/components/PopMenu.jsx
@@ -30,6 +30,26 @@ const inStyle = {
fontSize: '14px',
};
+class CourseDetailsWindow extends React.Component {
+ render() {
+ const course = this.props.course;
+ return (
+
+
×
+
+ {`${course.name} ${course.title}`}
+
+
+
{`Instructor: ${course.instructor}`}
+
{`Terms: ${course.terms}`}
+
{`GE: ${course.geCategories}`}
+
{`Division: ${course.division}`}
+
{`Description: ${course.description}`}
+
+
+ );
+ }
+}
class PopMenu extends React.Component {
constructor(props) {
@@ -109,35 +129,23 @@ class PopMenu extends React.Component {
{
this.node = node;
}} style={lStyle} id="listDiv" onScroll={this.onListScroll}>
-
{data.map(({name, title, instructor, terms, description, geCategories, division}) => (
+ {data.map((course) => (
-
+
} modal>
- {close => (
-
-
×
-
- {`${name} ${title}`}
-
-
-
{`Instructor: ${instructor}`}
-
{`Terms: ${terms}`}
-
{`GE: ${geCategories}`}
-
{`Division: ${division}`}
-
{`Description: ${description}`}
-
-
- )}
+ {close =>
+
+ }
))}
diff --git a/components/Popups.jsx b/components/Popups.jsx
index a1c4bac..39937f5 100644
--- a/components/Popups.jsx
+++ b/components/Popups.jsx
@@ -1,13 +1,42 @@
import React from 'react';
import Popup from 'reactjs-popup';
+class CourseDetailsPanel extends React.Component {
+ render() {
+ const course = this.props.course;
+ return (
+
+
{'Instructor: '}{course.instructor}
+
{'Time: '}{course.time}
+
{'Location: '}{course.location}
+
+ );
+ }
+}
+
+class CourseDetailsWindow extends React.Component {
+ render() {
+ const course = this.props.course;
+ return (
+
+
×
+
+
+
+
{course.course_number}
+
+
+ );
+ }
+}
class Popups extends React.Component {
constructor(props) {
super(props);
- this.handleClick = this.handleClick.bind(this);
+ this.removeDefTags = this.removeDefTags.bind(this);
+ this.selectDefTag = this.selectDefTag.bind(this);
this.state = {
- tags: [] || props.tags,
+ deftags: [] || props.tags,
};
}
@@ -21,36 +50,37 @@ class Popups extends React.Component {
}),
};
- handleClick(e) {
+ removeDefTags() {
+ if (this.state.deftags === 'N/A'
+ || this.state.deftags === 'In Progress'
+ || this.state.deftags === 'Finished') {
+ this.state.deftags = '';
+ }
+ }
+
+ selectDefTag(e) {
+ this.removeDefTags();
if (e === 'N/A') {
- this.state.tags = 'N/A';
+ this.state.deftags = 'N/A';
}
if (e === 'In Progress') {
- this.state.tags = 'In Progress';
+ this.state.deftags = 'In Progress';
}
if (e === 'Finished') {
- this.state.tags = 'Finished';
+ this.state.deftags = 'Finished';
}
- console.log(`get tags: ${this.state.tags}`);
}
render() {
return
-
{this.props.myLists.course_title}} modal>
- {close => (
-
-
×
-
-
-
-
{this.props.myLists.course_number}
-
-
{'Instructor: '}{this.props.myLists.instructor}
-
{'Time: '}{this.props.myLists.time}
-
{'Location: '}{this.props.myLists.location}
-
-
- )}
+ {this.props.myLists.course_title}} modal>
+ {close =>
+ this.selectDefTag(tag)} />
+ }
;
}
diff --git a/components/Search.jsx b/components/Search.jsx
index 82d0b78..097738e 100644
--- a/components/Search.jsx
+++ b/components/Search.jsx
@@ -1,6 +1,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Configure, Highlight, Hits, SearchBox } from 'react-instantsearch/dom';
+
import { InstantSearch } from './Instantsearch';
/**
diff --git a/components/SearchBar.jsx b/components/SearchBar.jsx
deleted file mode 100644
index a8cf248..0000000
--- a/components/SearchBar.jsx
+++ /dev/null
@@ -1,23 +0,0 @@
-import React from 'react';
-
-import PopMenu from './PopMenu';
-
-class SearchBar extends React.Component {
-
-
-
-
-
-
- render() {
- return (
-
- )
- }
-
-
-}
-
-export default SearchBar;
\ No newline at end of file
diff --git a/components/Tooltip.jsx b/components/Tooltip.jsx
new file mode 100644
index 0000000..9047990
--- /dev/null
+++ b/components/Tooltip.jsx
@@ -0,0 +1,86 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+// The modal "window"
+const modalStyle = {
+ backgroundColor: '#fff',
+ borderRadius: 5,
+ maxWidth: 500,
+ minHeight: 300,
+ margin: '0 auto',
+ padding: 30,
+};
+
+class Tooltip extends React.Component {
+ static propTypes = {
+ content: PropTypes.object,
+ trigger: PropTypes.object,
+ };
+
+ static defaultProps = {
+ content: '',
+ trigger: '',
+ };
+
+ constructor(props) {
+ super(props);
+ this.handleMouseOver = this.handleMouseOver.bind(this);
+ this.handleMouseOut = this.handleMouseOut.bind(this);
+ this.handleClick = this.handleClick.bind(this);
+ this.handleOutsideClick = this.handleOutsideClick.bind(this);
+ this.state = {
+ isOpen: false,
+ };
+ }
+
+ handleMouseOver = () => {
+ this.setState({isOpen: true});
+ };
+
+ handleMouseOut = () => {
+ this.setState({isOpen: false});
+ };
+
+ handleClick = () => {
+ if (!this.state.isOpen) {
+ document.addEventListener('click', this.handleOutsideClick, false);
+ } else {
+ document.removeEventListener('click', this.handleOutsideClick, false);
+ }
+ this.setState({
+ isOpen: this.state.isOpen,
+ });
+ };
+
+ handleOutsideClick = (e) => {
+ if (this.node.contains(e.target)) {
+ return;
+ }
+ this.handleClick();
+ };
+
+
+ render() {
+ return (
+
+
+ {this.props.trigger}
+
+
+ {this.state.isOpen &&
+
+ {this.props.content}
+
+ }
+
+
+ );
+ }
+}
+
+export default Tooltip;
\ No newline at end of file
diff --git a/components/graph/CourseInfoCard.jsx b/components/graph/CourseInfoCard.jsx
new file mode 100644
index 0000000..9fc67e0
--- /dev/null
+++ b/components/graph/CourseInfoCard.jsx
@@ -0,0 +1,60 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+import { withStyles } from '@material-ui/core/styles';
+import Card from '@material-ui/core/Card';
+import CardContent from '@material-ui/core/CardContent';
+import CardHeader from '@material-ui/core/CardHeader';
+import Typography from '@material-ui/core/Typography';
+
+/**
+ * Define the style of components on this page
+ * @param theme
+ * @return {object}
+ */
+const styles = theme => ({
+ panel: {
+ 'maxWidth': 350,
+ 'maxHeight': 600,
+ // 'position': 'absolute',
+ 'margin-left': 'auto',
+ 'margin-right': 'auto',
+ 'z-index': 100,
+ 'top': theme.spacing.unit * 25,
+ 'right': theme.spacing.unit * 10,
+ },
+});
+
+/**
+ * @param classes {object}
+ * @param label {string}
+ * @param title {string}
+ * @param description {string}
+ * @return {Element}
+ * @constructor
+ */
+const CourseInfoCard = ({classes, label, title, description}) => {
+ return (
+
+
+
+ {description || 'Unavailable'}
+
+
+ );
+};
+
+CourseInfoCard.propTypes = {
+ classes: PropTypes.object.isRequired,
+ label: PropTypes.string.isRequired,
+ title: PropTypes.string.isRequired,
+ description: PropTypes.string.isRequired,
+};
+
+CourseInfoCard.defaultProps = {
+ title: 'Untitled',
+ description: 'Unavailable',
+ label: '- --',
+};
+
+export default withStyles(styles)(CourseInfoCard);
diff --git a/components/graph/GraphView.jsx b/components/graph/GraphView.jsx
new file mode 100644
index 0000000..b191e79
--- /dev/null
+++ b/components/graph/GraphView.jsx
@@ -0,0 +1,149 @@
+import React, { Component } from 'react';
+import PropTypes from 'prop-types';
+import Graph from 'react-graph-vis';
+
+import { withStyles } from '@material-ui/core/styles';
+
+/**
+ * Define the style of components on this page
+ * @param theme
+ * @return {object}
+ */
+const styles = theme => ({
+ fullpage: {
+ position: 'absolute',
+ top: 0,
+ bottom: 0,
+ left: 0,
+ right: 0,
+ overflow: 'hidden',
+ 'z-index': -1,
+ },
+});
+
+/**
+ * vis.js graph configuration setting
+ * @type {object}
+ */
+const options = {
+ groups: {
+ useDefaultGroups: true,
+ myGroupId: {
+ /*node options*/
+ },
+ },
+ layout: {
+ randomSeed: 666,
+ hierarchical: {
+ enabled: false,
+ sortMethod: 'hubsize',
+ },
+ improvedLayout: true,
+ },
+ edges: {
+ color: '#000000',
+ },
+ width: '100%',
+ height: '100%',
+ autoResize: true,
+ nodes: {
+ shape: 'box',
+ color: '#89C4F4',
+ shapeProperties: {
+ borderRadius: 0, // only for box shape
+ },
+ },
+ physics: {
+ solver: 'forceAtlas2Based',
+ adaptiveTimestep: true,
+ stabilization: {
+ enabled: true,
+ iterations: 1,
+ updateInterval: 100,
+ onlyDynamicEdges: false,
+ fit: true,
+ },
+ repulsion: {
+ nodeDistance: 250,
+ },
+ },
+ interaction: {
+ hover: true,
+ hoverConnectedEdges: false,
+ },
+};
+
+/**
+ * @param dept {string} the Department name
+ * @return {object}
+ */
+const generateGroupObject = (dept) => {
+ // gen random color
+
+ return {
+ color: {background: 'red'},
+ };
+};
+
+/**
+ * Takes the raw data received from the server, 'parser' it to add additional
+ * properties to the nodes.
+ * @param rawData
+ * @return {{data: object, departments: Set}}
+ */
+function parseGraphData(rawData) {
+ let graph = Object.assign({}, rawData);
+ let depts = new Set();
+
+ graph.nodes.forEach((node) => {
+ node.group = node.dept;
+ depts.add(node.dept);
+ });
+
+ return {graph, depts};
+}
+
+/**
+ * Wrapper to the Vis.js Graph. Handle additional Events.
+ */
+class GraphView extends Component {
+ static propTypes = {
+ classes: PropTypes.object.isRequired,
+ data: PropTypes.object.isRequired,
+ events: PropTypes.shape({
+ select: PropTypes.func.isRequired,
+ hoverNode: PropTypes.func.isRequired,
+ blurNode: PropTypes.func.isRequired,
+ }).isRequired,
+ };
+
+ state = {
+ toolOpen: false,
+ toolNode: '',
+ popOpen: false,
+ popNode: '',
+ };
+
+ render() {
+ const {classes, data, events} = this.props;
+
+ // Modify the graphData before passing to child component.
+ const {graph, depts} = parseGraphData(data);
+
+ // Must inject the groups data into the options.
+ for (const department of depts) {
+ options.groups.myGroupId[department] = generateGroupObject(department);
+ }
+
+ return (
+
+
+
+ );
+ }
+}
+
+export default withStyles(styles)(GraphView);
diff --git a/components/graph/GraphViewAssembly.jsx b/components/graph/GraphViewAssembly.jsx
new file mode 100644
index 0000000..861c73b
--- /dev/null
+++ b/components/graph/GraphViewAssembly.jsx
@@ -0,0 +1,154 @@
+import React, { Component } from 'react';
+import PropTypes from 'prop-types';
+
+import filteredGraph from '../utils/filterAlgortithm';
+import GraphView from './GraphView';
+import SearchbarDrawer from '../searchbar/SearchbarDrawer';
+import CourseInfoCard from './CourseInfoCard';
+
+/**
+ * GraphViewAssembly renders both drawers and the graph view. Takes a prop
+ * data. Which is the graph data of a school.
+ */
+class GraphViewAssembly extends Component {
+ static propTypes = {
+ data: PropTypes.object.isRequired,
+ };
+
+ state = {
+ graphData: {'nodes': [], 'edges': []},
+ selectedIDs: [],
+ selectedNode: null, // the current node object on graph
+ };
+
+ /**
+ * @param selection
+ */
+ updateSelected(selection) {
+ const {data} = this.props;
+
+ let graph = filteredGraph(data.nodes, selection);
+
+ // Update colors
+ // selection.forEach((selId) => {
+ // let needNewColorIndex = graph.nodes.findIndex((i) => i.id === selId);
+ // graph.nodes[needNewColorIndex].color = '#e04141';
+ // });
+
+ this.setState({
+ graphData: graph,
+ selectedIDs: selection,
+ });
+ }
+
+ /**
+ * @param nodeId {string} ? number I forgot
+ */
+ selectNode(nodeId) {
+ const {selectedIDs} = this.state;
+
+ // Skip if id is already selected
+ if (selectedIDs.includes(nodeId)) {
+ return;
+ }
+
+ // Add to selection + update graph
+ let selected = selectedIDs.slice();
+ selected.push(nodeId);
+ this.updateSelected(selected);
+ }
+
+ /**
+ * @param nodeId {string} ? number I forgot
+ */
+ deselectNode(nodeId) {
+ const {selectedIDs} = this.state;
+
+ const index = selectedIDs.findIndex((element) => element === nodeId);
+ let selected = selectedIDs.slice();
+ selected.splice(index, 1);
+ this.updateSelected(selected);
+ }
+
+ /**
+ * WTF ????
+ * @param nodeId
+ * @param event
+ */
+ handleItemClick(nodeId, event) {
+ this.selectNode(nodeId);
+ }
+
+ /**
+ * WTF ????
+ * @param nodeId
+ * @param event
+ */
+ handleSelectedClick(nodeId, event) {
+ this.deselectNode(nodeId);
+ }
+
+ /**
+ * Passed to child component
+ * @type {
+ * {select: GraphView.events.select,
+ * hoverNode: GraphView.events.hoverNode,
+ * blurNode: GraphView.events.blurNode}
+ * }
+ */
+ events = {
+ select: (event) => {
+ let {nodes} = event;
+
+ this.setState({
+ selectedNode: nodes.length > 0 ? this.getNode(nodes[0]) : null,
+ });
+ },
+ hoverNode: (event) => {
+ let {nodes} = event;
+ },
+ blurNode: () => {
+ let {nodes} = event;
+ },
+ };
+
+ /**
+ * @param id
+ * @returns {object}
+ */
+ getNode(id) {
+ let arr = this.props.data.nodes;
+ let result = null;
+
+ arr.forEach((node) => {
+ if (node.id == id) { // must use '==' instead of '==='
+ result = node;
+ }
+ });
+
+ return result;
+ }
+
+ render() {
+ const {selectedNode} = this.state;
+
+ return (
+
+ this.handleItemClick(event, id)}
+ selClick={(event, sel) => this.handleSelectedClick(event, sel)}
+ selected={this.state.selectedIDs}
+ />
+
+ {selectedNode &&
+ }
+
+ );
+ }
+}
+
+export default GraphViewAssembly;
diff --git a/components/graph/GraphViewLoader.jsx b/components/graph/GraphViewLoader.jsx
new file mode 100644
index 0000000..f76cf78
--- /dev/null
+++ b/components/graph/GraphViewLoader.jsx
@@ -0,0 +1,83 @@
+import React from 'react';
+import GraphViewAssembly from './GraphViewAssembly';
+
+class LoadingMessage extends React.Component {
+ render() {
+ return (
+
+ );
+ }
+}
+class LoadErrorMessage extends React.Component {
+ render() {
+ return (
+
+ );
+ }
+}
+
+export default class GraphViewLoader extends React.Component {
+ constructor(props) {
+ super(props);
+ this.state = {
+ data: null,
+ dataLoadError: null,
+ };
+ }
+
+ setData(data) {
+ this.setState({
+ data: {
+ nodes: data.nodes,
+ edges: data.edges,
+ },
+ });
+ }
+
+ setDataError(error) {
+ this.setState({
+ data: null,
+ dataLoadError: error,
+ });
+ // alert(error);
+ }
+
+ componentDidMount() {
+ console.log('Fetching data...');
+ fetch(this.props.jsonDataUrl)
+ .then((response) => {
+ if (!response.ok) {
+ throw Error(response.statusText);
+ }
+ console.log('Got response');
+ return response.json();
+ })
+ .then((data) => {
+ console.log('Got json data');
+ this.setData(data);
+ })
+ .catch((error) => {
+ console.log('Got error: ' + error);
+ this.setDataError(error);
+ });
+ }
+
+ render() {
+ if (this.state.dataLoadError) {
+ return ;
+ }
+ if (!this.state.data) {
+ return ;
+ }
+ return ;
+ }
+}
diff --git a/components/home/HomePanel.jsx b/components/home/HomePanel.jsx
new file mode 100644
index 0000000..84da8f5
--- /dev/null
+++ b/components/home/HomePanel.jsx
@@ -0,0 +1,83 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import Router from 'next/router';
+
+import { withStyles } from '@material-ui/core/styles';
+import Button from '@material-ui/core/Button';
+import Card from '@material-ui/core/Card';
+import CardActions from '@material-ui/core/CardActions';
+import CardContent from '@material-ui/core/CardContent';
+import Typography from '@material-ui/core/Typography';
+
+/**
+ * Define the style of components on this page
+ * @param theme
+ * @return {object}
+ */
+const styles = theme => ({
+ panel: {
+ 'maxWidth': 350,
+ 'position': 'absolute',
+ 'margin-left': 'auto',
+ 'margin-right': 'auto',
+ 'left': 0,
+ 'right': 0,
+ 'z-index': 100,
+ 'top': '50%',
+ 'transform': `translateY(-${50}%)`,
+ },
+ button: {
+ 'margin': 'auto',
+ },
+});
+
+/**
+ * @param href {string}
+ * @return {Function}
+ */
+const onClickHandler = (href) => (e) => {
+ e.preventDefault();
+ Router.push(href);
+};
+
+/**
+ * @param classes
+ * @return {Element}
+ * @constructor
+ */
+const HomePanel = ({classes}) => {
+ return (
+
+
+
+ Course Graph
+
+
+ v2.0.0
+
+
+ 🏅 A dynamic, browser based visualization course planner.
+ Designed to help students with course planning.
+
+
+
+
+
+
+
+ );
+};
+
+HomePanel.propTypes = {
+ classes: PropTypes.object,
+};
+
+export default withStyles(styles)(HomePanel);
diff --git a/components/home/HomePanel.test.jsx b/components/home/HomePanel.test.jsx
new file mode 100644
index 0000000..034bce2
--- /dev/null
+++ b/components/home/HomePanel.test.jsx
@@ -0,0 +1,11 @@
+import React from 'react';
+import { shallow } from 'enzyme';
+
+import HomePanel from './HomePanel';
+
+describe('A HomePanel', () => {
+ it('should render default header without throwing an error', () => {
+ const wrapper = shallow();
+ expect(wrapper.text()).toEqual('');
+ });
+});
diff --git a/components/login/Login.jsx b/components/login/Login.jsx
new file mode 100644
index 0000000..e5dfc6c
--- /dev/null
+++ b/components/login/Login.jsx
@@ -0,0 +1,100 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { withStyles } from '@material-ui/core/styles';
+
+import TextField from '@material-ui/core/TextField';
+import Button from '@material-ui/core/Button';
+
+import fetch from 'isomorphic-unfetch';
+
+const styles = theme => ({
+ container: {
+ display: 'flex',
+ flexWrap: 'wrap',
+ },
+ card: {
+ minWidth: 275,
+ },
+ button: {
+ margin: theme.spacing.unit * 3,
+ },
+});
+
+/**
+ * Login Component that provide text fields and submit button.
+ * @inheritDoc
+ */
+class Login extends React.Component {
+ static propTypes = {
+ // classes: PropTypes.object.isRequired,
+ };
+
+ state = {
+ email: 'email',
+ password: '',
+ };
+
+ // Helpers
+ handleChange = (email, password) => (event) => {
+ this.setState({
+ [email]: event.target.value,
+ [password]: event.target.value,
+ });
+ };
+
+ handleSubmit = async (event) => {
+ event.preventDefault();
+
+ let data = {
+ email: this.state.email,
+ password: this.state.password,
+ };
+
+ // https://coursegraph.org/account/login
+ await fetch('http://localhost:8080/account/login', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify(data),
+ });
+ };
+
+ /**
+ * @return {Element}
+ */
+ render() {
+ return (
+
+
);
+ }
+}
+
+export default withStyles(styles)(Login);
diff --git a/components/searchbar/FloatingActionButton.jsx b/components/searchbar/FloatingActionButton.jsx
new file mode 100644
index 0000000..c00aa54
--- /dev/null
+++ b/components/searchbar/FloatingActionButton.jsx
@@ -0,0 +1,56 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+import { withStyles } from '@material-ui/core/styles';
+import Button from '@material-ui/core/Button';
+import SearchIcon from '@material-ui/icons/Search';
+
+/**
+ * Material UI theme styles. This Button can float above other components.
+ * @param theme
+ * @return {{
+ * extendedIcon: {marginRight: number},
+ * absolute: {position: string, top: number, left: number}
+ * }}
+ */
+const styles = theme => ({
+ extendedIcon: {
+ 'marginRight': theme.spacing.unit,
+ },
+ absolute: {
+ 'position': 'absolute',
+ 'top': theme.spacing.unit * 4,
+ 'left': theme.spacing.unit * 6,
+ },
+});
+
+/**
+ * @param props
+ * @return {Element}
+ * @constructor
+ */
+class FloatingActionButtons extends React.Component {
+ static propTypes = {
+ classes: PropTypes.object.isRequired,
+ buttonClick: PropTypes.func.isRequired,
+ };
+
+ render() {
+ const {classes} = this.props;
+
+ return (
+
+
+
+ );
+ }
+}
+
+export default withStyles(styles)(FloatingActionButtons);
diff --git a/components/searchbar/SearchBar.jsx b/components/searchbar/SearchBar.jsx
new file mode 100644
index 0000000..f3abafb
--- /dev/null
+++ b/components/searchbar/SearchBar.jsx
@@ -0,0 +1,50 @@
+import React, { Component } from 'react';
+import PropTypes from 'prop-types';
+import { withStyles } from '@material-ui/core/styles';
+
+import TextField from '@material-ui/core/TextField';
+
+/**
+ * Define the style of components on this page
+ * @param theme
+ * @return {object}
+ */
+const styles = theme => ({
+ textField: {
+ marginLeft: theme.spacing.unit,
+ marginRight: theme.spacing.unit,
+ width: '90%',
+ },
+});
+
+/**
+ * Simple Input interface that prompts input from user.
+ */
+class SearchBar extends Component {
+ static propTypes = {
+ classes: PropTypes.object.isRequired,
+ };
+
+ state = {
+ value: '',
+ };
+
+ onChange = (event) => {
+ this.setState({value: event.target.value});
+ this.props.onChange(event.target.value);
+ };
+
+ render() {
+ const {classes} = this.props;
+
+ return (
+ );
+ }
+}
+
+export default withStyles(styles)(SearchBar);
diff --git a/components/searchbar/SearchResultList.jsx b/components/searchbar/SearchResultList.jsx
new file mode 100644
index 0000000..27cf08f
--- /dev/null
+++ b/components/searchbar/SearchResultList.jsx
@@ -0,0 +1,71 @@
+import React, { Component } from 'react';
+import PropTypes from 'prop-types';
+import { withStyles } from '@material-ui/core/styles';
+
+import List from '@material-ui/core/List';
+import ListItem from '@material-ui/core/ListItem';
+import ListItemText from '@material-ui/core/ListItemText';
+
+/**
+ * Define the style of components on this page
+ * @param theme
+ * @return {object}
+ */
+const styles = theme => ({
+ listdiv: {
+ overflow: 'auto',
+ maxHeight: '400px',
+ maxWidth: '300px',
+ },
+});
+
+/**
+ * List container that display the search result
+ */
+class SearchResultList extends Component {
+ static propTypes = {
+ classes: PropTypes.object.isRequired,
+ courses: PropTypes.array.isRequired,
+ };
+
+ state = {
+ visibleElements: 40,
+ };
+
+ // This is here because it needs to get scrollHeight and scrollTop of a div,
+ // taken any higher this is not possible.
+ onListScroll = (event) => {
+ const el = document.getElementById('listDiv'); // WTF????????
+
+ if ((el.scrollHeight - el.scrollTop) < 810) {
+ let newVisibleElements = this.state.visibleElements + 40;
+ this.setState({
+ visibleElements: newVisibleElements,
+ });
+ }
+ };
+
+ render() {
+ const {classes} = this.props;
+
+ let courses = this.props.courses.slice(0, this.state.visibleElements);
+
+ return (
+
+ {courses.map((course) => (
+
+
+
+ ))}
+
+ );
+ }
+}
+
+export default withStyles(styles)(SearchResultList);
diff --git a/components/searchbar/SearchbarAssembly.jsx b/components/searchbar/SearchbarAssembly.jsx
new file mode 100644
index 0000000..5234a22
--- /dev/null
+++ b/components/searchbar/SearchbarAssembly.jsx
@@ -0,0 +1,65 @@
+import React, { Component } from 'react';
+import PropTypes from 'prop-types';
+
+import { withStyles } from '@material-ui/core/styles';
+import Divider from '@material-ui/core/Divider';
+
+import { levenshtein, match } from '../utils/levenshtien';
+import SearchResultList from './SearchResultList';
+import SearchBar from './SearchBar';
+
+const styles = theme => ({
+ container: {
+ 'max-width': 300,
+ },
+});
+
+
+class SearchbarAssembly extends Component {
+ static propTypes = {
+ classes: PropTypes.object.isRequired,
+ courses: PropTypes.array.isRequired,
+ };
+
+ state = {
+ searchQuery: '',
+ };
+
+ tempArrayA = [];
+ tempArrayB = [];
+
+ updateSearch(search) {
+ this.setState({
+ searchQuery: search,
+ });
+ }
+
+ render() {
+ const {classes, courses} = this.props;
+
+ const searchQuery = this.state.searchQuery;
+ let filteredData = courses.filter(match(searchQuery));
+
+ filteredData.forEach((course) => {
+ course.priority = levenshtein(
+ searchQuery.toLowerCase(),
+ course.searchString.toLowerCase(),
+ this.tempArrayA,
+ this.tempArrayB
+ );
+ });
+
+ filteredData.sort((a, b) => a.priority > b.priority);
+
+ return (
+
+
this.updateSearch(search)}/>
+
+ this.props.itemClick(event, id)}/>
+
+ );
+ }
+}
+
+export default withStyles(styles)(SearchbarAssembly);
diff --git a/components/searchbar/SearchbarDrawer.jsx b/components/searchbar/SearchbarDrawer.jsx
new file mode 100644
index 0000000..e42dd52
--- /dev/null
+++ b/components/searchbar/SearchbarDrawer.jsx
@@ -0,0 +1,55 @@
+import React, { Component } from 'react';
+import PropTypes from 'prop-types';
+
+import Drawer from '@material-ui/core/Drawer';
+import Divider from '@material-ui/core/Divider';
+
+import SearchbarAssembly from '../searchbar/SearchbarAssembly';
+import SelectedList from '../searchbar/SelectedList';
+import FloatingActionButton from '../searchbar/FloatingActionButton';
+
+/**
+ * Component that you can trigger a button to open the drawer.
+ */
+class SearchbarDrawer extends Component {
+ static propTypes = {
+ classes: PropTypes.object,
+ courses: PropTypes.array.isRequired,
+ selected: PropTypes.array.isRequired,
+ itemClick: PropTypes.func.isRequired,
+ selClick: PropTypes.func.isRequired,
+ };
+
+ state = {
+ isOpen: false,
+ };
+
+ toggleDrawer = (open) => () => {
+ this.setState({
+ isOpen: open,
+ });
+ };
+
+ render() {
+ const {courses, selected, selClick, itemClick} = this.props;
+
+ return (
+
+
+
+ itemClick(event, id)}/>
+
+ selClick(event, sel)}/>
+
+
+ );
+ }
+}
+
+export default SearchbarDrawer;
diff --git a/components/searchbar/SelectedList.jsx b/components/searchbar/SelectedList.jsx
new file mode 100644
index 0000000..3461d84
--- /dev/null
+++ b/components/searchbar/SelectedList.jsx
@@ -0,0 +1,68 @@
+import React, { Component } from 'react';
+import PropTypes from 'prop-types';
+
+import Paper from '@material-ui/core/Paper';
+import Chip from '@material-ui/core/Chip';
+import { withStyles } from '@material-ui/core/styles/index';
+
+/**
+ * Define the style of components on this page
+ * @param theme
+ * @return {object}
+ */
+const styles = theme => ({
+ select: {
+ maxWidth: '300px',
+ overflow: 'auto',
+ maxHeight: '300px',
+ },
+ text: {
+ fontSize: 10,
+ },
+});
+
+/**
+ * Generate a list of Elements
+ * @param classes
+ * @param courses
+ * @param selClick
+ * @param selected
+ * @return {Element}
+ */
+const createChips = (classes, courses, selClick, selected) =>
+ selected.map((course) => (
+
+ ));
+
+/**
+ * An Chip container that user can select and deselect nodes on the graph
+ */
+class SelectedList extends Component {
+ static propTypes = {
+ classes: PropTypes.object,
+ courses: PropTypes.array.isRequired,
+ selClick: PropTypes.func.isRequired,
+ selected: PropTypes.array.isRequired,
+ };
+
+ render() {
+ const {classes, courses, selClick, selected} = this.props;
+
+ return (
+
+
+ {createChips(classes, courses, selClick, selected)}
+
+
+ );
+ }
+}
+
+export default withStyles(styles)(SelectedList);
diff --git a/components/utils/GraphSelection.js b/components/utils/GraphSelection.js
new file mode 100644
index 0000000..e54b0f2
--- /dev/null
+++ b/components/utils/GraphSelection.js
@@ -0,0 +1,257 @@
+//Graph Selection class
+
+class GraphSelection {
+ constructor(nodes, ids) {
+ this.nodeMap = new Map();
+ this.edgeMap = new Map();
+ //Leaves them as empty maps if not args provided
+ if (typeof (nodes) === 'object' && typeof (ids) !== 'undefined') {
+ buildGraph(nodes, ids, this.nodeMap, this.edgeMap);
+ }
+ }
+
+ addNodes(nodes, ids) {
+ buildGraph(nodes, ids, this.nodeMap, this.edgeMap);
+ }
+
+ removeNodes(nodes, ids) {
+ cleanGraph(nodes, ids, this.nodeMap, this.edgeMap);
+ }
+
+ clear() {
+ this.nodeMap.clear();
+ this.edgeMap.clear();
+ }
+
+ getGraphData() {
+ const graphEdges = this.edgeMap.values();
+ const graphNodes = this.nodeMap.values();
+ const graphData = {
+ edges: Array.from(graphEdges),
+ nodes: Array.from(graphNodes),
+ };
+ return graphData;
+ }
+
+
+
+}
+
+
+
+function addFromEdges(nodes, id, nodeMap, edgeMap) {
+ const edgesFrom = nodes[id].edges_from;
+
+ edgesFrom.forEach((fromID, index) => {
+ //Give every edge a unique key
+ edgeMap.set(
+ `${fromID}_${id}`,
+ {'from': fromID, 'to': id}
+ );
+ if (!nodeMap.has(fromID)) {
+ nodeMap.set(fromID, nodes[fromID]);
+ addFromEdges(nodes, fromID, nodeMap, edgeMap);
+ }
+ });
+}
+
+function buildGraph(nodes, ids, nodeMap, edgeMap) {
+
+ if (typeof (ids) === 'number') {
+ if (!nodeMap.has(ids)) {
+ nodeMap.set(ids, nodes[ids]);
+ addFromEdges(nodes, ids, nodeMap, edgeMap);
+ }
+ } else {
+ ids.forEach((id) => {
+ if (!nodeMap.has(id)) {
+ nodeMap.set(id, nodes[ids]);
+ addFromEdges(nodes, id, nodeMap, edgeMap);
+ }
+ });
+ }
+ console.log(edgeMap);
+}
+
+function cleanFromEdges(nodes, id, nodeMap, edgeMap) {
+ const edgesFrom = nodes[id].edges_from;
+ const edgesTo = nodes[id].edges_to;
+ nodeMap.delete(id);
+ //console.log(`recieved edges from: ${edgesFrom}`);
+
+ //Delete edges by their unique key
+ edgesTo.forEach((toID, index) => {
+ console.log(`removing key: ${index}_${id}`);
+ edgeMap.delete(`${index}_${id}`);
+ });
+ edgesFrom.forEach((fromID, index) => {
+ console.log(`removing key: ${id}_${index}`);
+ edgeMap.delete(`${fromID}_${id}`);
+ if (nodeMap.delete(fromID)) {
+ cleanFromEdges(nodes, fromID, nodeMap, edgeMap);
+ }
+ });
+
+}
+
+function cleanGraph(nodes, ids, nodeMap, edgeMap) {
+ console.log('in clean');
+ if (typeof (ids) === 'number') {
+ if (nodeMap.has(ids)) {
+ console.log('number clean');
+ cleanFromEdges(nodes, ids, nodeMap, edgeMap);
+ }
+ } else {
+ console.log('somehow here');
+ ids.forEach( (id) => {
+ if (nodeMap.has(id)) {
+ cleanFromEdges(nodes, id, nodeMap, edgeMap);
+ }
+ });
+ }
+}
+//export default GraphSelection;
+
+const nodes = [
+ {
+ 'dept': 'life',
+ 'description': 'being able to make good use of college',
+ 'edges_from': [
+ 4,
+ 5,
+ ],
+ 'edges_to': [
+ 1,
+ ],
+ 'id': 0,
+ 'label': 'CL 101',
+ 'title': 'How to College',
+ },
+ {
+ 'dept': 'life',
+ 'description': 'Doing more advanced college stuff',
+ 'edges_from': [
+ 0,
+ ],
+ 'edges_to': [
+ 2,
+ 3,
+ ],
+ 'id': 1,
+ 'label': 'CL 102',
+ 'title': 'Advanced college',
+ },
+ {
+ 'dept': 'life',
+ 'description': 'Welcome to the real world ya pansy!',
+ 'edges_from': [
+ 1,
+ ],
+ 'edges_to': [],
+ 'id': 2,
+ 'label': 'RL 7',
+ 'title': 'REAL LIFE',
+ },
+ {
+ 'dept': 'Life',
+ 'description': 'Your educated, now getting rich',
+ 'edges_from': [
+ 1,
+ ],
+ 'edges_to': [],
+ 'id': 3,
+ 'label': 'RL M2',
+ 'title': 'Getting Money',
+ },
+ {
+ 'dept': 'PD',
+ 'description': 'You are not that important, accept it',
+ 'edges_from': [
+ 8,
+ ],
+ 'edges_to': [
+ 0,
+ ],
+ 'id': 4,
+ 'label': 'PD 42',
+ 'title': 'Getting over yourself',
+ },
+ {
+ 'dept': 'HS',
+ 'description': 'A totally wonderful time',
+ 'edges_from': [
+ 6,
+ 7,
+ ],
+ 'edges_to': [
+ 0,
+ ],
+ 'id': 5,
+ 'label': 'HS K',
+ 'title': 'High School stuff',
+ },
+ {
+ 'dept': 'HS',
+ 'description': 'seeing how terrible people, especially kids, are',
+ 'edges_from': [
+ 8,
+ ],
+ 'edges_to': [
+ 5,
+ ],
+ 'id': 6,
+ 'label': 'HS 69',
+ 'title': 'Petty School Drama',
+ },
+ {
+ 'dept': 'PD',
+ 'description': 'the essential stage of life that is childhood',
+ 'edges_from': [],
+ 'edges_to': [
+ 5,
+ ],
+ 'id': 7,
+ 'label': 'PD 3',
+ 'title': 'Kid stuff',
+ },
+ {
+ 'dept': 'PD',
+ 'description' : 'we have all been *that* person before',
+ 'edges_from' : [],
+ 'edges_to' : [
+ 4,
+ 6,
+ ],
+ 'id' : 8,
+ 'label' : 'PD FU',
+ 'title' : 'Being a snot nosed brat',
+ },
+ {
+ 'dept': '=/',
+ 'description': 'question mark',
+ 'edges_from': [],
+ 'edges_to': [],
+ 'id': 9,
+ 'label': 'whatevs',
+ 'title': 'whatevs',
+ },
+];
+
+function outPut(graph) {
+ const data = graph.getGraphData();
+ console.log('NEW OUTPUT SEGMENT STARTS HERE!');
+ data.nodes.forEach( (x) => {
+ console.log(x.id);
+ });
+ console.log(data.edges);
+}
+
+let myGraph = new GraphSelection(nodes, 0);
+outPut(myGraph);
+
+//myGraph.addNodes(nodes, 3);
+//outPut(myGraph);
+
+myGraph.removeNodes(nodes, 5);
+myGraph.clear();
+outPut(myGraph);
diff --git a/components/utils/filterAlgortithm.js b/components/utils/filterAlgortithm.js
new file mode 100644
index 0000000..c1172dc
--- /dev/null
+++ b/components/utils/filterAlgortithm.js
@@ -0,0 +1,191 @@
+const nodes = [
+ {
+ 'dept': 'life',
+ 'description': 'being able to make good use of college',
+ 'edges_from': [
+ 4,
+ 5,
+ ],
+ 'edges_to': [
+ 1,
+ ],
+ 'id': 0,
+ 'label': 'CL 101',
+ 'title': 'How to College',
+ },
+ {
+ 'dept': 'life',
+ 'description': 'Doing more advanced college stuff',
+ 'edges_from': [
+ 0,
+ ],
+ 'edges_to': [
+ 2,
+ 3,
+ ],
+ 'id': 1,
+ 'label': 'CL 102',
+ 'title': 'Advanced college',
+ },
+ {
+ 'dept': 'life',
+ 'description': 'Welcome to the real world ya pansy!',
+ 'edges_from': [
+ 1,
+ ],
+ 'edges_to': [],
+ 'id': 2,
+ 'label': 'RL 7',
+ 'title': 'REAL LIFE',
+ },
+ {
+ 'dept': 'Life',
+ 'description': 'Your educated, now getting rich',
+ 'edges_from': [
+ 1,
+ ],
+ 'edges_to': [],
+ 'id': 3,
+ 'label': 'RL M2',
+ 'title': 'Getting Money',
+ },
+ {
+ 'dept': 'PD',
+ 'description': 'You are not that important, accept it',
+ 'edges_from': [
+ 8,
+ ],
+ 'edges_to': [
+ 0,
+ ],
+ 'id': 4,
+ 'label': 'PD 42',
+ 'title': 'Getting over yourself',
+ },
+ {
+ 'dept': 'HS',
+ 'description': 'A totally wonderful time',
+ 'edges_from': [
+ 6,
+ 7,
+ ],
+ 'edges_to': [
+ 0,
+ ],
+ 'id': 5,
+ 'label': 'HS K',
+ 'title': 'High School stuff',
+ },
+ {
+ 'dept': 'HS',
+ 'description': 'seeing how terrible people, especially kids, are',
+ 'edges_from': [
+ 8,
+ ],
+ 'edges_to': [
+ 5,
+ ],
+ 'id': 6,
+ 'label': 'HS 69',
+ 'title': 'Petty School Drama',
+ },
+ {
+ 'dept': 'PD',
+ 'description': 'the essential stage of life that is childhood',
+ 'edges_from': [],
+ 'edges_to': [
+ 5,
+ ],
+ 'id': 7,
+ 'label': 'PD 3',
+ 'title': 'Kid stuff',
+ },
+ {
+ 'dept': 'PD',
+ 'description' : 'we have all been *that* person before',
+ 'edges_from' : [],
+ 'edges_to' : [
+ 4,
+ 6,
+ ],
+ 'id' : 8,
+ 'label' : 'PD FU',
+ 'title' : 'Being a snot nosed brat',
+ },
+ {
+ 'dept': '=/',
+ 'description': 'question mark',
+ 'edges_from': [],
+ 'edges_to': [],
+ 'id': 9,
+ 'label': 'whatevs',
+ 'title': 'whatevs',
+ },
+];
+
+function doFromEdges(nodes, id, newNodes, newEdges) {
+ const edgesFrom = nodes[id].edges_from;
+
+ edgesFrom.forEach((fromID) => {
+ newEdges.push({
+ 'from': fromID,
+ 'to': id,
+ });
+ if (!newNodes.has(fromID)) {
+ newNodes.set(fromID, false);
+ doFromEdges(nodes, fromID, newNodes, newEdges);
+ }
+ });
+}
+
+/**
+ * @param nodes Array.