removed old components

master
Tudor Stanciu 2020-05-13 13:57:07 +03:00
parent 828ab3883a
commit 28ab66ac09
15 changed files with 7 additions and 504 deletions

View File

@ -1,6 +0,0 @@
import { get } from "./api";
const baseUrl = process.env.API_URL + "/authors/";
export function getAuthors() {
return get(baseUrl);
}

View File

@ -1,15 +0,0 @@
import { get, del, post, put } from "./api";
const baseUrl = process.env.API_URL + "/courses/";
export function getCourses() {
return get(baseUrl);
}
export function saveCourse(course) {
const url = baseUrl + (course.id || "");
return course.id ? put(url, course) : post(url, course); // POST for create, PUT to update when id already exists.
}
export function deleteCourse(courseId) {
return del(baseUrl + courseId);
}

View File

@ -4,8 +4,6 @@ import HomePage from "./home/HomePage";
import AboutPage from "./about/AboutPage"; import AboutPage from "./about/AboutPage";
import Header from "./common/Header"; import Header from "./common/Header";
import PageNotFound from "./PageNotFound"; import PageNotFound from "./PageNotFound";
import CoursesPage from "./courses/CoursesPage";
import ManageCoursePage from "./courses/ManageCoursePage";
import { ToastContainer } from "react-toastify"; import { ToastContainer } from "react-toastify";
import "react-toastify/dist/ReactToastify.css"; import "react-toastify/dist/ReactToastify.css";
import SessionContainer from "../features/session/components/SessionContainer"; import SessionContainer from "../features/session/components/SessionContainer";
@ -17,9 +15,6 @@ function App() {
<Switch> <Switch>
<Route exact path="/" component={HomePage} /> <Route exact path="/" component={HomePage} />
<Route path="/about" component={AboutPage} /> <Route path="/about" component={AboutPage} />
<Route path="/courses" component={CoursesPage} />
<Route path="/course/:slug" component={ManageCoursePage} />
<Route path="/course" component={ManageCoursePage} />
<Route path="/sessions" component={SessionContainer} /> <Route path="/sessions" component={SessionContainer} />
<Route component={PageNotFound} /> <Route component={PageNotFound} />
</Switch> </Switch>

View File

@ -1,67 +0,0 @@
import React from "react";
import PropTypes from "prop-types";
import TextInput from "../common/TextInput";
import SelectInput from "../common/SelectInput";
const CourseForm = ({
course,
authors,
onSave,
onChange,
saving = false,
errors = {}
}) => {
return (
<form onSubmit={onSave}>
<h2>{course.id ? "Edit" : "Add"} Course</h2>
{errors.onSave && (
<div className="alert alert-danger" role="alert">
{errors.onSave}
</div>
)}
<TextInput
name="title"
label="Title"
value={course.title}
onChange={onChange}
error={errors.title}
/>
<SelectInput
name="authorId"
label="Author"
value={course.authorId || ""}
defaultOption="Select Author"
options={authors.map((author) => ({
value: author.id,
text: author.name
}))}
onChange={onChange}
error={errors.author}
/>
<TextInput
name="category"
label="Category"
value={course.category}
onChange={onChange}
error={errors.category}
/>
<button type="submit" disabled={saving} className="btn btn-primary">
{saving ? "Saving..." : "Save"}
</button>
</form>
);
};
CourseForm.propTypes = {
authors: PropTypes.array.isRequired,
course: PropTypes.object.isRequired,
errors: PropTypes.object,
onSave: PropTypes.func.isRequired,
onChange: PropTypes.func.isRequired,
saving: PropTypes.bool
};
export default CourseForm;

View File

@ -1,53 +0,0 @@
import React from "react";
import PropTypes from "prop-types";
import { Link } from "react-router-dom";
const CourseList = ({ courses, onDeleteClick }) => (
<table className="table">
<thead>
<tr>
<th />
<th>Title</th>
<th>Author</th>
<th>Category</th>
<th />
</tr>
</thead>
<tbody>
{courses.map((course) => {
return (
<tr key={course.id}>
<td>
<a
className="btn btn-light"
href={"http://pluralsight.com/courses/" + course.slug}
>
Watch
</a>
</td>
<td>
<Link to={"/course/" + course.slug}>{course.title}</Link>
</td>
<td>{course.authorName}</td>
<td>{course.category}</td>
<td>
<button
className="btn btn-outline-danger"
onClick={() => onDeleteClick(course)}
>
Delete
</button>
</td>
</tr>
);
})}
</tbody>
</table>
);
CourseList.propTypes = {
courses: PropTypes.array.isRequired,
onDeleteClick: PropTypes.func.isRequired
};
export default CourseList;

View File

@ -1,107 +0,0 @@
import React from "react";
import { connect } from "react-redux";
import * as courseActions from "../../redux/actions/courseActions";
import * as authorActions from "../../redux/actions/authorActions";
import PropTypes from "prop-types";
import { bindActionCreators } from "redux";
import CourseList from "./CourseList";
import { Redirect } from "react-router-dom";
import Spinner from "../common/Spinner";
import { toast } from "react-toastify";
class CoursesPage extends React.Component {
state = {
redirectToAddCoursePage: false
};
componentDidMount() {
const { courses, authors, actions } = this.props;
if (courses.length === 0) {
actions.loadCourses().catch((error) => {
alert("Loading courses failed. " + error);
});
}
if (authors.length === 0) {
actions.loadAuthors().catch((error) => {
alert("Loading authors failed. " + error);
});
}
}
handleDeleteCourse = async (course) => {
toast.success("Course deleted");
try {
await this.props.actions.deleteCourse(course);
} catch (error) {
toast.error("Delete failed. " + error.message, { autoClose: false });
}
};
render() {
return (
<>
{this.state.redirectToAddCoursePage && <Redirect to="/course" />}
<h2>Courses</h2>
{this.props.loading ? (
<Spinner />
) : (
<>
<button
style={{ marginBottom: 20 }}
className="btn btn-primary add-course"
onClick={() => this.setState({ redirectToAddCoursePage: true })}
>
Add Course
</button>
<CourseList
courses={this.props.courses}
onDeleteClick={this.handleDeleteCourse}
/>
</>
)}
</>
);
}
}
CoursesPage.propTypes = {
courses: PropTypes.array.isRequired,
authors: PropTypes.array.isRequired,
actions: PropTypes.object.isRequired,
loading: PropTypes.bool.isRequired
};
function mapStateToProps(state) {
return {
courses:
state.authors.length === 0
? []
: state.courses.map((course) => {
const author = state.authors.find((a) => a.id === course.authorId);
return {
...course,
authorName: author.name
};
}),
authors: state.authors,
loading: state.ajaxCallsInProgress > 0
};
}
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators(
{
loadCourses: courseActions.loadCourses,
loadAuthors: authorActions.loadAuthors,
deleteCourse: courseActions.deleteCourse
},
dispatch
)
};
}
export default connect(mapStateToProps, mapDispatchToProps)(CoursesPage);

View File

@ -1,122 +0,0 @@
import React, { useEffect, useState } from "react";
import { connect } from "react-redux";
import { loadCourses, saveCourse } from "../../redux/actions/courseActions";
import { loadAuthors } from "../../redux/actions/authorActions";
import PropTypes from "prop-types";
import { bindActionCreators } from "redux";
import CourseForm from "./CourseForm";
import { newCourse } from "../../../tools/mockData";
import Spinner from "../common/Spinner";
import { toast } from "react-toastify";
function ManageCoursePage({ courses, authors, actions, history, ...props }) {
const [course, setCourse] = useState({ ...props.course });
const [errors, setErrors] = useState({});
const [saving, setSaving] = useState(false);
useEffect(() => {
if (courses.length === 0) {
actions.loadCourses().catch((error) => {
alert("Loading courses failed. " + error);
});
} else {
setCourse({ ...props.course });
}
if (authors.length === 0) {
actions.loadAuthors().catch((error) => {
alert("Loading authors failed. " + error);
});
}
}, [props.course]);
function handleChange(event) {
const { name, value } = event.target;
setCourse((prevCourse) => ({
...prevCourse,
[name]: name === "authorId" ? parseInt(value, 10) : value
}));
}
function formIsValid() {
const { title, authorId, category } = course;
const errors = {};
if (!title) errors.title = "Title is required.";
if (!authorId) errors.author = "Author is required";
if (!category) errors.category = "Category is required";
setErrors(errors);
// Form is valid if the errors object still has no properties
return Object.keys(errors).length === 0;
}
function handleSave(event) {
event.preventDefault();
if (!formIsValid()) return;
setSaving(true);
actions
.saveCourse(course)
.then(() => {
toast.success("Course saved.");
history.push("/courses");
})
.catch((error) => {
setSaving(false);
setErrors({ onSave: error.message });
});
}
return authors.length === 0 || course.length === 0 ? (
<Spinner />
) : (
<CourseForm
course={course}
errors={errors}
authors={authors}
onChange={handleChange}
onSave={handleSave}
saving={saving}
/>
);
}
ManageCoursePage.propTypes = {
course: PropTypes.object.isRequired,
courses: PropTypes.array.isRequired,
authors: PropTypes.array.isRequired,
actions: PropTypes.object.isRequired,
history: PropTypes.object.isRequired
};
function mapStateToProps(state, ownProps) {
const slug = ownProps.match.params.slug;
const course =
slug && state.courses.length > 0
? getCourseBySlug(state.courses, slug)
: newCourse;
return {
course,
courses: state.courses,
authors: state.authors
};
}
function getCourseBySlug(courses, slug) {
return courses.find((course) => course.slug === slug) || null;
}
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators(
{
loadCourses,
loadAuthors,
saveCourse
},
dispatch
)
};
}
export default connect(mapStateToProps, mapDispatchToProps)(ManageCoursePage);

View File

@ -7,6 +7,7 @@ import Typography from "@material-ui/core/Typography";
import ExpandMoreIcon from "@material-ui/icons/ExpandMore"; import ExpandMoreIcon from "@material-ui/icons/ExpandMore";
import PropTypes from "prop-types"; import PropTypes from "prop-types";
import SessionSummary from "./SessionSummary"; import SessionSummary from "./SessionSummary";
import Spinner from "../../../components/common/Spinner";
const useStyles = makeStyles((theme) => ({ const useStyles = makeStyles((theme) => ({
root: { root: {
@ -24,7 +25,10 @@ const SessionListComponent = ({ sessions }) => {
return ( return (
<div className={classes.root}> <div className={classes.root}>
<h2>Sessions</h2> <h2>Sessions</h2>
{sessions.loaded && {sessions.loading ? (
<Spinner />
) : (
sessions.loaded &&
sessions.map((session) => { sessions.map((session) => {
return ( return (
<ExpansionPanel key={session.sessionId}> <ExpansionPanel key={session.sessionId}>
@ -44,7 +48,8 @@ const SessionListComponent = ({ sessions }) => {
</ExpansionPanelDetails> </ExpansionPanelDetails>
</ExpansionPanel> </ExpansionPanel>
); );
})} })
)}
</div> </div>
); );
}; };

View File

@ -1,16 +1,3 @@
export const CREATE_COURSE = "CREATE_COURSE";
export const LOAD_COURSES_SUCCESS = "LOAD_COURSES_SUCCESS";
export const LOAD_AUTHORS_SUCCESS = "LOAD_AUTHORS_SUCCESS";
export const CREATE_COURSE_SUCCESS = "CREATE_COURSE_SUCCESS";
export const UPDATE_COURSE_SUCCESS = "UPDATE_COURSE_SUCCESS";
// By convention, actions that end in "_SUCCESS" are assumed to have been the result of a completed
// API call. But since we're doing an optimistic delete, we're hiding loading state.
// So this action name deliberately omits the "_SUCCESS" suffix.
// If it had one, our apiCallsInProgress counter would be decremented below zero
// because we're not incrementing the number of apiCallInProgress when the delete request begins.
export const DELETE_COURSE_OPTIMISTIC = "DELETE_COURSE_OPTIMISTIC";
export const BEGIN_AJAX_CALL = "BEGIN_AJAX_CALL"; export const BEGIN_AJAX_CALL = "BEGIN_AJAX_CALL";
export const AJAX_CALL_ERROR = "AJAX_CALL_ERROR"; export const AJAX_CALL_ERROR = "AJAX_CALL_ERROR";
export const END_AJAX_CALL = "END_AJAX_CALL"; export const END_AJAX_CALL = "END_AJAX_CALL";

View File

@ -1,18 +0,0 @@
import * as types from "./actionTypes";
import * as authorApi from "../../api/authorApi";
import { sendHttpRequest } from "../actions/httpActions";
function loadAuthorsSuccess(authors) {
return { type: types.LOAD_AUTHORS_SUCCESS, authors };
}
export function loadAuthors() {
return async function (dispatch) {
try {
const authors = await dispatch(sendHttpRequest(authorApi.getAuthors()));
dispatch(loadAuthorsSuccess(authors));
} catch (error) {
throw error;
}
};
}

View File

@ -1,55 +0,0 @@
import * as types from "./actionTypes";
import * as courseApi from "../../api/courseApi";
import { sendHttpRequest } from "../actions/httpActions";
function loadCoursesSuccess(courses) {
return { type: types.LOAD_COURSES_SUCCESS, courses };
}
export function createCourseSuccess(course) {
return { type: types.CREATE_COURSE_SUCCESS, course };
}
export function updateCourseSuccess(course) {
return { type: types.UPDATE_COURSE_SUCCESS, course };
}
export function deleteCourseOptimistic(course) {
return { type: types.DELETE_COURSE_OPTIMISTIC, course };
}
export function loadCourses() {
return function (dispatch) {
return dispatch(sendHttpRequest(courseApi.getCourses()))
.then((courses) => {
dispatch(loadCoursesSuccess(courses));
})
.catch((error) => {
throw error;
});
};
}
export function saveCourse(course) {
//eslint-disable-next-line no-unused-vars
return function (dispatch, getState) {
return dispatch(sendHttpRequest(courseApi.saveCourse(course)))
.then((savedCourse) => {
course.id
? dispatch(updateCourseSuccess(savedCourse))
: dispatch(createCourseSuccess(savedCourse));
})
.catch((error) => {
throw error;
});
};
}
export function deleteCourse(course) {
return function (dispatch) {
// Doing optimistic delete, so not dispatching begin/end api call
// actions, or apiCallError action since we're not showing the loading status for this.
dispatch(deleteCourseOptimistic(course));
return courseApi.deleteCourse(course.id);
};
}

View File

@ -1,12 +0,0 @@
import * as types from "../actions/actionTypes";
import initialState from "./initialState";
export default function authorReducer(state = initialState.authors, action) {
switch (action.type) {
case types.LOAD_AUTHORS_SUCCESS:
return action.authors;
default:
return state;
}
}

View File

@ -1,23 +0,0 @@
import * as types from "../actions/actionTypes";
import initialState from "./initialState";
export default function courseReducer(state = initialState.courses, action) {
switch (action.type) {
case types.LOAD_COURSES_SUCCESS:
return action.courses;
case types.CREATE_COURSE_SUCCESS:
return [...state, { ...action.course }];
case types.UPDATE_COURSE_SUCCESS:
return state.map((course) =>
course.id === action.course.id ? action.course : course
);
case types.DELETE_COURSE_OPTIMISTIC:
return state.filter((course) => course.id !== action.course.id);
default:
return state;
}
}

View File

@ -1,6 +1,4 @@
import { combineReducers } from "redux"; import { combineReducers } from "redux";
import courseReducer from "./courseReducer";
import authorReducer from "./authorReducer";
import ajaxStatusReducer from "./ajaxStatusReducer"; import ajaxStatusReducer from "./ajaxStatusReducer";
import systemReducer from "../../features/system/reducer"; import systemReducer from "../../features/system/reducer";
import sessionsReducer from "../../features/session/reducer"; import sessionsReducer from "../../features/session/reducer";
@ -8,8 +6,6 @@ import sessionsReducer from "../../features/session/reducer";
const rootReducer = combineReducers({ const rootReducer = combineReducers({
system: systemReducer, system: systemReducer,
sessions: sessionsReducer, sessions: sessionsReducer,
courses: courseReducer,
authors: authorReducer,
ajaxCallsInProgress: ajaxStatusReducer ajaxCallsInProgress: ajaxStatusReducer
}); });

View File

@ -1,7 +1,5 @@
export default { export default {
system: {}, system: {},
sessions: Object.assign([], { loading: false, loaded: false }), sessions: Object.assign([], { loading: false, loaded: false }),
courses: [],
authors: [],
ajaxCallsInProgress: 0 ajaxCallsInProgress: 0
}; };