diff --git a/client/src/components/common/Button.js b/client/src/components/common/Button.js index cd2e5384..53ef599b 100644 --- a/client/src/components/common/Button.js +++ b/client/src/components/common/Button.js @@ -1,9 +1,15 @@ import React from "react"; import styled, { css } from "styled-components"; import PropTypes from "prop-types"; +import LoadingDots from "./animation/LoadingDots"; -const Button = ({ children, ...props }) => { - return {children}; +const Button = ({ children, loading, onClick, ...props }) => { + const clickHandler = loading ? undefined : onClick; + return ( + + {loading ? : children} + + ); }; Button.propTypes = { @@ -13,6 +19,8 @@ Button.propTypes = { secondary: PropTypes.bool, /* Makes the button width === 100% */ autoWidth: PropTypes.bool, + /* Replaces the button label with a loading indicator and prevents onClick */ + loading: PropTypes.bool, }; export default Button; @@ -22,8 +30,11 @@ const Btn = styled.button` border: none; border-radius: 4px; padding: 5px 12px; - font-size: 14px; + font-size: 16px; width: ${(props) => props.autoWidth && "100%"}; + display: flex; + justify-content: center; + align-items: center; // If secondary prop === true ${(props) => @@ -41,7 +52,7 @@ const Btn = styled.button` ${(props) => props.primary && css` - padding: 7px 12px; + padding: 9px 12px; border-radius: 3px; background-color: #4a86fa; color: #fff; diff --git a/client/src/components/common/Errors.js b/client/src/components/common/Errors.js new file mode 100644 index 00000000..dc761f7c --- /dev/null +++ b/client/src/components/common/Errors.js @@ -0,0 +1,27 @@ +import React from "react"; +import PropTypes from "prop-types"; + +const Errors = ({ errors, margin }) => { + console.log(errors); + if (!errors || errors.length < 1) { + return null; + } + return ( +
+ {errors.map((err, index) => ( +

+ {err} +

+ ))} +
+ ); +}; + +export default Errors; + +Errors.propTypes = { + /* Array of strings containing error messages */ + errors: PropTypes.arrayOf(PropTypes.string), + /* Top margin for error div */ + margin: PropTypes.number, +}; diff --git a/client/src/components/common/Input.js b/client/src/components/common/Input.js index f358e03b..958b1855 100644 --- a/client/src/components/common/Input.js +++ b/client/src/components/common/Input.js @@ -31,7 +31,7 @@ const StyledInput = styled.input` border: 1px solid #818181; background-color: #fff; border-radius: 4px; - font-size: 14px; + font-size: 16px; padding: 7px 9px; width: 100%; `; diff --git a/client/src/components/common/InputLabel.js b/client/src/components/common/InputLabel.js index c370796b..667159b8 100644 --- a/client/src/components/common/InputLabel.js +++ b/client/src/components/common/InputLabel.js @@ -8,7 +8,8 @@ const InputLabel = ({ children, margin }) => { export default InputLabel; -const Label = styled.h5` - font-size: 14px; +const Label = styled.h3` + font-size: 16px; + font-weight: 500; margin: ${(props) => props.margin || "13px 0 7px 0"}; `; diff --git a/client/src/components/common/Modal.js b/client/src/components/common/Modal.js index 71fc9c8a..682d962d 100644 --- a/client/src/components/common/Modal.js +++ b/client/src/components/common/Modal.js @@ -42,7 +42,7 @@ const Content = styled.div` position: relative; background-color: #fff; margin: 15% auto; - padding: 20px; + padding: 35px 20px; border-radius: 4px; box-shadow: 3px 3px 9px #48484830; width: ${(props) => props.width || "520px"}; diff --git a/client/src/components/common/SearchBar.js b/client/src/components/common/SearchBar.js index 58bf432c..c9e8b1b8 100644 --- a/client/src/components/common/SearchBar.js +++ b/client/src/components/common/SearchBar.js @@ -14,7 +14,7 @@ const SearchBar = ({ onChange, placeholder }) => { export default SearchBar; const SearchDiv = styled.div` - height: 26px; + height: 32px; align-items: center; border-radius: 3px; max-width: 360px; @@ -36,6 +36,7 @@ const TextInput = styled.input` background-color: transparent; width: 100%; padding-left: 11px; + font-size: 16px; &:focus { outline: none; diff --git a/client/src/components/common/Select.js b/client/src/components/common/Select.js index 6ea00027..2e3aafcc 100644 --- a/client/src/components/common/Select.js +++ b/client/src/components/common/Select.js @@ -23,13 +23,13 @@ const customStyles = { ...provided, // none of react-select's styles are passed to border: "1px solid #818181", - fontSize: 14, - height: 33, - minHeight: 33, + fontSize: 16, + height: 35, + minHeight: 35, }), valueContainer: (provided) => ({ ...provided, - height: 33, + height: 35, padding: "0 4px", }), @@ -42,7 +42,7 @@ const customStyles = { }), indicatorsContainer: (provided, state) => ({ ...provided, - height: "33px", + height: "35px", padding: "8px", }), }; diff --git a/client/src/components/common/animation/LoadingDots.js b/client/src/components/common/animation/LoadingDots.js new file mode 100644 index 00000000..8f5190d7 --- /dev/null +++ b/client/src/components/common/animation/LoadingDots.js @@ -0,0 +1,49 @@ +import React from "react"; +import styled from "styled-components"; + +const LoadingDots = ({ size = 8, color }) => { + return ( + + + + + + ); +}; + +export default LoadingDots; + +const Wrapper = styled.div` + display: flex; + + div { + background-color: ${(props) => props.color || "#fff"}; + } +`; + +const Dot = styled.div` + height: ${(props) => props.size + "px"}; + width: ${(props) => props.size + "px"}; + border-radius: ${(props) => props.size / 2 + "px"}; + margin: ${(props) => props.size * 0.3 + "px"}; + + @keyframes inout { + from { + transform: scale(1); + } + + 50% { + transform: scale(1.35); + } + + to { + transform: scale(1); + } + } + + animation-name: inout; + animation-delay: ${(props) => props.delay + "ms"}; + animation-iteration-count: infinite; + animation-direction: forward; + animation-duration: 850ms; +`; diff --git a/client/src/components/courses/CreateCourse.js b/client/src/components/courses/CreateCourse.js index dd9a2515..ff5b9716 100644 --- a/client/src/components/courses/CreateCourse.js +++ b/client/src/components/courses/CreateCourse.js @@ -1,22 +1,16 @@ import React, { useState } from "react"; import styled from "styled-components"; import Button from "../common/Button"; -import Input from "../common/Input"; -import InputLabel from "../common/InputLabel"; import Modal from "../common/Modal"; -import Select from "../common/Select"; - -const INVITE_OPTIONS = [ - { - label: "Share Link / Access Code", - value: "code", - description: - "Anyone with the link or access code can join. Share the link / code below with students.", - }, -]; +import CourseConfirmation from "./createCourse/CourseConfirmation"; +import CourseInfo from "./createCourse/CourseInfo"; const CreateCourse = () => { const [modalIsShown, toggleModal] = useState(false); + // Course is set by the CourseInfo component when instructors create the course + // The info is used to share the course link/access code provided by the API + const [course, setCourse] = useState(null); + return ( <> + {!course ? ( + + ) : ( + { + toggleModal(false); + setCourse(null); + }} + /> + )} )} @@ -75,20 +49,4 @@ const CreateCourse = () => { export default CreateCourse; -const LeftColumn = styled.div` - margin-right: 5px; -`; - -const RightColumn = styled.div` - margin-left: 5px; -`; - -const TopSection = styled.div` - padding: 0 15px; -`; - -const HighlightedSection = styled.div` - background-color: #f8f8f8; - margin-top: 15px; - padding: 15px; -`; +const AnimationSlider = styled.div``; diff --git a/client/src/components/courses/JoinCourse.js b/client/src/components/courses/JoinCourse.js index 20c0daa7..4390664b 100644 --- a/client/src/components/courses/JoinCourse.js +++ b/client/src/components/courses/JoinCourse.js @@ -19,12 +19,12 @@ const JoinCourse = () => { width="420px" data-testid="join-course-modal" > -

SEARCH FOR A COURSE

+

SEARCH FOR A COURSE

University Course Name -

OR JOIN BY ACCESS CODE

+

OR JOIN BY ACCESS CODE

Access Code + + ); +}; + +export default CourseConfirmation; + +const LeftColumn = styled.div` + margin-right: 5px; +`; + +const RightColumn = styled.div` + margin-left: 5px; +`; + +const Circle = styled.div` + background: #4a86fa; + box-shadow: 0px 0px 7px 2px rgb(74 134 250 / 19%); + height: 55px; + width: 55px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 50px; +`; + +const ShareIcon = styled.img` + height: 34px; +`; + +const HighlightedSection = styled.div` + background-color: #f8f8f8; + margin-top: 20px; + padding: 15px; + border-radius: 4px; + width: 100%; +`; + +const CopyOverlay = styled.div` + position: relative; + cursor: pointer; +`; + +const CopyIcon = styled.img` + height: 24px; + position: absolute; + right: 5px; + background: white; + top: 4px; +`; diff --git a/client/src/components/courses/createCourse/CourseInfo.js b/client/src/components/courses/createCourse/CourseInfo.js new file mode 100644 index 00000000..1636eb9d --- /dev/null +++ b/client/src/components/courses/createCourse/CourseInfo.js @@ -0,0 +1,141 @@ +import React, { useState } from "react"; +import Select from "../../common/Select"; +import axios from "axios"; +import Button from "../../common/Button"; +import Input from "../../common/Input"; +import InputLabel from "../../common/InputLabel"; +import styled from "styled-components"; +import Errors from "../../common/Errors"; + +const INVITE_OPTIONS = [ + { + label: "Share Link / Access Code", + value: "code", + description: + "Anyone with the link or access code can join. Create the course to generate a link.", + }, +]; + +const CourseInfo = ({ setCourse }) => { + const [form, setForm] = useState({ + university: null, + course: null, + canJoinById: true, + loading: false, + errors: null, + }); + + const handleChange = (e) => { + setForm({ + ...form, + [e.target.name]: e.target.value, + }); + }; + + const selectUniversity = (option) => { + setForm({ + ...form, + university: option.value, + }); + }; + + const sendCourseRequest = () => { + setForm({ ...form, loading: true }); + setTimeout(() => { + const endpoint = "/api/courses"; + const data = { + university: form.university, + course: form.course, + canJoinById: form.canJoinById, + }; + axios + .post(process.env.REACT_APP_SERVER_URL + endpoint, data) + .then((res) => { + setCourse(res.data); + }) + .catch((err) => { + if (err.response && err.response.data) { + // Set the errors provided by our API request + console.log(err.response.data.errors); + setForm({ + ...form, + errors: err.response.data.errors, + loading: false, + }); + } else { + setForm({ + ...form, + loading: false, + errors: [ + "There was an error creating the course. Please try again.", + ], + }); + } + }); + }, 1000); + }; + + return ( + <> +

CREATE A COURSE

+ + + University Name + + + + + + Student Access +