├── pharmd-app ├── .prettierrc ├── public │ ├── robots.txt │ ├── favicon.ico │ ├── logo192.png │ ├── logo512.png │ └── manifest.json ├── src │ ├── styles │ │ ├── README.md │ │ └── App.css │ ├── assets │ │ ├── images │ │ │ └── mountains.jpg │ │ └── icons │ │ │ ├── errorFilled.svg │ │ │ ├── verticalSplit.svg │ │ │ ├── test.svg │ │ │ ├── diagram.svg │ │ │ ├── filter.svg │ │ │ ├── help.svg │ │ │ ├── student.svg │ │ │ └── person.svg │ ├── components │ │ ├── README.md │ │ ├── Form │ │ │ ├── Button.js │ │ │ ├── index.js │ │ │ ├── FileSelector.js │ │ │ └── Select.js │ │ ├── Fields │ │ │ ├── AvatarField.js │ │ │ ├── EmphasisField.js │ │ │ ├── TextField.js │ │ │ ├── TemporaryField.js │ │ │ ├── CohortField.js │ │ │ ├── ChipField.js │ │ │ ├── ScoredListField │ │ │ │ ├── ScoredListField.js │ │ │ │ ├── ScoredListItemSkeleton.js │ │ │ │ ├── ScoredListItemError.js │ │ │ │ └── ScoredListItem.js │ │ │ ├── QuickChipField.js │ │ │ ├── CourseField.js │ │ │ ├── GradeField.js │ │ │ ├── QuickInfoField.js │ │ │ ├── NoteListField.js │ │ │ ├── CourseListField.js │ │ │ ├── QuickProfileField.js │ │ │ └── NoteField.js │ │ ├── Basic │ │ │ ├── Radio Controls │ │ │ │ ├── RadioButton.js │ │ │ │ └── RadioButtonGroup.js │ │ │ ├── IconButton.js │ │ │ ├── Temp.css │ │ │ ├── IconInfo.js │ │ │ ├── Checkbox Controls │ │ │ │ ├── CheckboxButton.js │ │ │ │ ├── CheckboxFilterButtonGroup.js │ │ │ │ └── CheckboxButtonGroup.js │ │ │ ├── QuickInfo.js │ │ │ ├── FilterChip.js │ │ │ ├── NoteIcon.js │ │ │ ├── Avatar.js │ │ │ ├── Tooltip.js │ │ │ ├── Icon.js │ │ │ ├── ExpansionPanel.js │ │ │ ├── GridCard.js │ │ │ ├── StatusBullet.js │ │ │ ├── BarGraph.js │ │ │ ├── RangeSlider.js │ │ │ ├── Breadcrumb.js │ │ │ └── Autocomplete.js │ │ ├── Table │ │ │ ├── Table.js │ │ │ └── TableRow.js │ │ ├── Nav │ │ │ ├── NavIndicator.js │ │ │ ├── AppBar.js │ │ │ ├── NavItemSecondary.js │ │ │ └── NavItemLink.js │ │ ├── Inputs │ │ │ ├── StudentSearchInput.js │ │ │ ├── SearchInput.js │ │ │ ├── GpaSliderInput.js │ │ │ ├── StatusCheckboxInput.js │ │ │ └── CohortMultiSelectInput.js │ │ └── Layout │ │ │ ├── DashboardLayout.js │ │ │ └── Menu.js │ ├── services │ │ ├── README.md │ │ ├── index.js │ │ ├── Auth │ │ │ ├── isAuth.js │ │ │ ├── getPermissions.js │ │ │ ├── logout.js │ │ │ ├── index.js │ │ │ ├── authError.js │ │ │ └── login.js │ │ ├── Data │ │ │ ├── CreateHooks.js │ │ │ ├── httpClient.js │ │ │ ├── index.js │ │ │ ├── EditHooks.js │ │ │ ├── DeleteHooks.js │ │ │ ├── UpdateHooks.js │ │ │ └── GetHooks.js │ │ ├── Utility.js │ │ ├── FileUploadService.js │ │ └── StudentAssessmentService.js │ ├── config │ │ ├── README.md │ │ ├── customRoutes.js │ │ └── backendRoutes.js │ ├── constants │ │ ├── README.md │ │ ├── text.js │ │ ├── routes.js │ │ ├── mappers.js │ │ └── apiObjects.js │ ├── screens │ │ ├── courses │ │ │ ├── index.js │ │ │ ├── CourseList.js │ │ │ ├── CourseResource.js │ │ │ └── CourseFilter.js │ │ ├── upload │ │ │ ├── index.js │ │ │ ├── UploadResource.js │ │ │ └── components │ │ │ │ ├── UploadFileChooser.js │ │ │ │ └── SpreadsheetUploader.js │ │ ├── student │ │ │ ├── index.js │ │ │ ├── StudentQuickView.js │ │ │ ├── StudentList.js │ │ │ ├── StudentToolbarFilter.js │ │ │ ├── StudentContentGrid.js │ │ │ ├── StudentResource.js │ │ │ └── StudentDisplayFilters.js │ │ ├── profile │ │ │ ├── ProfileForm.js │ │ │ ├── ProfilePage.js │ │ │ ├── index.js │ │ │ ├── ProfileEdit.js │ │ │ ├── ProfileCreate.js │ │ │ ├── ProfileShow.js │ │ │ └── ProfileManage.js │ │ └── studentDetails │ │ │ ├── StudentDetailsSide.js │ │ │ ├── StudentExamList.js │ │ │ ├── StudentCourseList.js │ │ │ ├── StudentDetailsScreen.js │ │ │ └── StudentDetailsContentGrid.js │ ├── setupTests.js │ ├── stories │ │ ├── fileSelector.stories.js │ │ ├── Spreadsheet-Uploader.stories.js │ │ ├── spreadsheet-uploader.stories.js │ │ ├── README.md │ │ ├── navBar.stories.js │ │ ├── routerBreadcrumb.stories.js │ │ ├── rangeSlider.stories.js │ │ ├── selector.stories.js │ │ ├── expansionPanel.stories.js │ │ ├── radioGroup.stories.js │ │ ├── scoredListField.stories.js │ │ ├── studentSearchInput.stories.js │ │ ├── tooltip.stories.js │ │ ├── navItem.stories.js │ │ ├── avatar.stories.js │ │ ├── icon.stories.js │ │ ├── tableCells.stories.js │ │ └── table.stories.js │ ├── themes │ │ ├── selected-theme.js │ │ ├── dark-theme.js │ │ ├── light-theme.js │ │ └── field-colors.js │ ├── Dashboard.js │ ├── typeDefs.js │ ├── index.js │ ├── twin.d.ts │ ├── plugins │ │ └── type-system │ │ │ └── index.js │ ├── App.js │ └── index.html ├── .babelrc ├── .eslintrc.json ├── .env.test ├── .storybook │ ├── addons.js │ ├── main.js │ ├── config.js │ └── theme-provider.js ├── .env.production ├── .env.development ├── .env ├── babel-plugin-macros.config.js ├── .gitignore ├── webpack.config.js ├── package.json └── README.md └── README.md /pharmd-app/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 96, 3 | "singleQuote": false 4 | } 5 | -------------------------------------------------------------------------------- /pharmd-app/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | -------------------------------------------------------------------------------- /pharmd-app/src/styles/README.md: -------------------------------------------------------------------------------- 1 | # Styles 2 | 3 | Contains stylesheets used by various parts of the application 4 | -------------------------------------------------------------------------------- /pharmd-app/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sandboxnu/pharmd-tracker/develop/pharmd-app/public/favicon.ico -------------------------------------------------------------------------------- /pharmd-app/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sandboxnu/pharmd-tracker/develop/pharmd-app/public/logo192.png -------------------------------------------------------------------------------- /pharmd-app/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sandboxnu/pharmd-tracker/develop/pharmd-app/public/logo512.png -------------------------------------------------------------------------------- /pharmd-app/src/assets/images/mountains.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sandboxnu/pharmd-tracker/develop/pharmd-app/src/assets/images/mountains.jpg -------------------------------------------------------------------------------- /pharmd-app/src/components/README.md: -------------------------------------------------------------------------------- 1 | # Components 2 | 3 | The modular pieces that get used throughout the app to make it render and function 4 | -------------------------------------------------------------------------------- /pharmd-app/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-env", "@babel/preset-react"], 3 | "plugins": ["macros", "@babel/plugin-proposal-class-properties"] 4 | } 5 | -------------------------------------------------------------------------------- /pharmd-app/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["airbnb", "prettier"], 3 | "plugins": ["prettier"], 4 | "rules": { 5 | "prettier/prettier": ["error"] 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /pharmd-app/src/services/README.md: -------------------------------------------------------------------------------- 1 | #Services 2 | 3 | These files contain advanced/core logic of the application, including that functions to communicate with the backend 4 | -------------------------------------------------------------------------------- /pharmd-app/src/config/README.md: -------------------------------------------------------------------------------- 1 | #Config 2 | 3 | These files contain values that establish the configuration of the app, including environment variables and paths to access network resources 4 | -------------------------------------------------------------------------------- /pharmd-app/src/constants/README.md: -------------------------------------------------------------------------------- 1 | # Constants 2 | 3 | These files will contain values used by multiple portions of the app, but which will not readily change or be retrieved from the server 4 | -------------------------------------------------------------------------------- /pharmd-app/src/screens/courses/index.js: -------------------------------------------------------------------------------- 1 | import TestIcon from "../../assets/icons/test.svg"; 2 | import CourseScreen from "./CourseResource"; 3 | 4 | export default { 5 | list: CourseScreen, 6 | icon: TestIcon 7 | }; 8 | -------------------------------------------------------------------------------- /pharmd-app/src/screens/upload/index.js: -------------------------------------------------------------------------------- 1 | import DiagramIcon from "../../assets/icons/diagram.svg"; 2 | import UploadScreen from "./UploadResource"; 3 | 4 | export default { 5 | list: UploadScreen, 6 | icon: DiagramIcon 7 | }; 8 | -------------------------------------------------------------------------------- /pharmd-app/src/screens/student/index.js: -------------------------------------------------------------------------------- 1 | import StudentIcon from "../../assets/icons/student.svg"; 2 | import StudentScreen from "./StudentResource"; 3 | 4 | export default { 5 | list: StudentScreen, 6 | icon: StudentIcon 7 | }; 8 | -------------------------------------------------------------------------------- /pharmd-app/src/components/Form/Button.js: -------------------------------------------------------------------------------- 1 | import tw, { styled } from "twin.macro"; 2 | import Button from "@material-ui/core/Button"; 3 | 4 | const ButtonCustom = styled(Button)` 5 | ${tw`m-4`} 6 | `; 7 | 8 | export default ButtonCustom; 9 | -------------------------------------------------------------------------------- /pharmd-app/src/components/Fields/AvatarField.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Avatar from "../Basic/Avatar"; 3 | 4 | const AvatarField = ({ record = {}, source }) => ; 5 | 6 | export default AvatarField; 7 | -------------------------------------------------------------------------------- /pharmd-app/src/components/Form/index.js: -------------------------------------------------------------------------------- 1 | import Radio from "@material-ui/core/Radio"; 2 | import Select from "./Select"; 3 | import FileSelector from "./FileSelector"; 4 | import Button from "./Button"; 5 | 6 | export { Select, FileSelector, Button, Radio }; 7 | -------------------------------------------------------------------------------- /pharmd-app/.env.test: -------------------------------------------------------------------------------- 1 | # THESE VALUES WILL BE USED DURING TESTING (npm test) 2 | 3 | API_URL=http://localhost:3000 4 | OLD_API_URL=https://student-db-remote.herokuapp.com 5 | 6 | AUTH_URL=http://localhost:3000/auth 7 | OLD_AUTH_URL=https://student-db-remote.herokuapp.com/login 8 | -------------------------------------------------------------------------------- /pharmd-app/.storybook/addons.js: -------------------------------------------------------------------------------- 1 | import "@storybook/addon-knobs/register"; 2 | import "@storybook/addon-actions/register"; 3 | import "@storybook/addon-links/register"; 4 | 5 | import "storybook-addon-styled-component-theme/dist/register"; 6 | import "storybook-addon-designs/register"; 7 | -------------------------------------------------------------------------------- /pharmd-app/src/components/Basic/Radio Controls/RadioButton.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Radio from "@material-ui/core/Radio"; 3 | 4 | const RadioButton = props => { 5 | return ; 6 | }; 7 | 8 | export default RadioButton; 9 | -------------------------------------------------------------------------------- /pharmd-app/.env.production: -------------------------------------------------------------------------------- 1 | # THESE VALUES WILL BE USED IN PRODUCTION CODE (npm build) 2 | 3 | API_URL=http://localhost:3000 4 | OLD_API_URL=https://student-db-remote.herokuapp.com 5 | 6 | AUTH_URL=http://localhost:3000/auth 7 | OLD_AUTH_URL=https://student-db-remote.herokuapp.com/login 8 | 9 | -------------------------------------------------------------------------------- /pharmd-app/src/services/index.js: -------------------------------------------------------------------------------- 1 | import FileUploadService from "./FileUploadService"; 2 | import AuthProvider from "./Auth"; 3 | import DataProvider from "./Data"; 4 | 5 | // This should be kept up-to-date as new service files are added 6 | export { FileUploadService, AuthProvider, DataProvider }; 7 | -------------------------------------------------------------------------------- /pharmd-app/src/setupTests.js: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import "@testing-library/jest-dom/extend-expect"; 6 | -------------------------------------------------------------------------------- /pharmd-app/src/constants/text.js: -------------------------------------------------------------------------------- 1 | export const HOME_TITLE = "Home"; 2 | export const STUDENT_TITLE = "Students"; 3 | export const COURSES_TITLE = "Courses"; 4 | export const UPLOAD_TITLE = "Upload"; 5 | export const STUDENT_QUICKVIEW_TITLE = "Quickview"; 6 | export const STUDENT_DETAILS_TITLE = "Details"; 7 | -------------------------------------------------------------------------------- /pharmd-app/src/stories/fileSelector.stories.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import FileSelectorComponent from "../components/Form/FileSelector"; 3 | 4 | export default { 5 | title: "File Selector", 6 | component: File 7 | }; 8 | 9 | export const Default = () => ; 10 | -------------------------------------------------------------------------------- /pharmd-app/src/constants/routes.js: -------------------------------------------------------------------------------- 1 | export const HOME_MAIN = "/"; 2 | export const STUDENT_DETAILS = "/students/:id/details"; 3 | export const STUDENT_QUICKVIEW = "/students/:id"; 4 | export const STUDENTS_MAIN = "/students"; 5 | export const COURSES_MAIN = "/courses"; 6 | export const UPLOAD_MAIN = "/upload"; 7 | -------------------------------------------------------------------------------- /pharmd-app/.env.development: -------------------------------------------------------------------------------- 1 | # THESE VALUES WILL BE USED IN ANYTHING NOT DEVELOPMENT, PRODUCTION, OR TESTING (FALLBACK) 2 | 3 | API_URL=http://localhost:3000 4 | OLD_API_URL=https://student-db-remote.herokuapp.com 5 | 6 | AUTH_URL=http://localhost:3000/auth 7 | OLD_AUTH_URL=https://student-db-remote.herokuapp.com/login 8 | -------------------------------------------------------------------------------- /pharmd-app/.env: -------------------------------------------------------------------------------- 1 | # THESE ENVIRONMENT VARIABLES WILL ACT AS THE DEFAULT CASE IF NOT IN LOCAL, PRODUCTION, OR TEST RUNNING ENVIRONMENT 2 | 3 | API_URL=http://localhost:3000 4 | OLD_API_URL=https://student-db-remote.herokuapp.com 5 | 6 | AUTH_URL=http://localhost:3000/auth 7 | OLD_AUTH_URL=https://student-db-remote.herokuapp.com/login 8 | -------------------------------------------------------------------------------- /pharmd-app/src/stories/Spreadsheet-Uploader.stories.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import SpreadsheetUploader from "../screens/upload/components/SpreadsheetUploader"; 3 | export default { 4 | title: "Spreadsheet Uploader", 5 | component: SpreadsheetUploader 6 | }; 7 | 8 | export const Default = () => ; 9 | -------------------------------------------------------------------------------- /pharmd-app/src/stories/spreadsheet-uploader.stories.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import SpreadsheetUploader from "../screens/upload/components/SpreadsheetUploader"; 3 | export default { 4 | title: "Spreadsheet Uploader", 5 | component: SpreadsheetUploader 6 | }; 7 | 8 | export const Default = () => ; 9 | -------------------------------------------------------------------------------- /pharmd-app/src/constants/mappers.js: -------------------------------------------------------------------------------- 1 | import { COURSES_TITLE, STUDENT_TITLE, UPLOAD_TITLE } from "./text"; 2 | import { COURSES_MAIN, STUDENTS_MAIN, UPLOAD_MAIN } from "./routes"; 3 | 4 | export const MAP_BREADCRUMB_NAME = { 5 | [STUDENTS_MAIN]: STUDENT_TITLE, 6 | [COURSES_MAIN]: COURSES_TITLE, 7 | [UPLOAD_MAIN]: UPLOAD_TITLE 8 | }; 9 | -------------------------------------------------------------------------------- /pharmd-app/src/config/customRoutes.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Route } from "react-router-dom"; 3 | 4 | import StudentDetailsScreen from "../screens/studentDetails/StudentDetailsScreen"; 5 | import { STUDENT_DETAILS } from "../constants/routes"; 6 | 7 | export default []; 8 | -------------------------------------------------------------------------------- /pharmd-app/src/themes/selected-theme.js: -------------------------------------------------------------------------------- 1 | import createDarkTheme from "./dark-theme"; 2 | import createLigthTheme from "./light-theme"; 3 | 4 | function selectTheme(theme) { 5 | switch (theme) { 6 | case "dark": 7 | return createDarkTheme(); 8 | default: 9 | return createLigthTheme(); 10 | } 11 | } 12 | 13 | export default selectTheme; 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pharmd-tracker 2 | 3 | **To run the front end:** 4 | 5 | **In pharmd-app, run:** 6 | 7 | - npm install 8 | - npm start 9 | 10 | **Afterward** 11 | 12 | - Install CORS plugin 13 | - link to [chrome plugin](https://chrome.google.com/webstore/detail/allow-cors-access-control/lhobafahddgcelffkeicbaginigeejlf?hl=en) 14 | - Turn it on by clicking on the C icon 15 | -------------------------------------------------------------------------------- /pharmd-app/src/assets/icons/errorFilled.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /pharmd-app/src/components/Fields/EmphasisField.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import tw, { styled } from "twin.macro"; 3 | import TextField from "./TextField"; 4 | 5 | const StyledField = styled(TextField)` 6 | ${tw`fontStyle-6 text-secondary font-semibold`} 7 | `; 8 | 9 | const EmphasisField = props => { 10 | return ; 11 | }; 12 | 13 | export default EmphasisField; 14 | -------------------------------------------------------------------------------- /pharmd-app/src/Dashboard.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Card from "@material-ui/core/Card"; 3 | import CardContent from "@material-ui/core/CardContent"; 4 | import CardHeader from "@material-ui/core/CardHeader"; 5 | 6 | export default () => ( 7 | 8 | 9 | Lorem ipsum sic dolor amet... 10 | 11 | ); 12 | -------------------------------------------------------------------------------- /pharmd-app/src/components/Fields/TextField.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import tw, { styled } from "twin.macro"; 3 | import { TextField as RATextField } from "react-admin"; 4 | 5 | const StyledTextField = styled(RATextField)` 6 | ${tw`fontStyle-6 text-gray-700 font-medium`} 7 | `; 8 | 9 | const TextField = props => { 10 | return ; 11 | }; 12 | 13 | export default TextField; 14 | -------------------------------------------------------------------------------- /pharmd-app/babel-plugin-macros.config.js: -------------------------------------------------------------------------------- 1 | // babel-plugin-macros.config.js 2 | module.exports = { 3 | tailwind: { 4 | config: "./src/tailwind.config.js", 5 | styled: "styled-components/macro" 6 | }, 7 | twin: { 8 | config: "./src/tailwind.config.js", 9 | preset: "styled-components", 10 | autoCssProp: true, 11 | debugProp: true, 12 | debugPlugins: false, 13 | debug: false, 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /pharmd-app/src/stories/README.md: -------------------------------------------------------------------------------- 1 | # Stories 2 | 3 | These files are [Storybook](https://www.google.com/url?sa=t&rct=j&q=&esrc=s&source=web&cd=1&cad=rja&uact=8&ved=2ahUKEwjJxpeg3-vnAhVKU98KHa_ZB2MQFjAAegQICRAC&url=https%3A%2F%2Fstorybook.js.org%2F&usg=AOvVaw3yFiehIItinYyB9pHN8YIy) stories used primarily for visual testing of the application's components. Can be tested by cloning the repo and running 'npm run storybook' in a terminal 4 | -------------------------------------------------------------------------------- /pharmd-app/src/services/Auth/isAuth.js: -------------------------------------------------------------------------------- 1 | /** 2 | * The place to validate the user's credentials 3 | * Redirecting to the login page whenever the REST response is 401 is usually not enough, so something else 4 | * should be done here to avoid using stale data 5 | * @return {Promise|Promise} 6 | */ 7 | export default () => { 8 | return localStorage.getItem("token") ? Promise.resolve() : Promise.reject(); 9 | }; 10 | -------------------------------------------------------------------------------- /pharmd-app/src/components/Basic/IconButton.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import IconButtonMaterial from "@material-ui/core/IconButton"; 3 | import Icon from "./Icon"; 4 | 5 | const IconButton = ({ src, color = "tertiary" }) => { 6 | return ( 7 | 8 | 9 | 10 | ); 11 | }; 12 | 13 | export default IconButton; 14 | -------------------------------------------------------------------------------- /pharmd-app/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | 25 | ../.idea/* -------------------------------------------------------------------------------- /pharmd-app/src/components/Basic/Temp.css: -------------------------------------------------------------------------------- 1 | .bar { 2 | animation: grow .25s cubic-bezier(0.42, 0, 1, 1); 3 | } 4 | 5 | .car { 6 | animation: grow .25s linear reverse; 7 | } 8 | 9 | @keyframes grow { 10 | 0% { 11 | height: 0%; 12 | } 13 | 100% { 14 | height: 100%; 15 | } 16 | } 17 | 18 | @keyframes shrink { 19 | 100% { 20 | height: 100%; 21 | } 22 | 0% { 23 | height: 0%; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /pharmd-app/src/components/Basic/IconInfo.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Icon from "./Icon"; 3 | import Tooltip from "./Tooltip"; 4 | 5 | const InfoIcon = ({ src, text, type = "error", size = "small" }) => { 6 | return ( 7 | 8 |
9 | 10 |
11 |
12 | ); 13 | }; 14 | 15 | export default InfoIcon; 16 | -------------------------------------------------------------------------------- /pharmd-app/src/services/Auth/getPermissions.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Calls this method to check user permissions 3 | * Useful for enabling or disabling features on a per user basis 4 | * Called when user redirects 5 | * More info here: https://marmelab.com/react-admin/Authorization.html 6 | * @return {Promise|Promise} 7 | */ 8 | export default () => { 9 | const role = localStorage.getItem("permissions"); 10 | return role ? Promise.resolve(role) : Promise.reject(); 11 | }; 12 | -------------------------------------------------------------------------------- /pharmd-app/src/typeDefs.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @type {{ 3 | * NUID: string, 4 | * courseName: string, 5 | * examName: string, 6 | * percentage: number, 7 | * studentName: string 8 | * courseTerm: string | null 9 | * }} 10 | * */ 11 | export class BasicStudentAssessment {} 12 | 13 | /** 14 | * @type {{ 15 | * data: Array, 16 | * headers: Array, 17 | * subHeaders: Array | null 18 | * 19 | * }} 20 | */ 21 | export class FileData {} 22 | -------------------------------------------------------------------------------- /pharmd-app/src/assets/icons/verticalSplit.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /pharmd-app/src/stories/navBar.stories.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { withKnobs } from "@storybook/addon-knobs"; 3 | import { MemoryRouter } from "react-router"; 4 | import AppBar from "../components/Nav/AppBar"; 5 | 6 | export default { 7 | title: "AppBar", 8 | component: AppBar, 9 | decorators: [withKnobs] 10 | }; 11 | 12 | export const Default = () => ( 13 | 14 | } /> 15 | 16 | ); 17 | -------------------------------------------------------------------------------- /pharmd-app/src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import "./index.css"; 4 | import App from "./App"; 5 | import * as serviceWorker from "./serviceWorker"; 6 | 7 | ReactDOM.render(, document.getElementById("root")); 8 | 9 | // If you want your app to work offline and load faster, you can change 10 | // unregister() to register() below. Note this comes with some pitfalls. 11 | // Learn more about service workers: https://bit.ly/CRA-PWA 12 | serviceWorker.unregister(); 13 | -------------------------------------------------------------------------------- /pharmd-app/.storybook/main.js: -------------------------------------------------------------------------------- 1 | // Storybook is using the same webpack config file 2 | // we used for our app. THis allows us to use css 3 | // and scss for styling in storybook. 4 | const custom = require("../webpack.config"); 5 | 6 | module.exports = { 7 | webpackFinal: config => { 8 | config.plugins.push(custom().plugins[2]); 9 | return { 10 | ...config, 11 | module: { ...config.module, rules: custom().module.rules } 12 | }; 13 | }, 14 | addons: ['@storybook/addon-actions/register'] 15 | }; 16 | -------------------------------------------------------------------------------- /pharmd-app/src/stories/routerBreadcrumb.stories.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { withKnobs } from "@storybook/addon-knobs"; 3 | import { MemoryRouter } from "react-router"; 4 | import RouterBreadcrumb from "../components/Basic/Breadcrumb"; 5 | 6 | export default { 7 | title: "Breadcrumb", 8 | component: RouterBreadcrumb, 9 | decorators: [withKnobs] 10 | }; 11 | 12 | export const Default = () => ( 13 | 14 | 15 | 16 | ); 17 | -------------------------------------------------------------------------------- /pharmd-app/src/stories/rangeSlider.stories.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import RangeSlider from "../components/Basic/RangeSlider"; 3 | 4 | export default { 5 | title: "RangeSlider", 6 | component: RangeSlider 7 | }; 8 | 9 | export const Default = () => ( 10 |
11 | console.log("NEW VALUE", newVal)} 13 | setValueText={value => `${value}`} 14 | min={0} 15 | max={4} 16 | step={0.5} 17 | disabled={false} 18 | /> 19 |
20 | ); 21 | -------------------------------------------------------------------------------- /pharmd-app/src/stories/selector.stories.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import SelectorComponent from "../components/Form/Select"; 3 | 4 | export default { 5 | title: "Selector", 6 | component: SelectorComponent 7 | }; 8 | 9 | const selectData = [ 10 | { value: ["3", "5"], displayValue: "3" }, 11 | { value: ["3", "6"], displayValue: "4" }, 12 | { value: ["3", "8"], displayValue: "5" } 13 | ]; 14 | 15 | export const Default = () => ( 16 | console.log(val)} /> 17 | ); 18 | -------------------------------------------------------------------------------- /pharmd-app/src/services/Auth/logout.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Called when the user clicks the Logout button given by React Admin 3 | * Used often to remove potentially sensitive data from Redux store 4 | * Immediately redirects user to login page 5 | * Responsible for cleaning up authentication data, like tokens stored in local storage 6 | * @return {Promise} 7 | */ 8 | export default () => { 9 | localStorage.removeItem("token"); 10 | localStorage.removeItem("permissions"); 11 | localStorage.removeItem("userInfo"); 12 | return Promise.resolve(); 13 | }; 14 | -------------------------------------------------------------------------------- /pharmd-app/src/twin.d.ts: -------------------------------------------------------------------------------- 1 | import 'twin.macro' 2 | import styledImport, {css as cssImport, CSSProp} from 'styled-components' 3 | 4 | declare module 'twin.macro' { 5 | // The styled and css imports 6 | const styled: typeof styledImport 7 | const css: typeof cssImport 8 | } 9 | 10 | declare module 'react' { 11 | // The css prop 12 | interface HTMLAttributes extends DOMAttributes { 13 | css?: CSSProp 14 | } 15 | // The inline svg css prop 16 | interface SVGProps extends SVGProps { 17 | css?: CSSProp 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /pharmd-app/src/services/Auth/index.js: -------------------------------------------------------------------------------- 1 | import login from "./login"; 2 | import logout from "./logout"; 3 | import authError from "./authError"; 4 | import isAuth from "./isAuth"; 5 | import getPermissions from "./getPermissions"; 6 | 7 | /** 8 | * Main source of authentication logic for the app 9 | * Object keys are hooks to execute authentication code 10 | * Provided to the components authProvider prop to connect to these hooks 11 | */ 12 | export default { 13 | login, 14 | logout, 15 | checkError: authError, 16 | checkAuth: isAuth, 17 | getPermissions 18 | }; 19 | -------------------------------------------------------------------------------- /pharmd-app/src/components/Fields/TemporaryField.js: -------------------------------------------------------------------------------- 1 | // This field is strictly for demonstration purposes only 2 | 3 | import React from "react"; 4 | import tw, { css, styled } from "twin.macro"; 5 | 6 | const Field = styled.span` 7 | ${tw`fontStyle-6 text-gray-700 font-medium`} 8 | 9 | ${props => 10 | props.primary && 11 | css` 12 | ${tw`fontStyle-6 text-secondary font-semibold`} 13 | `} 14 | font-size: 1.4em; 15 | `; 16 | 17 | const TempField = ({ source, isEmphasis }) => { 18 | return {source}; 19 | }; 20 | 21 | export default TempField; 22 | -------------------------------------------------------------------------------- /pharmd-app/src/screens/upload/UploadResource.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import tw, { styled } from "twin.macro"; 4 | 5 | import AppBar from "../../components/Nav/AppBar"; 6 | import SpreadsheetUploader from "./components/SpreadsheetUploader"; 7 | 8 | const MainContent = styled.div` 9 | ${tw`p-12 pt-2 `} 10 | flex-grow: 1; 11 | `; 12 | 13 | const UploadScreen = props => { 14 | return ( 15 | <> 16 | 17 | 18 | 19 | 20 | 21 | ); 22 | }; 23 | 24 | export default UploadScreen; 25 | -------------------------------------------------------------------------------- /pharmd-app/src/stories/expansionPanel.stories.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ExpansionPanel from "../components/Basic/ExpansionPanel"; 3 | 4 | export const Default = () => ( 5 | TEST

} 7 | DetailChild={ 8 |

9 | {" "} 10 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse malesuada lacus ex, 11 | sit amet blandit leo lobortis eget. Lorem ipsum dolor sit amet, consectetur adipiscing 12 | elit. Suspendisse malesuada lacus ex, sit amet blandit leo lobortis eget. 13 |

14 | } 15 | /> 16 | ); 17 | -------------------------------------------------------------------------------- /pharmd-app/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /pharmd-app/src/components/Basic/Checkbox Controls/CheckboxButton.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Description: 3 | * This Component creates a single checkbox button. 4 | * TODO: 5 | * Date: 04-23-2020 6 | */ 7 | 8 | // -------------------------- IMPORTS -------------------------- 9 | 10 | // Function Imports 11 | import React from "react"; 12 | 13 | // Component Imports 14 | import Checkbox from "@material-ui/core/Checkbox"; 15 | 16 | // -------------------------- COMPONENT -------------------------- 17 | const CheckboxButton = props => { 18 | return ; 19 | }; 20 | 21 | export default CheckboxButton; 22 | -------------------------------------------------------------------------------- /pharmd-app/src/screens/profile/ProfileForm.js: -------------------------------------------------------------------------------- 1 | import {BooleanInput, required, SimpleForm, TextInput} from "react-admin"; 2 | import React from "react"; 3 | 4 | export default () => 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /pharmd-app/src/services/Data/CreateHooks.js: -------------------------------------------------------------------------------- 1 | // React Admin hooks that will makes calls to the API to CREATE content 2 | 3 | /** 4 | * Returns object with hooks for creating resources 5 | * @param {Function} httpClient The function to make an HTTP request 6 | * @param {string} BACKEND_URL the base URL to make a request to 7 | * @return {{create: (function(*, *): *)}} 8 | */ 9 | export default (httpClient, BACKEND_URL) => ({ 10 | create: (resource, params) => 11 | httpClient(`${BACKEND_URL}/${resource}`, { 12 | method: "POST", 13 | body: JSON.stringify(params.data) 14 | }).then(({ json }) => ({ 15 | data: { ...params.data, id: json.id } 16 | })) 17 | }); 18 | -------------------------------------------------------------------------------- /pharmd-app/src/stories/radioGroup.stories.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import FormControlLabel from "@material-ui/core/FormControlLabel"; 4 | import RadioButtonsGroup from "../components/Basic/Radio Controls/RadioButtonGroup"; 5 | 6 | export const Default = () => ( 7 |
8 | console.log("NEW VALUE", newVal)} 10 | label="Origin" 11 | > 12 | 13 | 14 | 15 | 16 |
17 | ); 18 | -------------------------------------------------------------------------------- /pharmd-app/src/screens/profile/ProfilePage.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import tw, { styled } from "twin.macro"; 4 | import PropTypes from "prop-types"; 5 | 6 | import AppBar from "../../components/Nav/AppBar"; 7 | 8 | const MainContent = styled.div` 9 | ${tw`p-12 pt-2`} 10 | flex-grow: 1; 11 | flex-direction: row; 12 | `; 13 | 14 | const ProfilePage = props => { 15 | return ( 16 | <> 17 | 18 | 19 | {props.children} 20 | 21 | 22 | ); 23 | }; 24 | 25 | ProfilePage.propTypes = { 26 | pageName: PropTypes.string.isRequired 27 | }; 28 | 29 | export default ProfilePage; 30 | -------------------------------------------------------------------------------- /pharmd-app/src/services/Data/httpClient.js: -------------------------------------------------------------------------------- 1 | // Add authorization token to each request 2 | import { fetchUtils } from "react-admin"; 3 | 4 | /** 5 | * Wrapper for React Admin HTTP JSON fetcher, adding headers and token to each request 6 | * @param {string} url the url to make a request to 7 | * @param {Object?} options Optionally contains headers for this request 8 | * @return {Promise} 9 | */ 10 | export default (url, options = {}) => { 11 | if (!options.headers) { 12 | options.headers = new Headers({ Accept: "application/json" }); 13 | } 14 | const token = localStorage.getItem("token"); 15 | options.headers.set("Authorization", `Bearer ${token}`); 16 | return fetchUtils.fetchJson(url, options); 17 | }; 18 | -------------------------------------------------------------------------------- /pharmd-app/src/stories/scoredListField.stories.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { text } from "@storybook/addon-knobs"; 3 | import List from "@material-ui/core/List"; 4 | import ScoredListItem from "../components/Fields/ScoredListField/ScoredListItem"; 5 | 6 | const groupId = "GROUP-ID1"; 7 | 8 | const primaryText = "Primary"; 9 | const defaultPrimaryText = "Primary Text"; 10 | 11 | export const Loading = () => ( 12 | 13 | 14 | 15 | ); 16 | 17 | export const Primary = () => ( 18 | 19 | 25 | 26 | ); 27 | -------------------------------------------------------------------------------- /pharmd-app/src/screens/studentDetails/StudentDetailsSide.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import { styled } from "twin.macro"; 4 | import QuickProfileField from "../../components/Fields/QuickProfileField"; 5 | import QuickInfoField from "../../components/Fields/QuickInfoField"; 6 | import NoteListField from "../../components/Fields/NoteListField"; 7 | 8 | const Details = styled.div` 9 | padding: 8px 24px 24px; 10 | `; 11 | 12 | const StudentDetailsSide = ({ source }) => { 13 | return ( 14 |
15 | 16 | 17 | 18 |
19 | ); 20 | }; 21 | 22 | export default StudentDetailsSide; 23 | -------------------------------------------------------------------------------- /pharmd-app/src/assets/icons/test.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /pharmd-app/src/assets/icons/diagram.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /pharmd-app/src/components/Fields/CohortField.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import tw, { styled } from "twin.macro"; 3 | import InfoIcon from "../Basic/IconInfo"; 4 | import ErrorIcon from "../../assets/icons/errorFilled.svg"; 5 | 6 | const Field = styled.span` 7 | ${tw`fontStyle-6 text-gray-700 font-medium inline-flex`} 8 | `; 9 | 10 | const Offset = styled.span` 11 | ${tw`ml-3`} 12 | `; 13 | 14 | const CohortField = ({ record = {}, source }) => { 15 | const cohort = record[source]; 16 | return ( 17 | 18 | {cohort.current} 19 | {cohort.moved && ( 20 | 21 | 22 | 23 | )} 24 | 25 | ); 26 | }; 27 | export default CohortField; 28 | -------------------------------------------------------------------------------- /pharmd-app/src/services/Auth/authError.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Each time the API returns an error, react-admin calls the authProvider.checkError() method. 3 | * It’s up to this function to decide which HTTP status codes should let the user continue (by returning a resolved promise) 4 | * or log them out (by returning a rejected promise). 5 | * If the API requires authentication, and the user credentials are missing in the request or invalid, 6 | * the API usually answers with an HTTP error code 401 or 403. 7 | * @param {number} status 8 | * @return {Promise|Promise} 9 | */ 10 | export default ({ status }) => { 11 | if (status === 401 || status === 403) { 12 | localStorage.removeItem("token"); 13 | return Promise.reject(); 14 | } 15 | return Promise.resolve(); 16 | }; 17 | -------------------------------------------------------------------------------- /pharmd-app/.storybook/config.js: -------------------------------------------------------------------------------- 1 | import { addDecorator, configure } from "@storybook/react"; 2 | 3 | import { withThemesProvider } from "storybook-addon-styled-component-theme"; 4 | import ThemeProvider from "./theme-provider"; 5 | 6 | import createLigthTheme from "../src/themes/light-theme"; 7 | import createDarkTheme from "../src/themes/dark-theme"; 8 | 9 | import "../src/styles/App.css"; 10 | 11 | const themes = [createLigthTheme(), createDarkTheme()]; 12 | addDecorator(withThemesProvider(themes, ThemeProvider)); 13 | 14 | configure(require.context("../src/stories", true, /\.stories\.js$/), module); 15 | // stories: ['../src/**/*.stories.js'], 16 | // addons: [ 17 | // '@storybook/preset-create-react-app', 18 | // '@storybook/addon-actions', 19 | // '@storybook/addon-links', 20 | // ], 21 | -------------------------------------------------------------------------------- /pharmd-app/src/services/Data/index.js: -------------------------------------------------------------------------------- 1 | // Learn more here: https://marmelab.com/react-admin/DataProviders.html 2 | 3 | import { BACKEND_URL } from "../../config/backendRoutes"; 4 | import DeleteHooks from "./DeleteHooks"; 5 | import GetHooks from "./GetHooks"; 6 | import httpClient from "./httpClient"; 7 | import EditHooks from "./EditHooks"; 8 | import UpdateHooks from "./UpdateHooks"; 9 | import CreateHooks from "./CreateHooks"; 10 | 11 | // import real BACKEND_URL to use pharmD backend data 12 | // const BACKEND_URL = FAKE_API; 13 | 14 | export default { 15 | ...GetHooks(httpClient, BACKEND_URL), 16 | ...CreateHooks(httpClient, BACKEND_URL), 17 | ...EditHooks(httpClient, BACKEND_URL), 18 | ...UpdateHooks(httpClient, BACKEND_URL), 19 | ...DeleteHooks(httpClient, BACKEND_URL) 20 | }; 21 | -------------------------------------------------------------------------------- /pharmd-app/src/components/Basic/QuickInfo.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import tw, { styled } from "twin.macro"; 3 | 4 | const InfoDisplay = styled.div` 5 | width: calc(100% / 3); 6 | display: inline-block; 7 | text-align: center; 8 | margin-left: auto; 9 | margin-right: auto; 10 | `; 11 | 12 | const Heading = styled.h2` 13 | ${tw` fontStyle-6 text-sm`} 14 | font-weight: bold; 15 | text-align: center; 16 | `; 17 | 18 | const Detail = styled.h3` 19 | color: #2b2b90; 20 | text-align: center; 21 | font-size: 1.5em; 22 | margin-bottom: 0; 23 | `; 24 | 25 | const QuickInfo = ({ info, label }) => { 26 | return ( 27 | 28 | {info} 29 | {label} 30 | 31 | ); 32 | }; 33 | 34 | export default QuickInfo; 35 | -------------------------------------------------------------------------------- /pharmd-app/src/config/backendRoutes.js: -------------------------------------------------------------------------------- 1 | export const BACKEND_URL = process.env.API_URL; 2 | export const FAKE_API = "https://student-db-remote.herokuapp.com"; 3 | export const { AUTH_URL } = process.env; 4 | export const FAKE_AUTH = "https://student-db-remote.herokuapp.com/login"; 5 | 6 | export const BACKEND_STUDENTS = `${BACKEND_URL}students/`; 7 | export const BACKEND_STUDENTASSESSMENTS = `${BACKEND_URL}students-assessments/`; 8 | export const BACKEND_SESSIONS = `${BACKEND_URL}sessions/`; 9 | export const BACKEND_ASSESSMENTS = `${BACKEND_URL}assessments/`; 10 | export const BACKEND_COURSES = `${BACKEND_URL}courses/`; 11 | export const BACKEND_NOTES = `${BACKEND_URL}notes/`; 12 | export const BACKEND_STUDENTCOURSES = `${BACKEND_URL}student-courses/`; 13 | export const BACKEND_USERS = `${BACKEND_URL}users/`; 14 | -------------------------------------------------------------------------------- /pharmd-app/src/components/Table/Table.js: -------------------------------------------------------------------------------- 1 | // -------------------------- IMPORTS -------------------------- 2 | 3 | // Function Imports 4 | import React from "react"; 5 | // Style Imports 6 | import tw, { styled } from "twin.macro"; 7 | // Component Imports 8 | import { Datagrid as DatagridRA } from "react-admin"; 9 | 10 | // -------------------------- STYLE -------------------------- 11 | const Datagrid = styled(DatagridRA)` 12 | .headerCell { 13 | ${tw`h-18 fontStyle-5 tracking-wide text-gray-600`} 14 | } 15 | 16 | .rowCell { 17 | ${tw`h-16`} 18 | } 19 | `; 20 | 21 | // -------------------------- COMPONENT -------------------------- 22 | const Table = props => { 23 | return ; 24 | }; 25 | 26 | export default Table; 27 | -------------------------------------------------------------------------------- /pharmd-app/src/components/Nav/NavIndicator.js: -------------------------------------------------------------------------------- 1 | // -------------------------- IMPORTS -------------------------- 2 | 3 | // Function Imports 4 | import React from "react"; 5 | // Style Imports 6 | import tw, { styled } from "twin.macro"; 7 | 8 | // -------------------------- STYLE -------------------------- 9 | const Indicator = styled.div` 10 | ${tw`w-1 h-24 absolute rounded-l`} 11 | 12 | background: ${props => props.theme.palette.primary.main}; 13 | left: 92px; 14 | right: 0; 15 | top: ${props => props.top}px; 16 | bottom: 0; 17 | transition: top 0.3s ease-in-out; 18 | `; 19 | 20 | // -------------------------- COMPONENT -------------------------- 21 | const NavIndicator = ({ index = 0 }) => { 22 | const h = 96; 23 | return ; 24 | }; 25 | 26 | export default NavIndicator; 27 | -------------------------------------------------------------------------------- /pharmd-app/src/stories/studentSearchInput.stories.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { text, withKnobs } from "@storybook/addon-knobs"; 3 | 4 | import { Filter } from "react-admin"; 5 | import StudentSearchInput from "../components/Inputs/StudentSearchInput"; 6 | 7 | export default { 8 | title: "Search Input", 9 | component: StudentSearchInput, 10 | decorators: [withKnobs] 11 | }; 12 | 13 | const groupId = "GROUP-ID3"; 14 | 15 | const label = "Helper Text"; 16 | const defaultLabel = "Search"; 17 | 18 | export const Default = () => ( 19 | 20 | `^${inputValue}`} 24 | onChange={() => Console.log()} 25 | alwaysOn 26 | /> 27 | 28 | ); 29 | -------------------------------------------------------------------------------- /pharmd-app/src/services/Data/EditHooks.js: -------------------------------------------------------------------------------- 1 | // React Admin hooks that will makes calls to the API to EDIT content 2 | 3 | // NOTE: this may be added for compatibility with fake API--edit hook does not exist in React Admin 4 | 5 | /** 6 | * Returns object with hooks for editing resources 7 | * @param {Function} httpClient The function to make an HTTP request 8 | * @param {string} BACKEND_URL the base URL to make a request to 9 | * @return {{edit: (function(*=, *=): *)}} 10 | */ 11 | export default (httpClient, BACKEND_URL) => ({ 12 | edit: (resource, params) => { 13 | return httpClient(`${BACKEND_URL}/${resource}`, { 14 | method: "PUT", 15 | body: JSON.stringify(params.data) 16 | }).then(({ json }) => { 17 | return { 18 | data: { ...params.data, id: json.id } 19 | }; 20 | }); 21 | } 22 | }); 23 | -------------------------------------------------------------------------------- /pharmd-app/src/assets/icons/filter.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /pharmd-app/src/components/Fields/ChipField.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import tw, { styled } from "twin.macro"; 3 | import { Chip as MuiChip } from "@material-ui/core"; 4 | import { getFieldBackgroundColor, getFieldColor } from "../../themes/field-colors"; 5 | 6 | const Pill = styled(MuiChip)` 7 | ${tw`rounded-lg capitalize w-28 fontStyle-4 font-bold tracking-wider`} 8 | font-size: 1.1em; 9 | color: ${props => props.color}; 10 | background-color: ${props => props.backgroundColor}; 11 | `; 12 | 13 | const ChipField = props => { 14 | const readableStatus = props.record[props.source] 15 | .replace("_", " ") 16 | .split(" ") 17 | .map(s => s.charAt(0) + s.slice(1).toLowerCase()) 18 | .join(" "); 19 | 20 | return ( 21 | 26 | ); 27 | }; 28 | 29 | export default ChipField; 30 | -------------------------------------------------------------------------------- /pharmd-app/src/plugins/type-system/index.js: -------------------------------------------------------------------------------- 1 | function getTypeScale() { 2 | const types = {}; 3 | // TODO: Currently we have 12 font styles, yet we might add more in the future 4 | // Create a function that retrieves the unique number of Font Style css variables 5 | 6 | for (let i = 1; i <= 12; i++) { 7 | types[`.fontStyle-${i}`] = { 8 | fontSize: `var(--font-style-${i}-font-size)`, 9 | fontFamily: `var(--font-style-${i}-font-family)`, 10 | fontStyle: `var(--font-style-${i}-font-style)`, 11 | letterSpacing: `var(--font-style-${i}-letter-spacing)`, 12 | lineHeight: `var(--font-style-${i}-line-height)` 13 | }; 14 | } 15 | 16 | return types; 17 | } 18 | 19 | const customTypeStyles = {}; 20 | 21 | module.exports = function(variants) { 22 | return function({ addUtilities }) { 23 | addUtilities( 24 | { 25 | ...getTypeScale(), 26 | ...customTypeStyles 27 | }, 28 | variants 29 | ); 30 | }; 31 | }; 32 | -------------------------------------------------------------------------------- /pharmd-app/src/screens/profile/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import tw, { styled } from "twin.macro"; 3 | import ProfileEdit from "./ProfileEdit"; 4 | import ProfileCreate from "./ProfileCreate"; 5 | import ProfileShow from "./ProfileShow"; 6 | import Avatar from "../../components/Basic/Avatar"; 7 | import ProfileManage from "./ProfileManage"; 8 | 9 | const AvatarContainer = styled.div` 10 | ${tw`flex justify-center items-center w-24 h-24`} 11 | `; 12 | 13 | const AvatarIcon = () => { 14 | const info = JSON.parse(localStorage.getItem("userInfo")); 15 | let props = {}; 16 | if (info) { 17 | props = { 18 | firstName: info.firstName, 19 | lastName: info.lastName 20 | }; 21 | } 22 | 23 | return ( 24 | 25 | 26 | 27 | ); 28 | }; 29 | 30 | export default { 31 | list: ProfileManage, 32 | show: ProfileShow, 33 | create: ProfileCreate, 34 | edit: ProfileEdit, 35 | icon: AvatarIcon 36 | }; 37 | -------------------------------------------------------------------------------- /pharmd-app/src/components/Fields/ScoredListField/ScoredListField.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useGetOne } from "react-admin"; 3 | import List from "@material-ui/core/List"; 4 | import ScoredListItem from "./ScoredListItem"; 5 | 6 | const ScoredListField = ({ record = {}, source }) => { 7 | const courses = record[source]; 8 | return ( 9 | 10 | {courses.map((course, index) => { 11 | // TODO: Need to fix render issue here 12 | const { data, loading, error } = useGetOne("courses", course.course_id); 13 | 14 | return ( 15 | 24 | ); 25 | })} 26 | 27 | ); 28 | }; 29 | 30 | export default ScoredListField; 31 | -------------------------------------------------------------------------------- /pharmd-app/src/components/Basic/FilterChip.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Description: 3 | * This Component creates a multi select drop down / text input to filter Student data based on GPA. 4 | * This component also allows the user to type in the option they are looking for to filter the options. 5 | * 6 | * Date: 03-18-2021 7 | */ 8 | 9 | // -------------------------- IMPORTS -------------------------- 10 | // Function Imports 11 | import React from "react"; 12 | 13 | // Component Imports 14 | import Chip from "@material-ui/core/Chip"; 15 | import ClearIcon from '@material-ui/icons/Clear'; 16 | 17 | // Style Imports 18 | import tw from "twin.macro"; 19 | 20 | // -------------------------- COMPONENT -------------------------- 21 | const FilterChip = ({label, ...props}) => { 22 | return ( 23 | } 27 | label={label} 28 | /> 29 | ); 30 | } 31 | 32 | export default FilterChip; 33 | -------------------------------------------------------------------------------- /pharmd-app/src/stories/tooltip.stories.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { select, text, withKnobs } from "@storybook/addon-knobs"; 3 | import Tooltip from "../components/Basic/Tooltip"; 4 | 5 | export default { 6 | title: "Tooltip", 7 | component: Tooltip, 8 | decorators: [withKnobs] 9 | }; 10 | 11 | const label = "Placement"; 12 | const options = { 13 | Right: "right", 14 | Left: "left", 15 | Bottom: "bottom", 16 | Top: "top" 17 | }; 18 | const defaultValue = "right"; 19 | const groupId = "GROUP-ID1"; 20 | 21 | const labelText = "Title(Label)"; 22 | const defaultValueText = "Title"; 23 | 24 | export const Default = () => ( 25 |
26 | 31 |
Child
32 |
33 |
34 | ); 35 | -------------------------------------------------------------------------------- /pharmd-app/src/components/Basic/NoteIcon.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import SvgIcon from "@material-ui/core/SvgIcon"; 3 | import { css, styled } from "twin.macro"; 4 | 5 | const SVG = styled(SvgIcon)` 6 | vertical-align: middle; 7 | ${props => 8 | props.color === "grey" && 9 | css` 10 | color: ${props => props.theme.palette.tertiary.main}; 11 | `} 12 | 13 | ${props => 14 | props.color === "black" && 15 | css` 16 | color: black; 17 | `} 18 | 19 | ${props => 20 | props.primary && 21 | css` 22 | margin-left: auto; 23 | `} 24 | `; 25 | 26 | const NoteIcon = ({ src, color = "inherit", size, htmlColor, isPrimary }) => { 27 | const inputProps = { 28 | component: src, 29 | htmlColor: htmlColor || undefined, 30 | color: htmlColor ? undefined : color 31 | }; 32 | 33 | // spread declaration to add props 34 | return ; 35 | }; 36 | 37 | export default NoteIcon; 38 | -------------------------------------------------------------------------------- /pharmd-app/src/components/Fields/QuickChipField.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import tw, { styled } from "twin.macro"; 3 | import { Chip as MuiChip } from "@material-ui/core"; 4 | import { getFieldBackgroundColor, getFieldColor } from "../../themes/field-colors"; 5 | 6 | const Pill = styled(MuiChip)` 7 | ${tw`rounded-lg capitalize w-24 fontStyle-4 font-bold tracking-wider`} 8 | font-size: ${props => props.label === "Graduated" ? `0.75em` : `0.8em`}; 9 | color: ${props => props.color}; 10 | background-color: ${props => props.backgroundColor}; 11 | `; 12 | 13 | const QuickChipField = props => { 14 | const readableStatus = props.record[props.source] 15 | .replace("_", " ") 16 | .split(" ") 17 | .map(s => s.charAt(0) + s.slice(1).toLowerCase()) 18 | .join(" "); 19 | 20 | return ( 21 | 26 | ); 27 | }; 28 | 29 | export default QuickChipField; 30 | -------------------------------------------------------------------------------- /pharmd-app/src/stories/navItem.stories.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { BrowserRouter as Router } from "react-router-dom"; 3 | import TestIcon from "../assets/icons/student.svg"; 4 | import NavItemLink from "../components/Nav/NavItemLink"; 5 | 6 | export const Default = () => { 7 | const ref = React.createRef(); 8 | 9 | return ( 10 | 11 | {}} 17 | isOpen={false} 18 | ref={ref} 19 | /> 20 | 21 | ); 22 | }; 23 | 24 | export const Active = () => { 25 | const ref = React.createRef(); 26 | 27 | return ( 28 | 29 | {}} 35 | isOpen={false} 36 | isActive 37 | ref={ref} 38 | /> 39 | 40 | ); 41 | }; 42 | -------------------------------------------------------------------------------- /pharmd-app/src/screens/profile/ProfileEdit.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { BooleanInput, Edit, required, SimpleForm, TextInput } from "react-admin"; 3 | import ProfilePage from "./ProfilePage"; 4 | 5 | const EditForm = props => ( 6 | 7 | 8 | 9 | 10 | 11 | 18 | 19 | 20 | 21 | ); 22 | 23 | const ProfileEdit = props => { 24 | return ( 25 | 26 | 27 | 28 | ); 29 | }; 30 | 31 | export default ProfileEdit; 32 | -------------------------------------------------------------------------------- /pharmd-app/src/services/Data/DeleteHooks.js: -------------------------------------------------------------------------------- 1 | // React Admin hooks that will makes calls to the API to DELETE content 2 | 3 | /** 4 | * Returns object with hooks for deleting resources 5 | * @param {Function} httpClient The function to make an HTTP request 6 | * @param {string} BACKEND_URL the base URL to make a request to 7 | * @return {{deleteMany: (function(*, *): Promise<{data: *}>), delete: (function(*, *): *)}} 8 | */ 9 | export default (httpClient, BACKEND_URL) => ({ 10 | delete: (resource, params) => 11 | httpClient(`${BACKEND_URL}/${resource}/${params.id}`, { 12 | method: "DELETE" 13 | }).then(({ json }) => ({ data: json })), 14 | 15 | // json-server doesn't handle filters on DELETE route, so we fallback to calling DELETE n times instead 16 | deleteMany: (resource, params) => 17 | Promise.all( 18 | params.ids.map(id => 19 | httpClient(`${BACKEND_URL}/${resource}/${id}`, { 20 | method: "DELETE" 21 | }) 22 | ) 23 | ).then(responses => ({ data: responses.map(({ json }) => json.id) })) 24 | }); 25 | -------------------------------------------------------------------------------- /pharmd-app/src/screens/profile/ProfileCreate.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { BooleanInput, Create, required, SimpleForm, TextInput } from "react-admin"; 3 | import ProfilePage from "./ProfilePage"; 4 | 5 | const CreateForm = props => ( 6 | 7 | 8 | 9 | 10 | 11 | 18 | 19 | 20 | 21 | ); 22 | 23 | const ProfileCreate = props => { 24 | return ( 25 | 26 | 27 | 28 | ); 29 | }; 30 | 31 | export default ProfileCreate; 32 | -------------------------------------------------------------------------------- /pharmd-app/src/components/Basic/Avatar.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import AvatarMaterial from "@material-ui/core/Avatar"; 3 | import tw, { styled } from "twin.macro"; 4 | import Icon from "./Icon"; 5 | import Person from "../../assets/icons/person.svg"; 6 | 7 | const AvatarStyled = styled(AvatarMaterial)` 8 | ${tw`w-12 h-12`} 9 | &.MuiAvatar-colorDefault { 10 | ${tw``} 11 | } 12 | `; 13 | 14 | const PictureStyled = styled(AvatarMaterial)` 15 | display: block; 16 | height: 120px; 17 | width: 120px; 18 | margin-left: auto; 19 | margin-right: auto; 20 | `; 21 | 22 | const Avatar = ({ firstName, lastName, imgUrl }) => { 23 | if (imgUrl) { 24 | const fullName = `${firstName} ${lastName}`; 25 | return ; 26 | } 27 | if (firstName) { 28 | const initials = `${firstName.charAt(0)}${lastName ? lastName.charAt(0) : ""}`; 29 | return {initials}; 30 | } 31 | return ( 32 | 33 | 34 | 35 | ); 36 | }; 37 | 38 | export default Avatar; 39 | -------------------------------------------------------------------------------- /pharmd-app/src/stories/avatar.stories.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { text, withKnobs } from "@storybook/addon-knobs"; 3 | import { withDesign } from "storybook-addon-designs"; 4 | import Avatar from "../components/Basic/Avatar"; 5 | import imgProfile from "../assets/images/mountains.jpg"; 6 | 7 | export default { 8 | title: "Avatar", 9 | component: Avatar, 10 | decorators: [withKnobs, withDesign] 11 | }; 12 | 13 | const groupId = "GROUP-ID2"; 14 | 15 | const firstName = "First Name"; 16 | const defaultFirstName = ""; 17 | 18 | const lastName = "Last Name"; 19 | const defaultLastName = ""; 20 | 21 | export const Default = () => ( 22 | 26 | ); 27 | 28 | Default.story = { 29 | parameters: { 30 | design: { 31 | type: "figma", 32 | url: 33 | "https://www.figma.com/file/QBWcVYJkF6K8o7WErRPsxd/Student-List-Mockups?node-id=15%3A39" 34 | } 35 | } 36 | }; 37 | 38 | export const Image = () => ; 39 | -------------------------------------------------------------------------------- /pharmd-app/src/themes/dark-theme.js: -------------------------------------------------------------------------------- 1 | import { createMuiTheme } from "@material-ui/core/styles"; 2 | import green from "@material-ui/core/colors/green"; 3 | import purple from "@material-ui/core/colors/purple"; 4 | 5 | const primaryGreen = green[500]; 6 | const accentGreen = green.A200; 7 | const darkGreen = green[900]; 8 | const primaryPurple = purple[500]; 9 | const accentPurple = purple.A200; 10 | const darkPurple = purple[900]; 11 | 12 | function createDarkTheme() { 13 | const overridings = { 14 | name: "Dark Theme", 15 | palette: { 16 | awesomeColors: { 17 | primary: primaryGreen, 18 | secondary: accentPurple 19 | }, 20 | primary: { 21 | light: accentPurple, 22 | main: primaryPurple, 23 | dark: darkPurple, 24 | contrastText: "#fff" 25 | }, 26 | type: "dark", 27 | secondary: { 28 | light: accentGreen, 29 | main: primaryGreen, 30 | dark: darkGreen, 31 | contrastText: "#fff" 32 | } 33 | } 34 | }; 35 | 36 | return createMuiTheme(overridings); 37 | } 38 | 39 | export default createDarkTheme; 40 | -------------------------------------------------------------------------------- /pharmd-app/src/components/Fields/CourseField.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import tw, { css, styled } from "twin.macro"; 3 | 4 | const Info = styled.div` 5 | ${tw`fontStyle-6 text-black font-medium inline-flex`} 6 | width: 100%; 7 | margin-top: 10px; 8 | padding-bottom: 10px; 9 | border-bottom: 1px solid #dcdcdc; 10 | font-size: 0.89em; 11 | ${props => 12 | props.primary && 13 | css` 14 | border-bottom: 0px; 15 | `} 16 | `; 17 | 18 | const Name = styled.div` 19 | position: relative; 20 | `; 21 | 22 | const Grade = styled.div` 23 | font-weight: bold; 24 | position: relative; 25 | margin-left: auto; 26 | `; 27 | 28 | const Circle = styled.span` 29 | margin-top: auto; 30 | margin-bottom: auto; 31 | margin-right: 15px; 32 | height: 15px; 33 | width: 15px; 34 | background-color: red; 35 | border-radius: 50%; 36 | `; 37 | 38 | const CourseField = ({ name, grade, isLine }) => { 39 | return ( 40 | 41 | 42 | {name} 43 | {`${grade}%`} 44 | 45 | ); 46 | }; 47 | 48 | export default CourseField; 49 | -------------------------------------------------------------------------------- /pharmd-app/src/services/Data/UpdateHooks.js: -------------------------------------------------------------------------------- 1 | // React Admin hooks that will makes calls to the API to UPDATE content 2 | 3 | /** 4 | * Returns object with hooks for updating resources 5 | * @param {Function} httpClient The function to make an HTTP request 6 | * @param {string} BACKEND_URL the base URL to make a request to 7 | * @return {{updateMany: (function(*, *): Promise<{data: *}>), update: (function(*, *): *)}} 8 | */ 9 | export default (httpClient, BACKEND_URL) => ({ 10 | update: (resource, params) => 11 | httpClient(`${BACKEND_URL}/${resource}/${params.id}`, { 12 | method: "PUT", 13 | body: JSON.stringify(params.data) 14 | }).then(({ json }) => ({ data: json })), 15 | 16 | // json-server doesn't handle filters on UPDATE route, so we fallback to calling UPDATE n times instead 17 | updateMany: (resource, params) => 18 | Promise.all( 19 | params.ids.map(id => 20 | httpClient(`${BACKEND_URL}/${resource}/${id}`, { 21 | method: "PUT", 22 | body: JSON.stringify(params.data) 23 | }) 24 | ) 25 | ).then(responses => ({ data: responses.map(({ json }) => json.id) })) 26 | }); 27 | -------------------------------------------------------------------------------- /pharmd-app/src/screens/studentDetails/StudentExamList.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { ReferenceManyField } from "react-admin"; 3 | import TextField from "../../components/Fields/TextField"; 4 | import EmphasisField from "../../components/Fields/EmphasisField"; 5 | import GradeField from "../../components/Fields/GradeField"; 6 | import Table from "../../components/Table/Table"; 7 | import { STUDENT_EXAM } from "../../constants/apiObjects"; 8 | 9 | const StudentExamList = ({ resource, basePath, record }) => { 10 | return ( 11 | 19 | 20 | 21 | 22 | 23 | 24 |
25 |
26 | ); 27 | }; 28 | export default StudentExamList; 29 | -------------------------------------------------------------------------------- /pharmd-app/src/services/Utility.js: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | 3 | /** 4 | * Creates an object from 5 | * @param {Array} inputArray 6 | * @param {Array} keySourceArray 7 | * @return {Object} 8 | */ 9 | export function arrayToObject(inputArray, keySourceArray) { 10 | const obj = {}; 11 | inputArray.forEach((value, index) => (obj[keySourceArray[index]] = value)); 12 | return obj; 13 | } 14 | 15 | export const doAllRequests = requests => axios.all(requests); 16 | 17 | 18 | export function formatDecimal(preformatted) { 19 | const preformattedString = preformatted.toString(); 20 | const decimalRegex = /^(?\d*)(?\.)?(?\d*)?/; 21 | const numPlaces = 2; 22 | 23 | const matches = preformattedString.match(decimalRegex); 24 | const wholeNumber = matches.groups.wholeNumber; 25 | const point = matches.groups.point || '.'; 26 | let decimal = matches.groups.decimal || '00'; 27 | 28 | while (decimal.length < numPlaces) { 29 | decimal = decimal + '0'; 30 | } 31 | 32 | if (decimal.length > numPlaces) { 33 | decimal = decimal.substring(0, numPlaces); 34 | } 35 | 36 | return wholeNumber + point + decimal; 37 | } 38 | -------------------------------------------------------------------------------- /pharmd-app/src/components/Basic/Tooltip.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ToolTipMaterial from "@material-ui/core/Tooltip"; 3 | import tw, { styled } from "twin.macro"; 4 | import Zoom from "@material-ui/core/Zoom"; 5 | 6 | /* 7 | NOTE: Uses Portals to style component. 8 | The Portal provides a first-class way to render children 9 | into a DOM node that exists outside the DOM hierarchy of 10 | the parent component 11 | */ 12 | 13 | const TooltipStyled = styled(({ className, ...props }) => ( 14 | 15 | ))` 16 | ${tw`py-2 px-4 rounded-lg fontStyle-2 text-white`} 17 | background-color: var(--color-bg-tooltip); 18 | font-weight: bold; 19 | 20 | .MuiTooltip-arrow { 21 | color: var(--color-bg-tooltip); 22 | } 23 | `; 24 | 25 | const Tooltip = ({ title, placement = "right", ...props }) => { 26 | return ( 27 | 35 | {props.children} 36 | 37 | ); 38 | }; 39 | 40 | export default Tooltip; 41 | -------------------------------------------------------------------------------- /pharmd-app/src/components/Basic/Icon.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import SvgIcon from "@material-ui/core/SvgIcon"; 3 | import tw, { css, styled } from "twin.macro"; 4 | 5 | const SVG = styled(SvgIcon)` 6 | 7 | transition: color 0.5s ease; 8 | 9 | &.MuiSvgIcon-colorDisabled { 10 | ${tw`text-gray-400`} 11 | } 12 | 13 | &.MuiSvgIcon-colorAction { 14 | ${tw`text-gray-600`} 15 | } 16 | 17 | ${props => 18 | props.color === "tertiary" && 19 | css` 20 | color: ${props => props.theme.palette.tertiary.main}; 21 | `} 22 | 23 | ${props => 24 | props.color === "black" && 25 | css` 26 | color: #333; 27 | `} 28 | 29 | ${props => 30 | props.color === "white" && 31 | css` 32 | color: #fff; 33 | `} 34 | `; 35 | 36 | const Icon = ({ src, color = "inherit", size = "default", accessibleTitle, htmlColor }) => { 37 | const inputProps = { 38 | component: src, 39 | titleAccess: accessibleTitle || undefined, 40 | htmlColor: htmlColor || undefined, 41 | color: htmlColor ? undefined : color 42 | }; 43 | 44 | return ; 45 | }; 46 | 47 | export default Icon; 48 | -------------------------------------------------------------------------------- /pharmd-app/src/components/Fields/GradeField.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import tw, { styled } from "twin.macro"; 3 | import { FunctionField } from "react-admin"; 4 | import TextField from "./TextField"; 5 | import { getFieldColor } from "../../themes/field-colors"; 6 | import { STUDENT_COURSE } from "../../constants/apiObjects"; 7 | import { formatDecimal } from '../../services/Utility' 8 | 9 | const StyledTextField = styled(TextField)` 10 | ${tw`fontStyle-6 text-gray-700 font-medium`} 11 | 12 | color: ${props => props.color}; 13 | font-size: 1.4em; 14 | `; 15 | 16 | const StyledFunctionField = styled(FunctionField)` 17 | ${tw`fontStyle-6 text-gray-700 font-medium`} 18 | 19 | color: ${props => props.color}; 20 | font-size: 1.4em; 21 | `; 22 | 23 | const GradeField = props => { 24 | return props.source === STUDENT_COURSE.LETTER_GRADE ? 25 | ( 26 | 27 | ) : ( 28 | `${formatDecimal(record[props.source])}`} 30 | color={getFieldColor(props)} 31 | {...props} 32 | /> 33 | ); 34 | }; 35 | 36 | export default GradeField; 37 | -------------------------------------------------------------------------------- /pharmd-app/.storybook/theme-provider.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import CssBaseline from "@material-ui/core/CssBaseline"; 4 | import { MuiThemeProvider } from "@material-ui/core/styles"; 5 | import { StylesProvider } from "@material-ui/styles"; 6 | import { ThemeProvider as StyledThemeProvider } from "styled-components"; 7 | 8 | const ThemeProvider = ({ theme, children }) => { 9 | const nextTheme = Object.assign({}, theme); 10 | 11 | var d = document.documentElement; 12 | if (nextTheme.name === "Dark Theme") { 13 | d.classList.add("theme-dark"); 14 | } else if (nextTheme.name === "Light Theme") { 15 | d.classList.remove("theme-dark"); 16 | } 17 | 18 | return ( 19 | 20 | 21 | 22 | 23 | {children} 24 | 25 | 26 | 27 | ); 28 | }; 29 | 30 | ThemeProvider.propTypes = { 31 | children: PropTypes.oneOfType([PropTypes.node, PropTypes.func]).isRequired, 32 | theme: PropTypes.object.isRequired 33 | }; 34 | 35 | export default ThemeProvider; 36 | -------------------------------------------------------------------------------- /pharmd-app/src/assets/icons/help.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /pharmd-app/src/screens/studentDetails/StudentCourseList.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { ReferenceManyField } from "react-admin"; 3 | import TextField from "../../components/Fields/TextField"; 4 | import EmphasisField from "../../components/Fields/EmphasisField"; 5 | import GradeField from "../../components/Fields/GradeField"; 6 | import Table from "../../components/Table/Table"; 7 | import { STUDENT_COURSE } from "../../constants/apiObjects"; 8 | 9 | const StudentCourseList = ({ resource, basePath, record }) => { 10 | return ( 11 | 19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 |
27 | ); 28 | }; 29 | export default StudentCourseList; 30 | -------------------------------------------------------------------------------- /pharmd-app/src/screens/student/StudentQuickView.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import { Show, SimpleShowLayout as SimpleShowLayoutRA, useEditController } from "react-admin"; 4 | 5 | import { styled } from "twin.macro"; 6 | import QuickProfileField from "../../components/Fields/QuickProfileField"; 7 | import QuickInfoField from "../../components/Fields/QuickInfoField"; 8 | 9 | import NoteListField from "../../components/Fields/NoteListField"; 10 | /* 11 | TODO: Material Skeleton for loading 12 | */ 13 | 14 | const SimpleShowLayout = styled(SimpleShowLayoutRA)` 15 | &.MuiCard-root { 16 | box-shadow: none; 17 | } 18 | `; 19 | 20 | const StudentQuickView = props => { 21 | const controllerProps = useEditController(props); 22 | if (!controllerProps.record) { 23 | return null; 24 | } 25 | return ( 26 | 27 | 33 | 34 | 35 | 36 | 37 | ); 38 | }; 39 | 40 | export default StudentQuickView; 41 | -------------------------------------------------------------------------------- /pharmd-app/src/components/Table/TableRow.js: -------------------------------------------------------------------------------- 1 | // -------------------------- IMPORTS -------------------------- 2 | 3 | // Function Imports 4 | import React, { cloneElement } from "react"; 5 | // Style Imports 6 | import tw, { styled } from "twin.macro"; 7 | // Component Imports 8 | import TableCell from "@material-ui/core/TableCell"; 9 | import MuiTableRow from "@material-ui/core/TableRow"; 10 | 11 | // -------------------------- STYLE -------------------------- 12 | const TableRowStyled = styled(MuiTableRow)` 13 | &.MuiTableRow-root { 14 | ${tw`h-16`} 15 | } 16 | 17 | & .MuiTableCell-head { 18 | ${tw`h-18 fontStyle-5 tracking-wide text-gray-600`} 19 | } 20 | `; 21 | 22 | // -------------------------- COMPONENT -------------------------- 23 | const TableRow = ({ record, resource, id, children, selected, basePath }) => { 24 | return ( 25 | 26 | {React.Children.map(children, field => ( 27 | 28 | {cloneElement(field, { 29 | record, 30 | basePath, 31 | resource 32 | })} 33 | 34 | ))} 35 | 36 | ); 37 | }; 38 | 39 | export default TableRow; 40 | -------------------------------------------------------------------------------- /pharmd-app/src/screens/courses/CourseList.js: -------------------------------------------------------------------------------- 1 | // -------------------------- IMPORTS -------------------------- 2 | 3 | // Function Imports 4 | import React from "react"; 5 | import { Datagrid as DatagridRA } from "react-admin"; 6 | import tw, { styled } from "twin.macro"; 7 | import { COURSE } from "../../constants/apiObjects"; 8 | // Component Imports 9 | import EmphasisField from "../../components/Fields/EmphasisField"; 10 | import TextField from "../../components/Fields/TextField"; 11 | 12 | // Style Imports 13 | 14 | // -------------------------- STYLE -------------------------- 15 | 16 | const Datagrid = styled(DatagridRA)` 17 | .headerCell { 18 | /* background-color: red; */ 19 | ${tw`h-18 fontStyle-5 tracking-wide text-gray-600`} 20 | } 21 | 22 | .rowCell { 23 | ${tw`h-16`} 24 | } 25 | `; 26 | 27 | // -------------------------- COMPONENT -------------------------- 28 | 29 | const CourseList = props => { 30 | return ( 31 | 32 | 33 | 34 | 35 | 36 | ); 37 | }; 38 | export default CourseList; 39 | -------------------------------------------------------------------------------- /pharmd-app/src/screens/courses/CourseResource.js: -------------------------------------------------------------------------------- 1 | // -------------------------- IMPORTS -------------------------- 2 | 3 | // Function Imports 4 | import React from "react"; 5 | // Component Imports 6 | import { List as RaList } from "react-admin"; 7 | import styled from "styled-components/macro"; 8 | import tw from "tailwind.macro"; 9 | import AppBar from "../../components/Nav/AppBar"; 10 | import CourseList from "./CourseList"; 11 | import { CourseFilter } from "./CourseFilter"; 12 | 13 | // Style Imports 14 | 15 | // -------------------------- STYLE -------------------------- 16 | 17 | const MainContent = styled.div` 18 | ${tw`p-12 pt-2 `} 19 | flex-grow: 1; 20 | `; 21 | 22 | const List = styled(RaList)` 23 | ${tw`p-6 bg-white rounded-xl`} 24 | margin-top: 50px; 25 | 26 | .MuiPaper-elevation1 { 27 | box-shadow: none; 28 | } 29 | `; 30 | 31 | // -------------------------- COMPONENT -------------------------- 32 | 33 | const CourseResource = props => { 34 | return ( 35 | <> 36 | 37 | 38 | } {...props}> 39 | 40 | 41 | 42 | 43 | ); 44 | }; 45 | 46 | export default CourseResource; 47 | -------------------------------------------------------------------------------- /pharmd-app/src/components/Form/FileSelector.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Button from './Button'; 3 | import PropTypes from "prop-types"; 4 | 5 | class Selector extends React.Component { 6 | static propTypes = { 7 | onChoose: PropTypes.func 8 | }; 9 | 10 | static defaultProps = { 11 | onChoose() {} 12 | }; 13 | 14 | constructor(props) { 15 | super(props); 16 | this.state = { 17 | fileName: 'Upload File' 18 | }; 19 | this.onChooseFile = this.onChooseFile.bind(this); 20 | } 21 | 22 | onChooseFile(event) { 23 | const file = event.target.files[0]; 24 | this.setState({ 25 | fileName: file.name 26 | }); 27 | this.props.onChoose(file) 28 | } 29 | 30 | render() { 31 | return ; 45 | } 46 | 47 | } 48 | 49 | export default Selector; 50 | -------------------------------------------------------------------------------- /pharmd-app/src/stories/icon.stories.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { select, withKnobs } from "@storybook/addon-knobs"; 3 | import Icon from "../components/Basic/Icon"; 4 | import IconButtonI from "../components/Basic/IconButton"; 5 | import TestIcon from "../assets/icons/student.svg"; 6 | 7 | export default { 8 | title: "Icon", 9 | decorators: [withKnobs] 10 | }; 11 | 12 | const label = "Color"; 13 | const options = { 14 | Default: "inherit", 15 | Primary: "primary", 16 | Secondary: "secondary", 17 | Tertiary: "tertiary", 18 | Action: "action", 19 | Error: "error", 20 | Disable: "disabled", 21 | White: "white", 22 | Black: "black" 23 | }; 24 | const defaultValue = "inherit"; 25 | const groupId = "GROUP-ID1"; 26 | 27 | const labelS = "Size"; 28 | const optionsS = { 29 | Default: "inherit", 30 | Small: "small", 31 | Large: "large" 32 | }; 33 | const defaultValueS = "inherit"; 34 | 35 | export const Default = () => ( 36 | 41 | ); 42 | 43 | export const IconButton = () => ( 44 |
45 | 46 |
47 | ); 48 | -------------------------------------------------------------------------------- /pharmd-app/src/components/Fields/QuickInfoField.js: -------------------------------------------------------------------------------- 1 | // -------------------------- IMPORTS -------------------------- 2 | 3 | // Function Imports 4 | import React from "react"; 5 | import { Loading, useGetOne } from "react-admin"; 6 | import { formatDecimal } from "../../services/Utility"; 7 | 8 | // Component Imports 9 | import { styled } from "twin.macro"; 10 | import QuickInfo from "../Basic/QuickInfo"; 11 | 12 | // Style Imports 13 | 14 | // -------------------------- STYLE -------------------------- 15 | 16 | const Info = styled.div` 17 | display: inline-block; 18 | margin-left: auto; 19 | margin-right: auto; 20 | position: relative; 21 | text-align: center; 22 | width: 100%; 23 | `; 24 | 25 | // -------------------------- COMPONENT -------------------------- 26 | 27 | const QuickInfoField = ({ record = {}, source }) => { 28 | const id = record[source]; 29 | const { data, loading, error } = useGetOne("students", id); 30 | if (loading) { 31 | return ; 32 | } 33 | if (error) { 34 | return

Error, id:{id} {' '} is not found

; 35 | } 36 | return ( 37 | 38 | 39 | 40 | 41 | ); 42 | }; 43 | 44 | export default QuickInfoField; 45 | -------------------------------------------------------------------------------- /pharmd-app/src/components/Fields/NoteListField.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Error, Loading, useQuery } from "react-admin"; 3 | import tw, { styled } from "twin.macro"; 4 | import NoteField from "./NoteField"; 5 | 6 | const Label = styled.h1` 7 | ${tw`fontStyle-6 font-medium inline-flex`} 8 | font-size: 1.3em; 9 | color: black; 10 | font-weight: bold; 11 | `; 12 | 13 | const Notes = styled.div` 14 | margin-top: 12px; 15 | `; 16 | 17 | const NoteListField = ({ record = {}, source }) => { 18 | const { data, loading, error } = useQuery({ 19 | type: "getManyReference", 20 | resource: "notes", 21 | payload: { 22 | target: "student", 23 | id: record[source], 24 | pagination: { 25 | page: 1, 26 | perPage: 10 // TODO: how many per page? 27 | }, 28 | sort: { 29 | field: "", // TODO: Backend doesn't currently handle sorting for notes 30 | order: "" 31 | } 32 | } 33 | }); 34 | 35 | if (loading) return ; 36 | if (error) return ; 37 | if (!data) return null; 38 | 39 | return ( 40 | 41 | 42 | {data.map(note => { 43 | return ; 44 | })} 45 | 46 | ); 47 | }; 48 | 49 | export default NoteListField; 50 | -------------------------------------------------------------------------------- /pharmd-app/src/components/Basic/ExpansionPanel.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import tw, { styled } from "twin.macro"; 3 | import ExpansionPanelMaterial from "@material-ui/core/ExpansionPanel"; 4 | import ExpansionPanelSummaryMaterial from "@material-ui/core/ExpansionPanelSummary"; 5 | import ExpansionPanelDetailsMaterial from "@material-ui/core/ExpansionPanelDetails"; 6 | 7 | const ExpansionPanelC = styled(ExpansionPanelMaterial)` 8 | ::before { 9 | height: 0; 10 | } 11 | `; 12 | 13 | const ExpansionPanelSummary = styled(ExpansionPanelSummaryMaterial)` 14 | ${tw`p-0 m-0`} 15 | 16 | .MuiExpansionPanelSummary-content { 17 | ${tw`m-0`} 18 | } 19 | `; 20 | 21 | const ExpansionPanelDetails = styled(ExpansionPanelDetailsMaterial)` 22 | ${tw`flex-col py-0`} 23 | `; 24 | 25 | const ExpansionPanel = ({ SummaryChild, DetailChild, defaultExpanded, expanded, onChange }) => { 26 | return ( 27 | 35 | {SummaryChild} 36 | {DetailChild} 37 | 38 | ); 39 | }; 40 | 41 | export default ExpansionPanel; 42 | -------------------------------------------------------------------------------- /pharmd-app/src/screens/profile/ProfileShow.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import { BooleanInput, Show, SimpleForm, SimpleShowLayout, TextInput } from "react-admin"; 3 | import axios from "axios"; 4 | import ProfilePage from "./ProfilePage"; 5 | import { BACKEND_USERS } from "../../config/backendRoutes"; 6 | 7 | const ProfileShowView = props => ( 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | ); 20 | 21 | const ProfileShow = props => { 22 | const [profile, setProfile] = useState(null); 23 | 24 | useEffect(() => { 25 | const info = JSON.parse(localStorage.getItem("userInfo")); 26 | const url = `${BACKEND_USERS}${info.userID}`; 27 | axios 28 | .get(url) 29 | .then(res => {}) 30 | .catch(err => {}); 31 | }, []); 32 | return ( 33 | 34 | 35 | 36 | ); 37 | }; 38 | 39 | export default ProfileShow; 40 | -------------------------------------------------------------------------------- /pharmd-app/src/screens/courses/CourseFilter.js: -------------------------------------------------------------------------------- 1 | // -------------------------- IMPORTS -------------------------- 2 | 3 | // Function Imports 4 | import React from "react"; 5 | import set from "lodash/set"; 6 | // Component Imports 7 | import { Filter } from "react-admin"; 8 | import StudentSearchInput from "../../components/Inputs/StudentSearchInput"; 9 | import { COURSE } from "../../constants/apiObjects"; 10 | 11 | // -------------------------- CONSTANTS -------------------------- 12 | 13 | const COURSE_SUBJECT_QUERY = `${COURSE.SUBJECT}_like`; 14 | const COURSE_NUMBER_QUERY = `${COURSE.NUMBER}_like`; 15 | const COURSE_NAME_QUERY = `${COURSE.NAME}_like`; 16 | 17 | // -------------------------- COMPONENT -------------------------- 18 | 19 | export const CourseFilter = props => { 20 | // Adds given filter key and value to the redux state filters 21 | const setFilter = (key, val) => { 22 | props.setFilters(set(props.filterValues, key, val)); 23 | }; 24 | 25 | const searchCourse = event => { 26 | // TODO: implement search here 27 | }; 28 | 29 | return ( 30 | 31 | `^${inputValue}`} // Regex parameter for start with 35 | onChange={searchCourse} 36 | alwaysOn 37 | /> 38 | 39 | ); 40 | }; 41 | -------------------------------------------------------------------------------- /pharmd-app/src/screens/studentDetails/StudentDetailsScreen.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import tw, { styled } from "twin.macro"; 4 | import Paper from "@material-ui/core/Paper"; 5 | 6 | import { Loading, useGetOne } from "react-admin"; 7 | import AppBar from "../../components/Nav/AppBar"; 8 | 9 | import StudentDetailsSide from "./StudentDetailsSide"; 10 | import StudentDetailsContentGrid from "./StudentDetailsContentGrid"; 11 | 12 | const MainContent = styled.div` 13 | ${tw`p-12 pt-2 `} 14 | margin-right: 24.75em; 15 | flex-grow: 1; 16 | `; 17 | 18 | const SideContent = styled(Paper)` 19 | position: fixed; 20 | right: 0; 21 | &.MuiPaper-root { 22 | ${tw`w-99`} 23 | height: 100vh; 24 | } 25 | `; 26 | 27 | const StudentDetailsScreen = props => { 28 | const { data, loading, error } = useGetOne("students", props.match.params.id); 29 | if (loading) { 30 | return ; 31 | } 32 | if (error) { 33 | return

Error, id:{id} is not found

; 34 | } 35 | return ( 36 | <> 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | ); 47 | }; 48 | 49 | export default StudentDetailsScreen; 50 | -------------------------------------------------------------------------------- /pharmd-app/src/components/Fields/ScoredListField/ScoredListItemSkeleton.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import tw, { styled } from "twin.macro"; 3 | import Skeleton from "@material-ui/lab/Skeleton"; 4 | import ListItem from "@material-ui/core/ListItem"; 5 | import ListItemIcon from "@material-ui/core/ListItemIcon"; 6 | import ListItemText from "@material-ui/core/ListItemText"; 7 | import PropTypes from "prop-types"; 8 | import ListItemSecondaryActionMaterial from "@material-ui/core/ListItemSecondaryAction"; 9 | 10 | const ListItemSecondaryAction = styled(ListItemSecondaryActionMaterial)` 11 | ${tw`right-1`} 12 | `; 13 | 14 | const ScoredListItemSkeleton = ({ variant = "primary", ...rest }) => { 15 | return ( 16 | 17 | 18 | 19 | 20 | } 22 | secondary={variant === "secondary" ? : null} 23 | /> 24 | 25 | 26 | 27 | 28 | ); 29 | }; 30 | 31 | ScoredListItemSkeleton.propTypes = { 32 | variant: PropTypes.oneOf(["primary", "secondary"]).isRequired 33 | }; 34 | 35 | export default ScoredListItemSkeleton; 36 | -------------------------------------------------------------------------------- /pharmd-app/src/components/Basic/Radio Controls/RadioButtonGroup.js: -------------------------------------------------------------------------------- 1 | import React, { cloneElement } from "react"; 2 | 3 | import RadioGroup from "@material-ui/core/RadioGroup"; 4 | import FormControl from "@material-ui/core/FormControl"; 5 | import FormLabel from "@material-ui/core/FormLabel"; 6 | import RadioButton from "./RadioButton"; 7 | 8 | const RadioButtonsGroup = props => { 9 | const { onChange, label, showLabel, error, children, className } = props; 10 | const [value, setValue] = React.useState("all"); 11 | 12 | const handleChange = event => { 13 | setValue(event.target.value); 14 | onChange(event, event.target.value); 15 | }; 16 | 17 | return ( 18 | 19 | {showLabel && {label}} 20 | 26 | {children && 27 | children.map((child, index) => 28 | cloneElement(child, { 29 | key: index, 30 | control: 31 | }) 32 | )} 33 | 34 | 35 | ); 36 | }; 37 | 38 | RadioButtonsGroup.defaultProps = { 39 | showLabel: true 40 | }; 41 | 42 | export default RadioButtonsGroup; 43 | -------------------------------------------------------------------------------- /pharmd-app/src/components/Basic/GridCard.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Description: 3 | * This component is a grid ccaard used for layouts 4 | * TODO: 5 | * - Add heigh adjustment 6 | * Date: 04-23-2020 7 | */ 8 | 9 | // -------------------------- IMPORTS -------------------------- 10 | 11 | // Function Imports 12 | import React from "react"; 13 | import PropTypes from "prop-types"; 14 | // Component Imports 15 | import MuiPaper from "@material-ui/core/Paper"; 16 | import MuiGrid from "@material-ui/core/Grid"; 17 | // Style Imports 18 | import tw, { styled } from "twin.macro"; 19 | 20 | // -------------------------- STYLE -------------------------- 21 | 22 | const GridItem = styled(MuiGrid)` 23 | ${tw`transition-1`} 24 | `; 25 | 26 | const Paper = styled(MuiPaper)` 27 | ${tw`rounded-xl h-64 w-64 shadow-cardLight`} 28 | `; 29 | 30 | // -------------------------- COMPONENT -------------------------- 31 | 32 | const GridCard = (xs, mdFull, mdShrink, isShrunk, { ...props }) => ( 33 | 34 | {props.children} 35 | 36 | ); 37 | 38 | GridCard.defaultProps = { 39 | xs: 12, 40 | mdFull: 3, 41 | mdShrink: 4, 42 | isShrunk: false 43 | }; 44 | 45 | GridCard.propTypes = { 46 | xs: PropTypes.number, 47 | mdFull: PropTypes.number, 48 | mdShrink: PropTypes.number, 49 | isShrunk: PropTypes.bool 50 | }; 51 | 52 | export default GridCard; 53 | -------------------------------------------------------------------------------- /pharmd-app/src/components/Fields/ScoredListField/ScoredListItemError.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import tw, { styled } from "twin.macro"; 3 | import ListItem from "@material-ui/core/ListItem"; 4 | import ListItemIcon from "@material-ui/core/ListItemIcon"; 5 | import ListItemTextMaterial from "@material-ui/core/ListItemText"; 6 | import PropTypes from "prop-types"; 7 | import ListItemSecondaryActionMaterial from "@material-ui/core/ListItemSecondaryAction"; 8 | import StatusBullet from "../../Basic/StatusBullet"; 9 | 10 | const ListItemText = styled(ListItemTextMaterial)` 11 | & .MuiListItemText-primary { 12 | ${tw`fontStyle-4 leading-6 font-medium block truncate`} 13 | } 14 | 15 | & .MuiListItemText-secondary { 16 | ${tw`fontStyle-2 text-gray-600`} 17 | } 18 | `; 19 | 20 | const ListItemSecondaryAction = styled(ListItemSecondaryActionMaterial)` 21 | ${tw`fontStyle-4 tracking-wider font-bold text-gray-900 right-1`} 22 | `; 23 | 24 | const ScoredListItemError = ({ errorText, ...rest }) => { 25 | return ( 26 | 27 | 28 | 29 | 30 | 31 | 000 32 | 33 | ); 34 | }; 35 | 36 | ScoredListItemError.propTypes = { 37 | erorText: PropTypes.string 38 | }; 39 | 40 | export default ScoredListItemError; 41 | -------------------------------------------------------------------------------- /pharmd-app/src/components/Fields/CourseListField.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import tw, { styled } from "twin.macro"; 3 | import { useGetOne } from "react-admin"; 4 | import CourseField from "./CourseField"; 5 | 6 | const Label = styled.h1` 7 | ${tw`fontStyle-6 font-medium inline-flex`} 8 | font-size: 1.3em; 9 | color: black; 10 | font-weight: bold; 11 | `; 12 | 13 | const Sublabel = styled.h2` 14 | ${tw`fontStyle-6 text-sm inline-flex`} 15 | color: rgba(192, 192, 192, 100); 16 | font-weight: bold; 17 | padding-left: 0.4em; 18 | `; 19 | 20 | const CourseListField = ({ record = {}, source }) => { 21 | const courses = record[source]; 22 | 23 | return ( 24 |
25 |
26 | 27 | Spring 2020 28 |
29 | {courses.map((course, index) => { 30 | const { data, loading, error } = useGetOne("courses", course.course_id); 31 | if (loading) { 32 | return

loading

; 33 | } 34 | if (error) { 35 | console.log("ERROR", error); 36 | return

ERROR

; 37 | } 38 | if (index === courses.length - 1) { 39 | return ; 40 | } 41 | 42 | return ; 43 | })} 44 |
45 | ); 46 | }; 47 | 48 | export default CourseListField; 49 | -------------------------------------------------------------------------------- /pharmd-app/src/constants/apiObjects.js: -------------------------------------------------------------------------------- 1 | export const STUDENT = { 2 | NEU_ID: "id", 3 | NAME: "name", 4 | FIRST_NAME: "firstName", 5 | LAST_NAME: "lastName", 6 | COHORT: "gradDate", 7 | STATUS: "status", 8 | GPA: "gpa" 9 | }; 10 | 11 | export const STUDENT_STATUS = { 12 | ENROLLED: "ENROLLED", 13 | LEAVE: "LEAVE", 14 | DROP_BACK: "DROP_BACK", 15 | COOP: "COOP", 16 | GRADUATED: "GRADUATED" 17 | }; 18 | 19 | export const NOTE = { 20 | ID: "id", 21 | TITLE: "title", 22 | BODY: "body", 23 | TAGS: "tags", 24 | DATE: "date" 25 | }; 26 | 27 | export const COURSE = { 28 | SUBJECT: "subject", 29 | NUMBER: "number", 30 | NAME: "name" 31 | }; 32 | 33 | export const STUDENT_COURSE = { 34 | SEMESTER: "semester", 35 | YEAR: "year", 36 | PERCENTAGE: "percentage", 37 | LETTER_GRADE: "letterGrade", 38 | COURSE_SUBJECT: "course.subject", 39 | COURSE_NUMBER: "course.number", 40 | COURSE_NAME: "course.name" 41 | }; 42 | 43 | export const STUDENT_EXAM = { 44 | SEMESTER: "semester", 45 | YEAR: "year", 46 | PERCENTAGE: "percentage", 47 | LETTER_GRADE: "letterGrade", 48 | EXAM_NAME: "exam.name", 49 | COURSE_SUBJECT: "exam.course.subject", 50 | COURSE_NUMBER: "exam.course.number", 51 | COURSE_NAME: "exam.course.name" 52 | }; 53 | 54 | export const LETTER_GRADE = { 55 | A: "A", 56 | A_MINUS: "A-", 57 | B_PLUS: "B+", 58 | B: "B", 59 | B_MINUS: "B-", 60 | C_PLUS: "C+", 61 | C: "C", 62 | C_MINUS: "C-", 63 | D_PLUS: "D+", 64 | D: "D", 65 | D_MINUS: "D-", 66 | F: "F" 67 | }; 68 | -------------------------------------------------------------------------------- /pharmd-app/src/components/Inputs/StudentSearchInput.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Description: 3 | * Redux cccconnected Search Box Input. 4 | * To be used inside a component 5 | * TODO: 6 | * -- Use Basic TextField Instead 7 | * Date: 04-24-2020 8 | */ 9 | // -------------------------- IMPORTS -------------------------- 10 | 11 | // Function Imports 12 | import React from "react"; 13 | import { useInput } from "react-admin"; 14 | import PropTypes from "prop-types"; 15 | // Style Imports 16 | import tw, { styled } from "twin.macro"; 17 | // Component Imports 18 | import SearchInput from "./SearchInput"; 19 | 20 | // -------------------------- STYLE -------------------------- 21 | const SearchBox = styled(SearchInput)` 22 | ${tw`rounded-xl shadow-none p-2 bg-background`} 23 | input { 24 | ${tw`bg-background fontStyle-6`} 25 | } 26 | `; 27 | 28 | // -------------------------- COMPONENT -------------------------- 29 | const StudentSearchInput = props => { 30 | const { 31 | input: { name, onChange }, 32 | meta: { touched, error }, 33 | isRequired 34 | } = useInput(props); 35 | 36 | return ( 37 | 44 | ); 45 | }; 46 | 47 | StudentSearchInput.defaultProps = {}; 48 | 49 | StudentSearchInput.propTypes = { 50 | label: PropTypes.string.isRequired, 51 | onChange: PropTypes.func.isRequired 52 | }; 53 | 54 | export default StudentSearchInput; 55 | -------------------------------------------------------------------------------- /pharmd-app/src/App.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import "./styles/App.css"; 3 | import { Admin, Resource } from "react-admin"; 4 | import { ThemeConsumer, ThemeProvider } from "styled-components"; 5 | import students from "./screens/student/index.js"; 6 | import upload from "./screens/upload/index.js"; 7 | import profile from "./screens/profile/index.js"; 8 | import courses from "./screens/courses"; 9 | import Dashboard from "./Dashboard"; 10 | import { AuthProvider, DataProvider } from "./services"; 11 | import createLigthTheme from "./themes/light-theme"; 12 | import DashboardLayout from "./components/Layout/DashboardLayout"; 13 | import customRoutes from "./config/customRoutes"; 14 | 15 | const App = () => { 16 | return ( 17 | 18 | 19 | {theme => ( 20 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | )} 36 | 37 | 38 | ); 39 | }; 40 | 41 | export default App; 42 | -------------------------------------------------------------------------------- /pharmd-app/src/screens/student/StudentList.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Description: 3 | This component contains a DataGrid that takes in field components for children. 4 | The componnet manages dispatching the data (resource) to each field and the source (data key). 5 | */ 6 | 7 | // -------------------------- IMPORTS -------------------------- 8 | 9 | // Function Imports 10 | import React from "react"; 11 | 12 | 13 | // Component Imports 14 | import Table from "../../components/Table/Table"; 15 | import EmphasisField from "../../components/Fields/EmphasisField"; 16 | import TextField from "../../components/Fields/TextField"; 17 | import ChipField from "../../components/Fields/ChipField"; 18 | import GradeField from "../../components/Fields/GradeField"; 19 | import { STUDENT } from "../../constants/apiObjects"; 20 | 21 | const StudentList = ({ selectedRow, ...props }) => { 22 | 23 | const studentRowClick = (id, basePath, record) => { 24 | props.setStudentSidebar(true); 25 | props.setStudentQuickViewExpanded(true); 26 | return record.editable ? "edit" : "show"; 27 | }; 28 | 29 | return ( 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 |
38 | ); 39 | }; 40 | export default StudentList; 41 | -------------------------------------------------------------------------------- /pharmd-app/src/styles/App.css: -------------------------------------------------------------------------------- 1 | /* Material UI CSS Variables */ 2 | 3 | :root { 4 | --color-bg-primary: var(--background); 5 | --color-bg-card: var(--white); 6 | --color-bg-tooltip: var(--gray-3); 7 | --color-primary: var(--primary); 8 | --color-secondary: var(--secondary); 9 | --color-tertiary: var(--tertiary); 10 | 11 | --color-text-primary: var(--gray-1); 12 | --color-text-secondary: var(--gray-3); 13 | --color-text-tertiary: var(--gray-4); 14 | --color-text-emphasis-high: var(--primary); 15 | --color-text-emphasis-low: var(--secondary); 16 | --color-text-inverse: var(--white); 17 | 18 | --color-red: var(--red); 19 | --color-orange: var(--orange); 20 | --color-yellow: var(--yellow); 21 | --color-green: var(--green); 22 | } 23 | 24 | /*! purgecss start ignore */ 25 | .theme-dark { 26 | --color-bg-primary: var(--palette-text); 27 | --color-bg-secondary: #dcd0c0; 28 | --color-bg-default: #f4f4f4; 29 | --color-bg-inverse: #373737; 30 | 31 | --color-text-primary: #fff; 32 | --color-text-secondary: #927e7e; 33 | --color-text-default: #373737; 34 | --color-text-default-soft: #6a6a6a; 35 | --color-text-inverse: #fff; 36 | --color-text-inverse-soft: #bbb; 37 | 38 | --font-display: Georgia, Times, "Times New Roman", serif; 39 | --font-body: Corbel, "Lucida Grande", "Lucida Sans Unicode", "Lucida Sans", "DejaVu Sans", 40 | "Bitstream Vera Sans", "Liberation Sans", Verdana, "Verdana Ref", sans-serif; 41 | 42 | --font-weight-normal: 400; 43 | --font-weight-display: 400; 44 | --font-weight-btn: 600; 45 | 46 | --rounded-btn: 0; 47 | } 48 | -------------------------------------------------------------------------------- /pharmd-app/src/components/Layout/DashboardLayout.js: -------------------------------------------------------------------------------- 1 | // -------------------------- IMPORTS -------------------------- 2 | 3 | // Function Imports 4 | import React from "react"; 5 | 6 | // Style Imports 7 | import tw from "twin.macro"; 8 | import { makeStyles } from "@material-ui/core/styles"; 9 | import { Layout, Notification, Sidebar } from "react-admin"; 10 | import Menu from "./Menu"; 11 | 12 | const MyAppBar = props => <>; 13 | 14 | const useSidebarStyles = makeStyles({ 15 | drawerPaper: { 16 | backgroundColor: "white", 17 | height: "100%", 18 | width: "96px", 19 | marginTop: "0px", 20 | position: "fixed", 21 | boxShadow: 22 | "rgba(0, 0, 0, 0.05) 0px 1px 2px, rgba(0, 0, 0, 0.07) 0px 2px 13px, rgba(0, 0, 0, 0.03) 0px 3px 10px" 23 | } 24 | }); 25 | 26 | const useLayoutStyles = makeStyles({ 27 | appFrame: { 28 | marginTop: "0px", 29 | flexDirection: "row" 30 | }, 31 | contentWithSidebar: { 32 | display: "flex", 33 | flexGrow: 1 34 | }, 35 | content: { 36 | display: "flex", 37 | flexDirection: "row", 38 | padding: 0 39 | } 40 | }); 41 | 42 | const MySidebar = props => { 43 | const classes = useSidebarStyles(); 44 | return ; 45 | }; 46 | 47 | const DashboardLayout = props => { 48 | return ( 49 | 57 | ); 58 | }; 59 | 60 | export default DashboardLayout; 61 | -------------------------------------------------------------------------------- /pharmd-app/src/stories/tableCells.stories.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { select, text, withKnobs } from "@storybook/addon-knobs"; 3 | import { withDesign } from "storybook-addon-designs"; 4 | import EmphasisField from "../components/Fields/EmphasisField"; 5 | import ChipField from "../components/Fields/ChipField"; 6 | 7 | export default { 8 | title: "Table Cells", 9 | component: ChipField, 10 | decorators: [withKnobs, withDesign, storyFn =>
{storyFn()}
] 11 | }; 12 | 13 | const label = "Status"; 14 | const options = { 15 | Enrolled: "enrolled", 16 | Dropback: "dropback", 17 | Leave: "leave", 18 | Gradutated: "graduated", 19 | Coop: "coop" 20 | }; 21 | const defaultValue = "enrolled"; 22 | const groupId = "GROUP-ID1"; 23 | 24 | const labelText = "Cell Text"; 25 | const defaultLabel = "BOLD TEXT"; 26 | 27 | export const StatusChip = () => ( 28 | 32 | ); 33 | 34 | StatusChip.story = { 35 | parameters: { 36 | design: { 37 | type: "figma", 38 | url: 39 | "https://www.figma.com/file/QBWcVYJkF6K8o7WErRPsxd/Student-List-Mockups?node-id=806%3A0" 40 | } 41 | } 42 | }; 43 | 44 | export const EmphasisCell = () => ( 45 | 46 | ); 47 | 48 | EmphasisCell.story = { 49 | parameters: { 50 | design: { 51 | type: "figma", 52 | url: 53 | "https://www.figma.com/file/QBWcVYJkF6K8o7WErRPsxd/Student-List-Mockups?node-id=31%3A40" 54 | } 55 | } 56 | }; 57 | -------------------------------------------------------------------------------- /pharmd-app/src/components/Nav/AppBar.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Description: 3 | * This Component creates a transparent app bar used to display the 4 | * current user location. It also provides the ability to use 5 | * breadcrumbs to navigate to previus locations 6 | */ 7 | 8 | // -------------------------- IMPORTS -------------------------- 9 | 10 | // Function Imports 11 | import React from "react"; 12 | import PropTypes from "prop-types"; 13 | // Style Imports 14 | import tw, { styled } from "twin.macro"; 15 | // Component Imports 16 | import MuiAppBar from "@material-ui/core/AppBar"; 17 | import MuiToolbar from "@material-ui/core/Toolbar"; 18 | import RouterBreadcrumb from "../Basic/Breadcrumb"; 19 | 20 | // -------------------------- STYLE -------------------------- 21 | const AppBarStyled = styled(MuiAppBar)` 22 | ${tw`shadow-none h-24`} 23 | `; 24 | 25 | const Title = styled.p` 26 | ${tw`fontStyle-10 m-0 text-gray-700`} 27 | `; 28 | 29 | const Toolbar = styled(MuiToolbar)` 30 | ${tw`absolute bottom-0`} 31 | `; 32 | 33 | // -------------------------- COMPONENT -------------------------- 34 | const AppBar = ({ title, location, children, props }) => ( 35 | 36 | 37 |
38 | 39 | {title} 40 |
41 | {children} 42 |
43 |
44 | ); 45 | 46 | AppBar.defaultProps = {}; 47 | 48 | AppBar.propTypes = { 49 | title: PropTypes.string.isRequired, 50 | location: PropTypes.string, 51 | children: PropTypes.element 52 | }; 53 | 54 | export default AppBar; 55 | -------------------------------------------------------------------------------- /pharmd-app/webpack.config.js: -------------------------------------------------------------------------------- 1 | const HtmlWebPackPlugin = require("html-webpack-plugin"); 2 | const webpack = require("webpack"); 3 | const dotenv = require("dotenv"); 4 | 5 | module.exports = () => { 6 | const env = dotenv.config().parsed; 7 | 8 | // reduce it to an object 9 | const envKeys = Object.keys(env).reduce((prev, next) => { 10 | prev[`process.env.${next}`] = JSON.stringify(env[next]); 11 | return prev; 12 | }, {}); 13 | 14 | return { 15 | entry: "./src/index.js", 16 | resolve: { symlinks: false }, 17 | module: { 18 | rules: [ 19 | { 20 | test: /\.js?$/, 21 | exclude: /node_modules/, 22 | use: { 23 | loader: "babel-loader" 24 | } 25 | }, 26 | { 27 | test: /\.css$/i, 28 | use: ["style-loader", "css-loader"] 29 | }, 30 | { 31 | test: /\.svg$/, 32 | use: ["@svgr/webpack"] 33 | }, 34 | { 35 | test: /\.(jpg|png)$/, 36 | use: { 37 | loader: "url-loader" 38 | } 39 | } 40 | ] 41 | }, 42 | plugins: [ 43 | new HtmlWebPackPlugin({ 44 | template: "./src/index.html", 45 | filename: "./index.html" 46 | }), 47 | new webpack.DefinePlugin(envKeys) 48 | ], 49 | performance: { hints: false }, 50 | target: "web", 51 | node: { 52 | fs: "empty" 53 | }, 54 | devServer: { 55 | stats: "errors-only", 56 | open: true, 57 | hot: true, 58 | watchOptions: { 59 | poll: 500, 60 | ignored: ["node_modules/**", ".git/**"] 61 | } 62 | } 63 | }; 64 | }; 65 | -------------------------------------------------------------------------------- /pharmd-app/src/components/Fields/QuickProfileField.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Loading, TextField, useGetOne } from "react-admin"; 3 | 4 | import tw, { styled } from "twin.macro"; 5 | import AvatarField from "./AvatarField"; 6 | import QuickChipField from "./QuickChipField"; 7 | 8 | const Profile = styled.section` 9 | display: block; 10 | text-align: center; 11 | margin-left: auto; 12 | margin-right: auto; 13 | `; 14 | 15 | const NameField = styled(TextField)` 16 | display: block; 17 | font-size: 2em; 18 | font-weight: bold; 19 | color: #2b2b90; 20 | `; 21 | 22 | const IdField = styled(TextField)` 23 | display: block; 24 | font-size: 1em; 25 | color: ${props => props.theme.palette.text.secondary}; 26 | `; 27 | 28 | const StatusField = styled.span` 29 | ${tw` w-28 `} 30 | display: block; 31 | margin-right: auto; 32 | margin-bottom: 15px; 33 | `; 34 | 35 | const PictureField = styled(AvatarField)` 36 | display: block; 37 | `; 38 | 39 | const QuickProfileField = ({ record = {}, source }) => { 40 | const id = record[source]; 41 | const { data, loading, error } = useGetOne("students", id); 42 | if (loading) { 43 | return ; 44 | } 45 | if (error) { 46 | return

Error, id:{id} is not found

; 47 | } 48 | return ( 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | ); 59 | }; 60 | 61 | export default QuickProfileField; 62 | -------------------------------------------------------------------------------- /pharmd-app/src/services/Auth/login.js: -------------------------------------------------------------------------------- 1 | import decodeJwt from "jwt-decode"; 2 | import { FAKE_AUTH } from "../../config/backendRoutes"; 3 | 4 | // import AUTH_URL instead to use real pharmd data 5 | const AUTH_URL = FAKE_AUTH; 6 | 7 | /* 8 | react-admin enables a new page on the /login route, 9 | which displays a login form asking for a username and password. 10 | Upon submission, this form calls this authProvider.login({ login, password }) method. 11 | It’s the ideal place to authenticate the user, and store their credentials. 12 | */ 13 | 14 | /** 15 | * Called when the user submits the login form. A good place to authenticate 16 | * @param {string} username 17 | * @param {string} password 18 | * @return {Promise} 19 | */ 20 | export default ({ username, password }) => { 21 | const request = new Request(AUTH_URL, { 22 | method: "POST", 23 | body: JSON.stringify({ email: username, password }), 24 | headers: new Headers({ "Content-Type": "application/json" }) 25 | }); 26 | return fetch(request) 27 | .then(response => { 28 | if (response.status < 200 || response.status >= 300) { 29 | return Promise.reject(response.statusText); 30 | } 31 | return response.json(); 32 | }) 33 | .then(({ accessToken }) => { 34 | const decodedToken = decodeJwt(accessToken); 35 | localStorage.setItem("token", accessToken); 36 | localStorage.setItem( 37 | "userInfo", 38 | JSON.stringify({ 39 | firstName: decodedToken.firstName, 40 | lastName: decodedToken.lastName, 41 | userID: decodedToken.userID 42 | }) 43 | ); 44 | localStorage.setItem( 45 | "permissions", 46 | decodedToken.isAdmin || decodedToken.canWrite || decodedToken.email === "kevin@mail.com" 47 | ? "admin" 48 | : "user" 49 | ); 50 | }) 51 | .catch(err => {}); 52 | }; 53 | -------------------------------------------------------------------------------- /pharmd-app/src/components/Inputs/SearchInput.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Description: A basic no logicc search box component 3 | * TODO: 4 | * -- Abstract the component: Potentially make this searchbox into a generic text field that takes in an icon. 5 | * -- Use Tailwind classes 6 | * -- Create more generic search fields 7 | * Date: 04-24-2020 8 | */ 9 | 10 | // -------------------------- IMPORTS -------------------------- 11 | 12 | // Function Imports 13 | import React from "react"; 14 | import PropTypes from "prop-types"; 15 | import clsx from "clsx"; 16 | // Style Imports 17 | import { styled } from "twin.macro"; 18 | // Component Imports 19 | import MuiInput from "@material-ui/core/Input"; 20 | import MuiPaper from "@material-ui/core/Paper"; 21 | import MuiSearchIcon from "@material-ui/icons/Search"; 22 | 23 | // -------------------------- STYLE -------------------------- 24 | const Paper = styled(MuiPaper)` 25 | border-radius: 4px; 26 | align-items: center; 27 | padding: ${props => `${props.theme.spacing(1)}px`}; 28 | display: flex; 29 | flex-basis: 420; 30 | `; 31 | 32 | const Input = styled(MuiInput)` 33 | flex-grow: 1; 34 | font-size: 14px; 35 | line-height: 16px; 36 | letter-spacing: -0.05px; 37 | `; 38 | 39 | const SearchIcon = styled(MuiSearchIcon)` 40 | margin-right: ${props => `${props.theme.spacing(1)}px`}; 41 | color: ${props => props.theme.palette.text.secondary}; 42 | `; 43 | 44 | // -------------------------- COMPONENT -------------------------- 45 | const SearchInput = props => { 46 | const { className, onChange, style, ...rest } = props; 47 | 48 | return ( 49 | 50 | 51 | 52 | 53 | ); 54 | }; 55 | 56 | SearchInput.defaultProps = {}; 57 | 58 | SearchInput.propTypes = { 59 | className: PropTypes.string, 60 | onChange: PropTypes.func, 61 | style: PropTypes.object 62 | }; 63 | 64 | export default SearchInput; 65 | -------------------------------------------------------------------------------- /pharmd-app/src/screens/upload/components/UploadFileChooser.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from "react"; 2 | import FileUploadService from "../../../services/FileUploadService"; 3 | import Button from '../../../components/Form/Button'; 4 | import FileSelector from '../../../components/Form/FileSelector'; 5 | import PropTypes from 'prop-types'; 6 | 7 | /** 8 | * @class UploadFileChooser a component for choosing a file to upload 9 | */ 10 | class UploadFileChooser extends Component { 11 | static propTypes = { 12 | uploadedFileData: PropTypes.func 13 | }; 14 | 15 | static defaultProps = { 16 | uploadedFileData() {} 17 | }; 18 | 19 | constructor(props) { 20 | super(props); 21 | this.state = { 22 | selectedFile: null, 23 | uploadedFile: {}, 24 | fileName: "", 25 | uploadMessage: "", 26 | uploadProgress: 0 27 | }; 28 | 29 | this.chooseFileHandler = this.chooseFileHandler.bind(this); 30 | this.uploadFileHandler = this.uploadFileHandler.bind(this); 31 | } 32 | 33 | /** 34 | * Executes when a file is chosen by the user 35 | * @param {File} file 36 | */ 37 | chooseFileHandler(file) { 38 | this.setState({ 39 | selectedFile: file, 40 | fileName: file.name 41 | }); 42 | } 43 | 44 | async uploadFileHandler(event) { 45 | event.preventDefault(); 46 | const spreadsheetData = await FileUploadService.loadSpreadsheet(this.state.selectedFile); 47 | this.props.uploadedFileData(spreadsheetData); 48 | } 49 | 50 | render() { 51 | return ( 52 |
53 | 58 |
59 | 60 | {this.state.selectedFile && 61 | 64 | } 65 |
66 | ); 67 | } 68 | } 69 | 70 | export default UploadFileChooser; 71 | -------------------------------------------------------------------------------- /pharmd-app/src/components/Basic/StatusBullet.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import clsx from "clsx"; 4 | import { makeStyles } from "@material-ui/styles"; 5 | 6 | const useStyles = makeStyles(theme => ({ 7 | root: { 8 | display: "inline-block", 9 | borderRadius: "50%", 10 | flexGrow: 0, 11 | flexShrink: 0 12 | }, 13 | sm: { 14 | height: theme.spacing(1), 15 | width: theme.spacing(1) 16 | }, 17 | md: { 18 | height: theme.spacing(2), 19 | width: theme.spacing(2) 20 | }, 21 | lg: { 22 | height: theme.spacing(3), 23 | width: theme.spacing(3) 24 | }, 25 | neutral: { 26 | backgroundColor: theme.palette.neutral.main 27 | }, 28 | primary: { 29 | backgroundColor: theme.palette.primary.main 30 | }, 31 | info: { 32 | backgroundColor: theme.palette.info.main 33 | }, 34 | warning: { 35 | backgroundColor: theme.palette.warning.main 36 | }, 37 | danger: { 38 | backgroundColor: theme.palette.error.main 39 | }, 40 | good: { 41 | backgroundColor: theme.palette.good.main 42 | }, 43 | success: { 44 | backgroundColor: theme.palette.success.main 45 | } 46 | })); 47 | 48 | const StatusBullet = props => { 49 | const { className, size, color, ...rest } = props; 50 | 51 | const classes = useStyles(); 52 | 53 | return ( 54 | 65 | ); 66 | }; 67 | 68 | StatusBullet.propTypes = { 69 | className: PropTypes.string, 70 | color: PropTypes.oneOf([ 71 | "neutral", 72 | "primary", 73 | "info", 74 | "good", 75 | "success", 76 | "warning", 77 | "danger" 78 | ]), 79 | size: PropTypes.oneOf(["sm", "md", "lg"]) 80 | }; 81 | 82 | StatusBullet.defaultProps = { 83 | size: "md", 84 | color: "default" 85 | }; 86 | 87 | export default StatusBullet; 88 | -------------------------------------------------------------------------------- /pharmd-app/src/assets/icons/student.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /pharmd-app/src/screens/student/StudentToolbarFilter.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Description: 3 | * Component manaages table toolbar filters. 4 | * Currently we only display a seaarh filter. 5 | * TODO: 6 | * - Add more filters if needed: A filter can be added 7 | * as a child of and might require a custom 8 | * onChange and parse filter function to communicate with the api. 9 | * Date: 04-24-2020 10 | */ 11 | // -------------------------- IMPORTS -------------------------- 12 | 13 | // Function Imports 14 | import React from "react"; 15 | import set from "lodash/set"; 16 | // Component Imports 17 | import { Filter } from "react-admin"; 18 | import StudentSearchInput from "../../components/Inputs/StudentSearchInput"; 19 | import { STUDENT } from "../../constants/apiObjects"; 20 | 21 | const STUDENT_NAME_QUERY = `${STUDENT.NAME}_like`; 22 | const STUDENT_ID_QUERY = `${STUDENT.NEU_ID}_like`; 23 | 24 | // -------------------------- COMPONENT -------------------------- 25 | 26 | export const StudentFilter = props => { 27 | // Adds given filter key and value to the redux state filters 28 | const setFilter = (key, val) => { 29 | props.setFilters(set(props.filterValues, key, val)); 30 | }; 31 | 32 | // Fuunction that manages how a student is searched based on input event 33 | const searchStudent = event => { 34 | const val = event.target.value; 35 | if (val === "") { 36 | delete props.filterValues[STUDENT_NAME_QUERY]; 37 | delete props.filterValues[STUDENT_ID_QUERY]; 38 | props.setFilters(props.filterValues); 39 | } else { 40 | setFilter( 41 | isNaN(val) ? STUDENT_NAME_QUERY : STUDENT_ID_QUERY, 42 | isNaN(val) ? val : `^${val}` 43 | ); 44 | } 45 | }; 46 | 47 | return ( 48 | 49 | `^${inputValue}`} // Regex parameter for start with 53 | onChange={searchStudent} 54 | alwaysOn 55 | /> 56 | 57 | ); 58 | }; 59 | -------------------------------------------------------------------------------- /pharmd-app/src/components/Layout/Menu.js: -------------------------------------------------------------------------------- 1 | import React, { Fragment } from "react"; 2 | import { useSelector } from "react-redux"; 3 | import { getResources, usePermissions } from "react-admin"; 4 | import { withRouter } from "react-router-dom"; 5 | import Divider from "@material-ui/core/Divider"; 6 | 7 | import NavItemLink from "../Nav/NavItemLink"; 8 | import NavIndicator from "../Nav/NavIndicator"; 9 | 10 | const Menu = ({ onMenuClick, logout, ...props }) => { 11 | const location = props.location.pathname; 12 | const { permissions } = usePermissions(); 13 | const hiddenResources = ["studentCourses", "studentExams"]; 14 | let resources = useSelector(getResources).filter(val => !hiddenResources.includes(val.name)); 15 | if (permissions === "user") { 16 | resources = resources.filter(val => val.name !== "upload"); 17 | } 18 | const flat = resources.reduce((total, val) => total.concat(val.name), []); 19 | 20 | const getCurrentIndex = () => { 21 | // TEMPORARY SOLUTION WILL FIND A BETTER ONE THAT RELOADS ONLY IF NEEDE 22 | const currentIndex = flat.indexOf(location.split("/")[1]); 23 | return currentIndex; 24 | }; 25 | 26 | return ( 27 |
28 | {resources.map((resource, index) => { 29 | const isActive = location.includes(resource.name); 30 | const linkProps = { 31 | key: resource.name || index, 32 | to: `/${resource.name}`, 33 | title: (resource.options && resource.options.label) || resource.name, 34 | iconSrc: resource.icon, 35 | onClick: onMenuClick, 36 | sidebarIsOpen: false, 37 | isActive 38 | }; 39 | if (index === 0) { 40 | return ( 41 | 42 | 43 | 44 | 45 | ); 46 | } 47 | return ; 48 | })} 49 | 50 | 51 | {logout} 52 |
53 | ); 54 | }; 55 | 56 | export default withRouter(Menu); 57 | -------------------------------------------------------------------------------- /pharmd-app/src/components/Form/Select.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import {FormControl, InputLabel, MenuItem, Select} from '@material-ui/core'; 3 | import PropTypes from 'prop-types'; 4 | import tw, {styled} from "twin.macro"; 5 | 6 | const StyledFormControl = styled(FormControl)` 7 | ${tw`m-4`}; 8 | min-width: 12rem; 9 | `; 10 | 11 | /** 12 | * @class CustomSelect a component for creating an easy select menu 13 | */ 14 | class CustomSelect extends React.Component { 15 | static propTypes = { 16 | title: PropTypes.string, 17 | onChange: PropTypes.func, 18 | selectItems: PropTypes.arrayOf(PropTypes.shape({ 19 | value: PropTypes.any, 20 | displayValue: PropTypes.string 21 | })) 22 | }; 23 | 24 | static defaultProps = { 25 | title: '', 26 | onChange() {}, 27 | selectItems: [] 28 | }; 29 | 30 | constructor(props) { 31 | super(props); 32 | this.state = { 33 | currentValue: '' 34 | }; 35 | this.handleOnChange = this.handleOnChange.bind(this); 36 | } 37 | 38 | handleOnChange($event) { 39 | const newVal = $event.target.value; 40 | this.setState({ 41 | currentValue: newVal 42 | }); 43 | this.props.onChange(newVal); 44 | } 45 | 46 | 47 | render() { 48 | return ( 49 | 50 | {this.props.title} 51 | 62 | 63 | ); 64 | } 65 | 66 | } 67 | 68 | export default CustomSelect; 69 | -------------------------------------------------------------------------------- /pharmd-app/src/components/Fields/NoteField.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import tw, { styled } from "twin.macro"; 3 | import AccessTimeIcon from "@material-ui/icons/AccessTimeOutlined"; 4 | import EditIcon from "@material-ui/icons/EditOutlined"; 5 | import NoteIcon from "../Basic/NoteIcon"; 6 | import { NOTE } from "../../constants/apiObjects"; 7 | 8 | const Info = styled.div` 9 | ${tw`fontStyle-6 text-black font-medium`} 10 | width: 100%; 11 | padding-bottom: 10px; 12 | font-size: 0.89em; 13 | display: grid; 14 | grid-template-columns: auto; 15 | `; 16 | 17 | const Heading = styled.div` 18 | margin-top: 1em; 19 | align-items: center; 20 | display: inline-flex; 21 | height: auto; 22 | `; 23 | 24 | const Time = styled.div` 25 | align-items: center; 26 | display: inline-flex; 27 | height: auto; 28 | `; 29 | 30 | const Title = styled.h3` 31 | margin: 0px; 32 | font-size: 1.2em; 33 | height: auto; 34 | `; 35 | 36 | const DateLabel = styled.h4` 37 | font-weight: normal; 38 | font-size: 0.85em; 39 | margin: 0px 0px 0px 0.5em; 40 | color: ${props => props.theme.palette.tertiary.main}; 41 | `; 42 | 43 | const Content = styled.div` 44 | height: 3em; 45 | line-height: 1em; 46 | overflow: hidden; 47 | margin-top: 0.4em; 48 | word-break: break-all; 49 | `; 50 | 51 | const NoteField = ({ record, source }) => { 52 | const title = record[NOTE.TITLE]; 53 | const body = record[NOTE.BODY]; 54 | const date = new Date(record[NOTE.DATE]).toLocaleString(); 55 | 56 | return ( 57 | 58 | 59 | {title} 60 | 61 | 62 | 66 | {/* if the length of string is more than 2 lines - ask jose how to check for this */} 67 | 68 |

{body}

69 |
70 | {/* add a contional where if 3 or more... */} 71 | {/* use QuickChipField for the chip component */} 72 |
73 | ); 74 | }; 75 | 76 | export default NoteField; 77 | -------------------------------------------------------------------------------- /pharmd-app/src/themes/light-theme.js: -------------------------------------------------------------------------------- 1 | import { createMuiTheme } from "@material-ui/core/styles"; 2 | 3 | const STYLE = getComputedStyle(document.body); 4 | function getValue(cssVariable) { 5 | return STYLE.getPropertyValue(cssVariable).trim(); 6 | } 7 | 8 | export const FIELD_COLOR_NAMES = { 9 | PRIMARY: "PRIMARY", 10 | GREEN: "GREEN", 11 | RED: "RED", 12 | ORANGE: "ORANGE", 13 | TERTIARY: "TERTIARY", 14 | DEFAULT: "DEFAULT" 15 | }; 16 | 17 | export const FIELD_COLOR_VALUES = { 18 | [FIELD_COLOR_NAMES.PRIMARY]: getValue("--primary"), 19 | [FIELD_COLOR_NAMES.GREEN]: getValue("--green-2"), 20 | [FIELD_COLOR_NAMES.RED]: getValue("--red"), 21 | [FIELD_COLOR_NAMES.ORANGE]: getValue("--orange"), 22 | [FIELD_COLOR_NAMES.TERTIARY]: getValue("--tertiary"), 23 | [FIELD_COLOR_NAMES.DEFAULT]: getValue("--gray-3") 24 | }; 25 | 26 | function createLightTheme() { 27 | const overrides = { 28 | name: "Light Theme", 29 | palette: { 30 | fieldColors: { 31 | ...FIELD_COLOR_VALUES 32 | }, 33 | primary: { 34 | main: getValue("--primary"), 35 | contrastText: getValue("--white") 36 | }, 37 | secondary: { 38 | main: getValue("--secondary"), 39 | contrastText: getValue("--white") 40 | }, 41 | tertiary: { 42 | main: getValue("--tertiary"), 43 | contrastText: getValue("--white") 44 | }, 45 | error: { 46 | main: getValue("--red") 47 | }, 48 | warning: { 49 | main: getValue("--orange") 50 | }, 51 | info: { 52 | main: getValue("--primary") 53 | }, 54 | good: { 55 | main: getValue("--yellow") 56 | }, 57 | success: { 58 | main: getValue("--green-2") 59 | }, 60 | neutral: { 61 | main: getValue("--gray-3") 62 | }, 63 | text: { 64 | primary: getValue("--gray-1"), 65 | secondary: getValue("--gray-3"), 66 | hint: getValue("--gray-4") 67 | }, 68 | background: { 69 | paper: getValue("--white"), 70 | default: getValue("--background") 71 | } 72 | } 73 | }; 74 | return createMuiTheme(overrides); 75 | } 76 | 77 | export default createLightTheme; 78 | -------------------------------------------------------------------------------- /pharmd-app/src/assets/icons/person.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /pharmd-app/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 16 | 20 | 21 | 25 | 26 | 35 | React App 36 | 37 | 38 | 39 |
40 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /pharmd-app/src/components/Basic/Checkbox Controls/CheckboxFilterButtonGroup.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Description: 3 | * This Component creates a group of filter checkbox buttons. 4 | * Has the same functionality as CheckboxButtonGroup component, but checkboxes apply filters 5 | * when checked. 6 | * 7 | * Date: 02-18-2021 8 | */ 9 | 10 | // -------------------------- IMPORTS -------------------------- 11 | 12 | // Function Imports 13 | import React from "react"; 14 | 15 | // Component Imports 16 | import CheckboxButtonGroup from "./CheckboxButtonGroup"; 17 | 18 | // -------------------------- COMPONENT -------------------------- 19 | const CheckboxFilterButtonGroup = props => { 20 | const { 21 | checkboxCheckedClass, 22 | checkboxClassName, 23 | children, 24 | className, 25 | color, 26 | error, 27 | formGroupClassName, 28 | label, 29 | onChange, 30 | showLabel, 31 | } = props; 32 | 33 | const [filters, setFilters] = React.useState([]); 34 | 35 | // Keeps tracks of filters that have been applied and gives onChange function an updated array 36 | // of filters along with the event (when the checkbox was checked/unchecked). 37 | const handleChange = event => { 38 | const filter = event.target.value; 39 | const filterIndex = filters.indexOf(filter); 40 | if (event.target.checked) { 41 | if (filterIndex < 0) { 42 | // If the checkbox was checked and the value is not in our array of filter values then add the value to the array 43 | filters.push(filter); 44 | } 45 | } else if (filterIndex >= 0) { 46 | // If the checkbox was unchecked and the value is in our array of filter values then remove the value from the array 47 | filters.splice(filterIndex, 1); 48 | } 49 | // Finally, update the filter values and call the onChange function with the new list 50 | setFilters(filters); 51 | onChange(event, filters); 52 | }; 53 | 54 | return ( 55 | 67 | ); 68 | }; 69 | 70 | export default CheckboxFilterButtonGroup; 71 | -------------------------------------------------------------------------------- /pharmd-app/src/screens/student/StudentContentGrid.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Description: 3 | * This component determines the main layout of the StudentDetails Screen 4 | * TODO: 5 | * - Add detail cards 6 | * Date: 04-23-2020 7 | */ 8 | 9 | // -------------------------- IMPORTS -------------------------- 10 | 11 | // Function Imports 12 | import React from "react"; 13 | import { useSelector } from "react-redux"; 14 | import PropTypes from "prop-types"; 15 | // Component Imports 16 | import { List as RaList } from "react-admin"; 17 | import MuiGrid from "@material-ui/core/Grid"; 18 | // Style Imports 19 | import tw, { styled } from "twin.macro"; 20 | import { StudentFilter } from "./StudentToolbarFilter"; 21 | import StudentList from "./StudentList"; 22 | import GridCard from "../../components/Basic/GridCard"; 23 | import StudenttDrawer from "./StudentDrawer"; 24 | 25 | // -------------------------- STYLE -------------------------- 26 | const List = styled(RaList)` 27 | ${tw`p-6 rounded-xl bg-white shadow-cardLight`} 28 | 29 | .content { 30 | ${tw`shadow-none`} 31 | } 32 | `; 33 | 34 | const MainGrid = styled(MuiGrid)` 35 | ${tw`pt-12 `} 36 | `; 37 | 38 | // -------------------------- COMPONENT -------------------------- 39 | const StudentContentGrid = ({ selected, ...props }) => { 40 | const isOpen = props.StudentSidebar; 41 | 42 | return ( 43 | 44 | 45 | {[...Array(3)].map((e, i) => ( 46 | 47 | {`GridItem-${i}`} 48 | 49 | ))} 50 | 51 | 52 | } 54 | bulkActionButtons={false} 55 | {...props} 56 | classes={{ content: "content" }} 57 | > 58 | 65 | 66 | 67 | 68 | ); 69 | }; 70 | 71 | StudentContentGrid.defaultProps = {}; 72 | 73 | StudentContentGrid.propTypes = { 74 | selected: PropTypes.any 75 | }; 76 | 77 | export default StudentContentGrid; 78 | -------------------------------------------------------------------------------- /pharmd-app/src/screens/student/StudentResource.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Description: 3 | * TODO:\Creates the main students screen 4 | * FIXME: Some Boolean logic error doesn't allow the quickview 5 | * sidebar to open when a table is clicked for the first time 6 | * Date: 04-23-2020$ 7 | */ 8 | // -------------------------- IMPORTS -------------------------- 9 | 10 | // Function Imports 11 | import React, { useCallback, useState } from "react"; 12 | import { Route, useHistory } from "react-router-dom"; 13 | // import { useDispatch } from "react-redux"; 14 | import tw, { styled } from "twin.macro"; 15 | // import { setStudentSideBar } from "../../redux/actions"; 16 | import { STUDENT_QUICKVIEW, STUDENTS_MAIN } from "../../constants/routes"; 17 | // Component Imports 18 | import AppBar from "../../components/Nav/AppBar"; 19 | import StudentContentGrid from "./StudentContentGrid"; 20 | import StudentDrawer from "./StudentDrawer"; 21 | 22 | // -------------------------- STYLE -------------------------- 23 | const MainContent = styled.div` 24 | ${tw`p-12 pt-2 flex-grow`} 25 | `; 26 | 27 | // -------------------------- COMPONENT -------------------------- 28 | const StudentScreen = props => { 29 | 30 | const [studentSidebar, setStudentSidebar] = useState(true); 31 | const [studentQuickViewExpanded, setStudentQuickViewExpanded] = useState(false); 32 | 33 | return ( 34 | 35 | {({ match }) => { 36 | const isMatch = match && match.params && match.params.id !== "create"; // has the user selected a new student 37 | 38 | return ( 39 | <> 40 | 41 | 42 | 51 | 52 | 53 | 62 | 63 | ); 64 | }} 65 | 66 | ); 67 | }; 68 | 69 | export default StudentScreen; 70 | -------------------------------------------------------------------------------- /pharmd-app/src/stories/table.stories.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Table from "@material-ui/core/Table"; 3 | import TableBody from "@material-ui/core/TableBody"; 4 | import TableHead from "@material-ui/core/TableHead"; 5 | import { TextField } from "react-admin"; 6 | import { withDesign } from "storybook-addon-designs"; 7 | import { FieldTitle } from "ra-core"; 8 | import TableRow from "../components/Table/TableRow"; 9 | 10 | import EmphasisField from "../components/Fields/EmphasisField"; 11 | // import TextField from "../components/Fields/TextField"; 12 | import ChipField from "../components/Fields/ChipField"; 13 | import CohortField from "../components/Fields/CohortField"; 14 | 15 | const data = require("../Mocks/students.json"); 16 | 17 | export default { 18 | title: "Table", 19 | decorators: [withDesign] 20 | }; 21 | 22 | console.log("DATA", data.students[0]); 23 | const props = { 24 | id: 1, 25 | neu_id: 40029272, 26 | name: "Maurine Rath", 27 | avatar: "https://s3.amazonaws.com/uifaces/faces/twitter/iduuck/128.jpg", 28 | status: "graduated", 29 | gpa: 1.289, 30 | test_avg: 84, 31 | cohort: { 32 | current: "19/24", 33 | original: "19/24", 34 | moved: false 35 | } 36 | }; 37 | 38 | export const Row = () => ( 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | {/* */} 49 | 50 | 51 |
52 | ); 53 | 54 | export const RowHeader = () => ( 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 |
67 | ); 68 | 69 | RowHeader.story = { 70 | parameters: { 71 | design: { 72 | type: "figma", 73 | url: 74 | "https://www.figma.com/file/QBWcVYJkF6K8o7WErRPsxd/Student-List-Mockups?node-id=31%3A412" 75 | } 76 | } 77 | }; 78 | 79 | Row.story = { 80 | parameters: { 81 | design: { 82 | type: "figma", 83 | url: 84 | "https://www.figma.com/file/QBWcVYJkF6K8o7WErRPsxd/Student-List-Mockups?node-id=31%3A413" 85 | } 86 | } 87 | }; 88 | -------------------------------------------------------------------------------- /pharmd-app/src/components/Basic/Checkbox Controls/CheckboxButtonGroup.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Description: 3 | * This Component creates a group of checkbox buttons. 4 | * There is one checkbox created for each children. 5 | * This function also accepts an onChange function that will be executed every time 6 | * a check box is unchecked. 7 | * 8 | * Date: 04-23-2020 9 | * This function also accepts an onChange function that will be exected every time 10 | * a check box is unchecked. 11 | */ 12 | 13 | // -------------------------- IMPORTS -------------------------- 14 | 15 | // Function Imports 16 | import React, { cloneElement } from "react"; 17 | // Component Imports 18 | import FormGroup from "@material-ui/core/FormGroup"; 19 | import FormControl from "@material-ui/core/FormControl"; 20 | import FormLabel from "@material-ui/core/FormLabel"; 21 | import CheckboxButton from "./CheckboxButton"; 22 | 23 | // -------------------------- COMPONENT -------------------------- 24 | 25 | const CheckboxButtonGroup = props => { 26 | const { 27 | checkboxCheckedClass, 28 | checkboxClassName, 29 | children, 30 | className, 31 | color, 32 | error, 33 | formGroupClassName, 34 | label, 35 | onChange, 36 | showLabel, 37 | } = props; 38 | 39 | return ( 40 | 41 | {showLabel && {label}} 42 | 43 | {children && 44 | children.map((child, index) => { 45 | // the child is expected to be a Material UI - FormControlLabel 46 | // this allows for the component to handle create a checkbox group of any size 47 | return cloneElement(child, { 48 | key: index, 49 | control: ( 50 | 63 | ) 64 | }); 65 | })} 66 | 67 | 68 | ); 69 | }; 70 | 71 | CheckboxButtonGroup.defaultProps = { 72 | showLabel: true 73 | }; 74 | 75 | export default CheckboxButtonGroup; 76 | -------------------------------------------------------------------------------- /pharmd-app/src/components/Basic/BarGraph.js: -------------------------------------------------------------------------------- 1 | // -------------------------- IMPORTS -------------------------- 2 | 3 | // Function Imports 4 | import React, { useState } from "react"; 5 | 6 | // Style Imports 7 | import { blue } from "@material-ui/core/colors"; 8 | import tw, { styled, css } from "twin.macro"; 9 | import "./Temp.css"; 10 | 11 | // -------------------------- COMPONENT -------------------------- 12 | 13 | const Bar = ({ max, min, height, threshold }) => { 14 | const heightStr = `${height.toString()}px`; 15 | // this bar covers (from threshold[0] to threshold[1]) 16 | let secondHeight; 17 | const withinThreshold = min < threshold[1] && max > threshold[0]; 18 | let animate; 19 | 20 | if (withinThreshold) { 21 | // the bar is within the range 22 | secondHeight = 0; 23 | animate = ""; 24 | } else { 25 | secondHeight = height; 26 | animate = "bar"; 27 | } 28 | 29 | const secondHeightStr = `${secondHeight.toString()}px`; 30 | const topStr = `-${height.toString()}px`; 31 | return ( 32 |
33 |
34 |
39 |
40 |
41 |
42 | ); 43 | }; 44 | 45 | const BarGraph = ({ value, barGraphData, css, ...props }) => { 46 | // bar graph data is an array, where each element is a bar 47 | 48 | // get the max and the min from value an array of size 2 49 | let max; 50 | let min; 51 | if (value[0] > value[1]) { 52 | max = value[0]; 53 | min = value[1]; 54 | } else { 55 | max = value[1]; 56 | min = value[0]; 57 | } 58 | 59 | const barGraphHeight = 60; 60 | 61 | // get the biggest number in the array 62 | const biggestFrequency = Math.max(...barGraphData); 63 | 64 | const interval = (1 / barGraphData.length) * 4; 65 | 66 | // the biggest number bar will be the height of the bar graph 67 | // use this number to determine the height of the other bars 68 | // Ex: x's height = (x / biggest_num) * height of bar graph 69 | return ( 70 |
71 | {barGraphData.map((frequency, index) => { 72 | // add height to div: height: heightRatio * height 73 | const height = (frequency / biggestFrequency) * barGraphHeight; 74 | 75 | const threshold = [index * interval, (index + 1) * interval]; 76 | return ; 77 | })} 78 |
79 | ); 80 | }; 81 | 82 | export default BarGraph; 83 | -------------------------------------------------------------------------------- /pharmd-app/src/components/Nav/NavItemSecondary.js: -------------------------------------------------------------------------------- 1 | // -------------------------- IMPORTS -------------------------- 2 | 3 | // Function Imports 4 | import React, { useCallback } from "react"; 5 | import PropTypes from "prop-types"; 6 | // Style Imports 7 | import tw, { styled } from "twin.macro"; 8 | // Component Imports 9 | import MenuItemMaterial from "@material-ui/core/MenuItem"; 10 | import ListItemIcon from "@material-ui/core/ListItemIcon"; 11 | import Tooltip from "../Basic/Tooltip"; 12 | import Icon from "../Basic/Icon"; 13 | 14 | const activeClassname = `NavItemLink-Active${btoa(Math.random())}`; 15 | 16 | const Item = styled(({ className, ...props }) => ( 17 | 18 | ))` 19 | ${tw`w-18 h-18 p-0 fontStyle-7`} 20 | ${props => 21 | props.sidebarIsOpen 22 | ? tw`w-full h-18 p-0 fontStyle-7` 23 | : tw`w-18 h-18 p-0 fontStyle-7`} 24 | 25 | .MuiListItemIcon-root { 26 | flex-grow: ${props => (props.sidebarIsOpen ? 0 : 1)}; 27 | ${tw`flex justify-center w-18`} 28 | } 29 | 30 | &.${activeClassname} { 31 | // Currently Empty 32 | } 33 | `; 34 | 35 | const IconItem = styled(ListItemIcon)` 36 | flex-grow: 1; 37 | ${tw`flex justify-center`} 38 | `; 39 | 40 | const NavItemSecondary = ( 41 | { className, title, iconSrc, onClick, sidebarIsOpen, isActive, ...props }, 42 | ref 43 | ) => { 44 | const handleMenuTap = useCallback( 45 | e => { 46 | onClick && onClick(e); 47 | }, 48 | [onClick] 49 | ); 50 | 51 | const renderNavItem = () => { 52 | return ( 53 | 60 | {iconSrc && ( 61 | 62 | 68 | 69 | )} 70 | {sidebarIsOpen && title} 71 | 72 | ); 73 | }; 74 | 75 | if (sidebarIsOpen) { 76 | return renderNavItem(); 77 | } 78 | return ( 79 | 80 |
{renderNavItem()}
81 |
82 | ); 83 | }; 84 | 85 | NavItemSecondary.propTypes = { 86 | className: PropTypes.string, 87 | iconSrc: PropTypes.object, 88 | onClick: PropTypes.func, 89 | title: PropTypes.node, 90 | staticContext: PropTypes.object, 91 | to: PropTypes.oneOfType([PropTypes.string, PropTypes.object]).isRequired, 92 | sidebarIsOpen: PropTypes.bool, 93 | isActive: PropTypes.bool 94 | }; 95 | 96 | export default NavItemSecondary; 97 | -------------------------------------------------------------------------------- /pharmd-app/src/screens/studentDetails/StudentDetailsContentGrid.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * This component determines the main layout of the StudentDetails Screen 4 | */ 5 | 6 | import React from "react"; 7 | 8 | import tw, { styled } from "twin.macro"; 9 | import Paper from "@material-ui/core/Paper"; 10 | import Grid from "@material-ui/core/Grid"; 11 | import Card from "@material-ui/core/Card"; 12 | import { withStyles } from "@material-ui/core/styles"; 13 | import { useSelector } from "react-redux"; 14 | import { Show, Tab, TabbedShowLayout, TabbedShowLayoutTabs } from "react-admin"; 15 | 16 | import StudentCourseList from "./StudentCourseList"; 17 | import StudentExamList from "./StudentExamList"; 18 | 19 | const MainGrid = styled(Grid)` 20 | ${tw`pt-12 `} 21 | `; 22 | 23 | const StyledCard = styled(Card)` 24 | ${tw`paperStyle`} 25 | height: auto; 26 | `; 27 | 28 | const StyledTabLayout = styled(TabbedShowLayout)` 29 | .MuiDivider-root { 30 | display: none; 31 | } 32 | `; 33 | 34 | const StyledTabs = withStyles({ 35 | root: { 36 | padding: "30px" 37 | }, 38 | indicator: { 39 | display: "none" 40 | } 41 | })(TabbedShowLayoutTabs); 42 | 43 | const StyledTab = styled(Tab)` 44 | ${tw`fontStyle-6 font-medium`} 45 | text-transform: none; 46 | color: ${props => (props.selected ? "#F0F4FF" : "#4573ee")}; 47 | background: ${props => (props.selected ? "#4573ee" : "#F0F4FF")}; 48 | border-radius: 14px; 49 | margin-right: 16px; 50 | `; 51 | 52 | const StudentDetailsContentGrid = props => { 53 | const isOpen = true; 54 | const { record } = props; 55 | 56 | return ( 57 | 58 | 59 | 60 | {/* */} 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 76 | }> 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | ); 88 | }; 89 | 90 | export default StudentDetailsContentGrid; 91 | -------------------------------------------------------------------------------- /pharmd-app/src/components/Basic/RangeSlider.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Description: 3 | * This Component creates a slider. 4 | * 5 | * Date: 03-25-2021 6 | */ 7 | 8 | // -------------------------- IMPORTS -------------------------- 9 | 10 | // Function Imports 11 | import React from "react"; 12 | import PropTypes from "prop-types"; 13 | 14 | // Component Imports 15 | import Slider from "@material-ui/core/Slider"; 16 | 17 | // Style Imports 18 | import { styled } from "twin.macro"; 19 | 20 | // -------------------------- COMPONENT -------------------------- 21 | 22 | const Label = styled.div``; 23 | 24 | function valuetext(value) { 25 | return `${value}`; 26 | } 27 | 28 | /* 29 | TODO: Might change onCHnage to onCHnageCommited 30 | for performace reason. Thiss will trigger the 31 | onChange after the user lift the mouse press up 32 | */ 33 | const RangeSlider = props => { 34 | const { 35 | onChange, 36 | setValueText, 37 | disabled, 38 | max, 39 | min, 40 | step, 41 | sliderClasses, 42 | value, 43 | setValue 44 | } = props; 45 | 46 | let rangeValue; 47 | let setRangeValue; 48 | 49 | if (value == null || setValue == null) { 50 | // Values were not given 51 | [rangeValue, setRangeValue] = React.useState([min, max]); 52 | } else { 53 | // use the variables givens 54 | rangeValue = value; 55 | setRangeValue = setValue; 56 | } 57 | 58 | const handleChange = (event, newValue) => { 59 | setRangeValue(newValue); 60 | onChange(event, newValue); 61 | }; 62 | 63 | return ( 64 | 81 | ); 82 | }; 83 | 84 | RangeSlider.propTypes = { 85 | // Typecheck for function with two arguments 86 | onChange(props, propName, componentName) { 87 | const fn = props[propName]; 88 | if ( 89 | !fn.prototype || 90 | (typeof fn.prototype.constructor !== "function" && fn.prototype.constructor.length !== 2) 91 | ) { 92 | return new Error(`${propName}must be a function with 2 args`); 93 | } 94 | }, 95 | setValueText: PropTypes.func, 96 | min: PropTypes.number, 97 | max: PropTypes.number, 98 | step: PropTypes.number, 99 | disabled: PropTypes.bool 100 | }; 101 | 102 | RangeSlider.defaultProps = { 103 | setValueText: value => `${value}`, 104 | min: 0, 105 | max: 100, 106 | step: 0.5, 107 | disabled: false 108 | }; 109 | 110 | export default RangeSlider; 111 | -------------------------------------------------------------------------------- /pharmd-app/src/components/Fields/ScoredListField/ScoredListItem.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import tw, { styled } from "twin.macro"; 3 | import PropTypes from "prop-types"; 4 | import ListItem from "@material-ui/core/ListItem"; 5 | import ListItemIcon from "@material-ui/core/ListItemIcon"; 6 | import ListItemTextMaterial from "@material-ui/core/ListItemText"; 7 | import ListItemSecondaryActionMaterial from "@material-ui/core/ListItemSecondaryAction"; 8 | import ScoredListItemError from "./ScoredListItemError"; 9 | import ScoredListItemSkeleton from "./ScoredListItemSkeleton"; 10 | import StatusBullet from "../../Basic/StatusBullet"; 11 | 12 | function statusMapping(value) { 13 | if (value >= 90) { 14 | return "success"; 15 | } 16 | if (value >= 80) { 17 | return "good"; 18 | } 19 | if (value >= 70) { 20 | return "warning"; 21 | } 22 | if (value >= 0) { 23 | return "danger"; 24 | } 25 | return "neutral"; 26 | } 27 | 28 | const ListItemText = styled(ListItemTextMaterial)` 29 | & .MuiListItemText-primary { 30 | ${tw`fontStyle-4 leading-6 font-medium block truncate`} 31 | } 32 | 33 | & .MuiListItemText-secondary { 34 | ${tw`fontStyle-2 text-gray-600`} 35 | } 36 | `; 37 | 38 | const ListItemSecondaryAction = styled(ListItemSecondaryActionMaterial)` 39 | ${tw`fontStyle-4 tracking-wider font-bold text-gray-900 right-1`} 40 | `; 41 | 42 | export const ScoredItem = ({ primaryText, secondaryText, value, ...rest }) => { 43 | return ( 44 | 45 | 46 | 47 | 48 | 49 | 50 | {value} 51 | 52 | 53 | ); 54 | }; 55 | 56 | ScoredItem.muiName = "ListItem"; 57 | 58 | const ScoredListItem = ({ 59 | primaryText, 60 | secondaryText, 61 | value, 62 | variant, 63 | loading, 64 | error, 65 | errorText, 66 | ...rest 67 | }) => { 68 | if (loading) { 69 | return ; 70 | } 71 | if (error) { 72 | return ; 73 | } 74 | return ( 75 | 81 | ); 82 | }; 83 | 84 | ScoredListItem.propTypes = { 85 | primaryText: PropTypes.string.isRequired, 86 | secondaryText: PropTypes.string, 87 | value: PropTypes.number, 88 | variant: PropTypes.oneOf(["primary", "secondary"]).isRequired, 89 | loading: PropTypes.bool, 90 | error: PropTypes.bool, 91 | errorText: PropTypes.string 92 | }; 93 | 94 | ScoredListItem.defaultProps = { 95 | variant: "primary", 96 | errorText: "Error Loading Data" 97 | }; 98 | export default ScoredListItem; 99 | -------------------------------------------------------------------------------- /pharmd-app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pharmd-app", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@material-ui/core": "^4.11.3", 7 | "@material-ui/icons": "^4.11.2", 8 | "@material-ui/lab": "^4.0.0-alpha.47", 9 | "@storybook/addon-knobs": "^5.3.17", 10 | "@testing-library/jest-dom": "^4.2.4", 11 | "@testing-library/react": "^9.4.0", 12 | "@testing-library/user-event": "^7.2.1", 13 | "axios": "^0.21.1", 14 | "clsx": "^1.1.1", 15 | "jwt-decode": "^2.2.0", 16 | "prop-types": "^15.7.2", 17 | "ra-data-json-server": "^3.1.3", 18 | "react": "^16.12.0", 19 | "react-admin": "^3.1.3", 20 | "react-dom": "^16.12.0", 21 | "react-scripts": "3.3.0", 22 | "styled-components": "^5.0.1", 23 | "twin.macro": "^1.11.0", 24 | "xlsx": "^0.15.5" 25 | }, 26 | "scripts": { 27 | "start": "webpack-dev-server", 28 | "build": "webpack --mode production", 29 | "test": "react-scripts test", 30 | "eject": "react-scripts eject", 31 | "storybook": "start-storybook -p 9009 -s public", 32 | "build-storybook": "build-storybook -s public" 33 | }, 34 | "eslintConfig": { 35 | "extends": "react-app" 36 | }, 37 | "browserslist": { 38 | "production": [ 39 | ">0.2%", 40 | "not dead", 41 | "not op_mini all" 42 | ], 43 | "development": [ 44 | "last 1 chrome version", 45 | "last 1 firefox version", 46 | "last 1 safari version" 47 | ] 48 | }, 49 | "devDependencies": { 50 | "@babel/core": "^7.8.7", 51 | "@babel/plugin-proposal-class-properties": "^7.4.4", 52 | "@babel/preset-env": "^7.4.5", 53 | "@babel/preset-react": "^7.0.0", 54 | "@storybook/addon-actions": "^5.3.18", 55 | "@storybook/addon-links": "^5.3.13", 56 | "@storybook/addons": "^5.3.13", 57 | "@storybook/preset-create-react-app": "^1.5.2", 58 | "@storybook/react": "^5.3.13", 59 | "@svgr/webpack": "^5.1.0", 60 | "babel-loader": "^8.0.6", 61 | "babel-plugin-macros": "^2.8.0", 62 | "copy-webpack-plugin": "^5.0.5", 63 | "css-loader": "^2.1.1", 64 | "dotenv": "^8.2.0", 65 | "eslint": "^7.3.1", 66 | "eslint-config-airbnb": "^18.2.1", 67 | "eslint-config-airbnb-base": " ^14.0.0", 68 | "eslint-config-prettier": "^6.10.0", 69 | "eslint-plugin-import": "^2.22.1", 70 | "eslint-plugin-jsx-a11y": "^6.2.3", 71 | "eslint-plugin-prettier": "^3.1.2", 72 | "eslint-plugin-react": "^7.14.3", 73 | "eslint-plugin-react-hooks": "^1.7.0", 74 | "html-loader": "^0.5.5", 75 | "html-webpack-plugin": "^3.2.0", 76 | "prettier": "^1.19.1", 77 | "storybook-addon-designs": "^5.2.0", 78 | "storybook-addon-styled-component-theme": "^1.3.0", 79 | "style-loader": "^0.23.1", 80 | "tailwind.macro": "^1.0.0-alpha.10", 81 | "url-loader": "^3.0.0", 82 | "webpack": "^4.41.2", 83 | "webpack-cli": "^3.3.5", 84 | "webpack-dev-server": "^3.11.0" 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /pharmd-app/src/components/Nav/NavItemLink.js: -------------------------------------------------------------------------------- 1 | // -------------------------- IMPORTS -------------------------- 2 | 3 | // Function Imports 4 | import React, { forwardRef, useCallback } from "react"; 5 | import PropTypes from "prop-types"; 6 | // Style Imports 7 | import tw, { styled } from "twin.macro"; 8 | // Component Imports 9 | import { NavLink } from "react-router-dom"; 10 | import MenuItemMaterial from "@material-ui/core/MenuItem"; 11 | import ListItemIcon from "@material-ui/core/ListItemIcon"; 12 | import Tooltip from "../Basic/Tooltip"; 13 | import Icon from "../Basic/Icon"; 14 | 15 | const NavLinkRef = forwardRef((props, ref) => ); 16 | const activeClassname = `NavItemLink-Active${btoa(Math.random())}`; 17 | 18 | // -------------------------- STYLE -------------------------- 19 | const Item = styled(({ className, ...props }) => ( 20 | 21 | ))` 22 | ${tw`w-24 h-24 p-0`} 23 | 24 | .MuiListItemIcon-root { 25 | flex-grow: 1; 26 | ${tw`flex justify-center`} 27 | } 28 | 29 | &.${activeClassname} { 30 | // Currently Empty 31 | } 32 | `; 33 | 34 | const IconItem = styled(ListItemIcon)` 35 | flex-grow: 1; 36 | ${tw`flex justify-center`} 37 | `; 38 | 39 | // -------------------------- COMPONENT -------------------------- 40 | const NavItemLink = forwardRef( 41 | ({ className, title, iconSrc, onClick, sidebarIsOpen, isActive, ...props }, ref) => { 42 | // Navigate to Route 43 | const handleMenuTap = useCallback( 44 | e => { 45 | onClick && onClick(e); 46 | }, 47 | [onClick] 48 | ); 49 | 50 | const renderNavItem = () => { 51 | return ( 52 | 59 | {iconSrc && ( 60 | 61 | 67 | 68 | )} 69 | {sidebarIsOpen && title} 70 | 71 | ); 72 | }; 73 | 74 | if (sidebarIsOpen) { 75 | return renderNavItem(); 76 | } 77 | return ( 78 | 79 |
{renderNavItem()}
80 |
81 | ); 82 | } 83 | ); 84 | 85 | NavItemLink.propTypes = { 86 | className: PropTypes.string, 87 | iconSrc: PropTypes.object, 88 | onClick: PropTypes.func, 89 | title: PropTypes.node, 90 | staticContext: PropTypes.object, 91 | to: PropTypes.oneOfType([PropTypes.string, PropTypes.object]).isRequired, 92 | sidebarIsOpen: PropTypes.bool, 93 | isActive: PropTypes.bool 94 | }; 95 | 96 | export default NavItemLink; 97 | -------------------------------------------------------------------------------- /pharmd-app/src/services/FileUploadService.js: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import * as XLSX from "xlsx"; 3 | import * as BackendRoutes from "../config/backendRoutes"; 4 | import {arrayToObject} from "./Utility"; 5 | 6 | /** 7 | * @typedef { import('../typeDefs.js').BasicStudentAssessment} BasicStudentAssessment 8 | * @typedef { import('../typeDefs.js').FileData} FileData 9 | */ 10 | 11 | class FileUploadService { 12 | static apiPrefix = "assessments/"; 13 | static EMPTY_CELL_VAL = "N/A"; 14 | 15 | // Enum holding the types of data to import to database 16 | static gradeInputTypes = { 17 | UNUSED: "Unused", 18 | STUDENT_NAME: "Student Name (Full)", 19 | STUDENT_LAST_NAME: "Student Last Name", 20 | STUDENT_FIRST_NAME:" Student First Name", 21 | EXAM: "Exam Grade", 22 | HOMEWORK: "Homework Grade", 23 | STUDENT_ID: "Student ID", 24 | SECTION: "Class Section", 25 | }; 26 | 27 | /** 28 | * Parses array of arrays holding the contents of each spreadsheet row, to populate grade data 29 | * @param sheetData the data from the spreadsheet 30 | */ 31 | static parseGradeData(sheetData) { 32 | const fields = Object.keys(sheetData[0]); 33 | } 34 | 35 | static parseInitialLoadData(sheetData) {} 36 | 37 | /** 38 | * Loads and begins parsing a spreadsheet 39 | * @param data the data representing the spreadsheet 40 | * @returns {Promise} 41 | */ 42 | static async loadSpreadsheet(data) { 43 | const reader = new FileReader(); 44 | let result = new Promise((resolve, reject) => { 45 | reader.onload = event => { 46 | const binary = event.target.result; 47 | const workbook = XLSX.read(binary, { type: "binary" }); 48 | // Grab the first worksheet 49 | const firstSheetName = workbook.SheetNames[0]; 50 | const firstSheet = workbook.Sheets[firstSheetName]; 51 | // Convert sheet to an array of arrays 52 | let data = XLSX.utils.sheet_to_json(firstSheet, { blankrows: true, defval: FileUploadService.EMPTY_CELL_VAL, header: 1 }); 53 | const headers = Object.values(data[0]); 54 | const subHeaders = Object.values(data[1]); 55 | if (data) { 56 | resolve({ 57 | data: data.slice(2).map(arr => arrayToObject(arr, headers)), 58 | subHeaders, 59 | headers: headers 60 | }); 61 | } else { 62 | reject("Couldn't load spreadsheet"); 63 | } 64 | }; 65 | }); 66 | await reader.readAsBinaryString(data); 67 | return result; 68 | } 69 | 70 | /** 71 | * Uploads student assessment data to the DB 72 | * @param data {Array} 73 | * @returns {Promise>} 74 | */ 75 | static UploadStudentAssessmentData(data) { 76 | const path = 'student-assessments/many/'; 77 | return axios.post(`${BackendRoutes.BACKEND_URL}${path}`, data); 78 | } 79 | } 80 | 81 | export default FileUploadService; 82 | -------------------------------------------------------------------------------- /pharmd-app/src/screens/profile/ProfileManage.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import { BooleanInput, SimpleForm, TextInput, useNotify } from "react-admin"; 3 | import axios from "axios"; 4 | 5 | import { BACKEND_USERS } from "../../config/backendRoutes"; 6 | import ProfilePage from "./ProfilePage"; 7 | 8 | function ProfileManage() { 9 | const [data, setData] = useState({ 10 | firstName: "", 11 | lastName: "", 12 | email: "", 13 | password: "", 14 | isAdmin: "" 15 | }); 16 | const [isAdmin, setIsAdmin] = useState(false); 17 | 18 | const notify = useNotify(); 19 | 20 | useEffect(() => { 21 | const info = JSON.parse(localStorage.getItem("userInfo")); 22 | const permissions = localStorage.getItem("permissions"); 23 | if (permissions === "admin") { 24 | setIsAdmin(true); 25 | } 26 | const url = `${BACKEND_USERS}${info.userID}`; 27 | axios 28 | .get(url) 29 | .then(res => { 30 | delete res.data.password; 31 | // TODO: remove this once we're getting real data 32 | setData({ 33 | firstName: "Foo", 34 | lastName: "bar", 35 | email: "b@a.com", 36 | password: "pass", 37 | isAdmin: true 38 | }); 39 | // setData(res.data) 40 | }) 41 | .catch(err => { 42 | notify("error fetching student data"); 43 | }); 44 | }, []); 45 | 46 | const handleSave = values => { 47 | axios 48 | .put(BACKEND_USERS, values) 49 | .then(res => { 50 | notify("Updating user successful", "warning"); 51 | }) 52 | .catch(err => { 53 | notify("error updating user"); 54 | }); 55 | }; 56 | 57 | const handleCreate = values => { 58 | axios 59 | .post(BACKEND_USERS, values) 60 | .then(res => { 61 | notify("Creating user successful", "warning"); 62 | }) 63 | .catch(err => { 64 | notify("error creating user"); 65 | }); 66 | }; 67 | 68 | return ( 69 | 70 |

Edit Profile

71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | {isAdmin && ( 81 | <> 82 |

Create Profile

83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | )} 92 |
93 | ); 94 | } 95 | 96 | export default ProfileManage; 97 | -------------------------------------------------------------------------------- /pharmd-app/src/services/StudentAssessmentService.js: -------------------------------------------------------------------------------- 1 | import * as BackendRoutes from "../config/backendRoutes" 2 | import axios from "axios"; 3 | 4 | /** 5 | * @typedef { import('../typeDefs.js').BasicStudentAssessment} BasicStudentAssessment 6 | */ 7 | 8 | class StudentAssessmentService { 9 | static apiPrefix = 'student-assessments/'; 10 | 11 | // GET Methods 12 | 13 | static getAssessmentsFromStudent = studentID => { 14 | const path = this.apiPrefix + studentID; 15 | return axios.get(`${BackendRoutes.BACKEND_URL}${path}`); 16 | }; 17 | 18 | // POST Methods 19 | 20 | /** 21 | * Adds a new student assessment to the backend 22 | * @param studentID {string} 23 | * @param assessment {} 24 | * @returns {Promise>} 25 | */ 26 | static addNewStudentAssessment = (studentID, assessment) => { 27 | const path = this.apiPrefix + studentID; 28 | return axios.post(`${BackendRoutes.BACKEND_URL}${path}`, assessment) 29 | }; 30 | 31 | /** 32 | * Given a string that contains a course term, finds it 33 | * @param courseName {string} 34 | * @returns {string} 35 | */ 36 | static findCourseNameTerm = (courseName) => { 37 | const words = courseName.split(' '); 38 | let wordIndex = -1; 39 | const semester = words.find((el, index) => { 40 | switch (el) { 41 | case 'Spring': 42 | case 'Summer': 43 | case 'Fall': 44 | wordIndex = index; 45 | return true; 46 | default: 47 | return false; 48 | } 49 | }); 50 | if (semester) { 51 | return semester + ' ' + words[wordIndex + 1]; 52 | } 53 | return ''; 54 | }; 55 | 56 | /** 57 | * Makes call to backend to upload these assessments to the DB 58 | * @param assessments {Array} 59 | * @param gradeTotals Array 60 | */ 61 | static addManyStudentAssessments = (assessments, gradeTotals) => { 62 | const path = this.apiPrefix + 'many/'; 63 | const formattedExams = assessments.map((ass, index) => { 64 | const courseID = ass.courseName.split(' ')[0]; 65 | const courseTerm = StudentAssessmentService.findCourseNameTerm(ass.courseName); 66 | const percentage = ass.percentage; // (parseInt(gradeTotals[index]) || 1); 67 | return { 68 | NUID: ass.NUID.replace(/\D/g,'') || '', 69 | courseID, 70 | courseTerm, 71 | assessmentName: ass.examName, 72 | percentage, 73 | courseName: ass.courseName, 74 | lastName: ass.studentName.split(',')[0].replace(' ', ''), 75 | firstName: ass.studentName.split(',')[1].replace(' ', '') 76 | } 77 | }); 78 | const url = `${BackendRoutes.BACKEND_URL}${path}`; 79 | return axios.post(url, formattedExams) 80 | }; 81 | } 82 | 83 | export default StudentAssessmentService; 84 | -------------------------------------------------------------------------------- /pharmd-app/README.md: -------------------------------------------------------------------------------- 1 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 2 | 3 | ## Available Scripts 4 | 5 | In the project directory, you can run: 6 | 7 | ### `npm start` 8 | 9 | Runs the app in the development mode.
10 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 11 | 12 | The page will reload if you make edits.
13 | You will also see any lint errors in the console. 14 | 15 | ### `npm test` 16 | 17 | Launches the test runner in the interactive watch mode.
18 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 19 | 20 | ### `npm run build` 21 | 22 | Builds the app for production to the `build` folder.
23 | It correctly bundles React in production mode and optimizes the build for the best performance. 24 | 25 | The build is minified and the filenames include the hashes.
26 | Your app is ready to be deployed! 27 | 28 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 29 | 30 | ### `npm run eject` 31 | 32 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 33 | 34 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 35 | 36 | Instead, it will copy all the configuration files and the transitive dependencies (Webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. 37 | 38 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. 39 | 40 | ## Learn More 41 | 42 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 43 | 44 | To learn React, check out the [React documentation](https://reactjs.org/). 45 | 46 | ### Code Splitting 47 | 48 | This section has moved here: https://facebook.github.io/create-react-app/docs/code-splitting 49 | 50 | ### Analyzing the Bundle Size 51 | 52 | This section has moved here: https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size 53 | 54 | ### Making a Progressive Web App 55 | 56 | This section has moved here: https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app 57 | 58 | ### Advanced Configuration 59 | 60 | This section has moved here: https://facebook.github.io/create-react-app/docs/advanced-configuration 61 | 62 | ### Deployment 63 | 64 | This section has moved here: https://facebook.github.io/create-react-app/docs/deployment 65 | 66 | ### `npm run build` fails to minify 67 | 68 | This section has moved here: https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify 69 | -------------------------------------------------------------------------------- /pharmd-app/src/themes/field-colors.js: -------------------------------------------------------------------------------- 1 | import { FIELD_COLOR_NAMES, FIELD_COLOR_VALUES } from "./light-theme"; 2 | import { LETTER_GRADE, STUDENT, STUDENT_COURSE, STUDENT_STATUS } from "../constants/apiObjects"; 3 | 4 | // get color value for the given field 5 | export const getFieldColor = ({ record, source }) => 6 | getFieldColorValue(getFieldColorName({ record, source })); 7 | 8 | // get color value for the given field 9 | export const getFieldBackgroundColor = ({ record, source }) => 10 | getFieldBackgroundColorValue(getFieldColorName({ record, source })); 11 | 12 | // get value of color w/given name 13 | export const getFieldColorValue = colorName => FIELD_COLOR_VALUES[colorName]; 14 | 15 | // get value of background color for the color w/ given name 16 | export const getFieldBackgroundColorValue = colorName => 17 | getFieldColorValue(colorName).replace("1)", "0.3)"); 18 | 19 | // get color name for given field 20 | export const getFieldColorName = ({ record, source }) => { 21 | switch (source) { 22 | case STUDENT.GPA: 23 | return getGPAColor({ record }); 24 | case STUDENT_COURSE.LETTER_GRADE: 25 | return getLetterGradeColor({ record }); 26 | case STUDENT_COURSE.PERCENTAGE: 27 | return getPercentageColor({ record }); 28 | case STUDENT.STATUS: 29 | return getStudentStatusColor({ record }); 30 | default: 31 | return FIELD_COLOR_NAMES.DEFAULT; 32 | } 33 | }; 34 | 35 | const getLetterGradeColor = ({ record }) => { 36 | switch (record[STUDENT_COURSE.LETTER_GRADE]) { 37 | case LETTER_GRADE.A: 38 | case LETTER_GRADE.A_MINUS: 39 | case LETTER_GRADE.B_PLUS: 40 | case LETTER_GRADE.B: 41 | return FIELD_COLOR_NAMES.GREEN; 42 | case LETTER_GRADE.B_MINUS: 43 | case LETTER_GRADE.C_PLUS: 44 | case LETTER_GRADE.C: 45 | case LETTER_GRADE.C_MINUS: 46 | return FIELD_COLOR_NAMES.ORANGE; 47 | case LETTER_GRADE.D_PLUS: 48 | case LETTER_GRADE.D: 49 | case LETTER_GRADE.D_MINUS: 50 | case LETTER_GRADE.F: 51 | return FIELD_COLOR_NAMES.RED; 52 | default: 53 | return FIELD_COLOR_NAMES.DEFAULT; 54 | } 55 | }; 56 | 57 | const getGPAColor = ({ record }) => { 58 | const gpa = record[STUDENT.GPA]; 59 | if (gpa >= 3.7) { 60 | return FIELD_COLOR_NAMES.GREEN; 61 | } 62 | if (gpa >= 1.7) { 63 | return FIELD_COLOR_NAMES.ORANGE; 64 | } 65 | return FIELD_COLOR_NAMES.RED; 66 | }; 67 | 68 | const getPercentageColor = ({ record }) => { 69 | const percentage = record[STUDENT_COURSE.PERCENTAGE]; 70 | if (percentage >= 83) { 71 | return FIELD_COLOR_NAMES.GREEN; 72 | } 73 | if (percentage >= 70) { 74 | return FIELD_COLOR_NAMES.ORANGE; 75 | } 76 | return FIELD_COLOR_NAMES.RED; 77 | }; 78 | 79 | const getStudentStatusColor = ({ record }) => { 80 | switch (record[STUDENT.STATUS]) { 81 | case STUDENT_STATUS.COOP: 82 | return FIELD_COLOR_NAMES.PRIMARY; 83 | case STUDENT_STATUS.ENROLLED: 84 | return FIELD_COLOR_NAMES.GREEN; 85 | case STUDENT_STATUS.DROP_BACK: 86 | return FIELD_COLOR_NAMES.RED; 87 | case STUDENT_STATUS.LEAVE: 88 | return FIELD_COLOR_NAMES.ORANGE; 89 | case STUDENT_STATUS.GRADUATED: 90 | return FIELD_COLOR_NAMES.TERTIARY; 91 | default: 92 | return FIELD_COLOR_NAMES.DEFAULT; 93 | } 94 | }; 95 | -------------------------------------------------------------------------------- /pharmd-app/src/services/Data/GetHooks.js: -------------------------------------------------------------------------------- 1 | // React Admin hooks that will makes calls to the API to GET content 2 | import { stringify } from "query-string"; 3 | import { fetchUtils } from "react-admin"; 4 | 5 | /** 6 | * Returns object with hooks for getting resources 7 | * @param {Function} httpClient The function to make an HTTP request 8 | * @param {string} BACKEND_URL the base URL to make a request to 9 | * @return {{getList: (function(*, *=): *), getMany: (function(*, *): *), getManyReference: (function(*, *): *), getOne: (function(*, *): *)}} 10 | */ 11 | export default (httpClient, BACKEND_URL) => ({ 12 | getList: (resource, params) => { 13 | const { page, perPage } = params.pagination; 14 | const { field, order } = params.sort; 15 | const query = { 16 | ...fetchUtils.flattenObject(params.filter), 17 | _sort: field, 18 | _order: order, 19 | _start: (page - 1) * perPage, 20 | _end: page * perPage 21 | }; 22 | const url = `${BACKEND_URL}/${resource}?${stringify(query)}`; 23 | return httpClient(url).then(({ headers, json, status }) => { 24 | if (!headers.has("x-total-count")) { 25 | return Promise.reject( 26 | "The X-Total-Count header is missing in the HTTP Response. The jsonServer Data Provider expects responses for lists of resources to contain this header with the total number of results to build the pagination. If you are using CORS, did you declare X-Total-Count in the Access-Control-Expose-Headers header?" 27 | ); 28 | } 29 | return { 30 | data: json, 31 | total: parseInt( 32 | headers 33 | .get("x-total-count") 34 | .split("/") 35 | .pop(), 36 | 10 37 | ) 38 | }; 39 | }); 40 | }, 41 | 42 | getOne: (resource, params) => 43 | httpClient(`${BACKEND_URL}/${resource}/${params.id}`).then(({ json }) => ({ 44 | data: json 45 | })), 46 | 47 | getMany: (resource, params) => { 48 | const query = { 49 | id: params.ids 50 | }; 51 | const url = `${BACKEND_URL}/${resource}?${stringify(query)}`; 52 | return httpClient(url).then(({ json }) => ({ data: json })); 53 | }, 54 | 55 | getManyReference: (resource, params) => { 56 | const { page, perPage } = params.pagination; 57 | const { field, order } = params.sort; 58 | const query = { 59 | ...fetchUtils.flattenObject(params.filter), 60 | [params.target]: params.id, 61 | _sort: field, 62 | _order: order, 63 | _start: (page - 1) * perPage, 64 | _end: page * perPage 65 | }; 66 | const url = `${BACKEND_URL}/${resource}?${stringify(query)}`; 67 | 68 | return httpClient(url).then(({ headers, json }) => { 69 | if (!headers.has("x-total-count")) { 70 | return Promise.reject( 71 | "The X-Total-Count header is missing in the HTTP Response. The jsonServer Data Provider expects responses for lists of resources to contain this header with the total number of results to build the pagination. If you are using CORS, did you declare X-Total-Count in the Access-Control-Expose-Headers header?" 72 | ); 73 | } 74 | return { 75 | data: json, 76 | total: parseInt( 77 | headers 78 | .get("x-total-count") 79 | .split("/") 80 | .pop(), 81 | 10 82 | ) 83 | }; 84 | }); 85 | } 86 | }); 87 | -------------------------------------------------------------------------------- /pharmd-app/src/components/Inputs/GpaSliderInput.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Description: 3 | * This Component creates a slider to filter Student data based on GPA. 4 | * 5 | * Date: 03-18-2021 6 | */ 7 | 8 | //-------------------------- IMPORTS -------------------------- 9 | 10 | // Function Imports 11 | import React from "react"; 12 | import { useInput } from "react-admin"; 13 | 14 | // Component Imports 15 | import RangeSlider from "../Basic/RangeSlider"; 16 | import { withStyles } from "@material-ui/core/styles"; 17 | 18 | // Style Imports 19 | import { blue } from "@material-ui/core/colors"; 20 | import BarGraph from "../Basic/BarGraph"; 21 | import Slider from "@material-ui/core/Slider"; 22 | 23 | // -------------------------- STYLE -------------------------- 24 | 25 | const styles = { 26 | // the line behind the range showing the full width of the range 27 | rail: { 28 | height: "5px" 29 | }, 30 | 31 | // rangSlider styling 32 | rangeSlider: { 33 | margin: "2rem 0 0 0", 34 | width: 300, 35 | "& h4": { 36 | margin: "0 0 1.33em 0", 37 | color: "black", 38 | fontWeight: "650", 39 | fontSize: "1.1rem", 40 | fontFamily: 41 | "Montserrat-SemiBold, Montserrat-Bold, Inter-Medium, Inter-SemiBold, Inter-Regular, Inter-Bold, sans-serif", 42 | "& span": { 43 | color: blue[700], 44 | fontSize: "1rem" 45 | } 46 | } 47 | }, 48 | 49 | // change the styling of the handles (round clickable object at either end of the slider) 50 | thumb: { 51 | height: "15px", 52 | width: "15px" 53 | }, 54 | 55 | // the line that dynamically shrinks or grows based on the user's interaction with this component 56 | track: { 57 | height: "5px" 58 | } 59 | }; 60 | 61 | //-------------------------- COMPONENT -------------------------- 62 | 63 | function valuetext(value) { 64 | return `${value}`; 65 | } 66 | 67 | const GpaSliderInput = ({ classes, setFilter, value, setValue, ...props }) => { 68 | const { 69 | meta: { error } 70 | } = useInput(props); 71 | 72 | // Adds a gpa filter to student data 73 | const searchGpaRange = (event, newValue) => { 74 | if (newValue) { 75 | const gpaMin = newValue[0]; 76 | const gpaMax = newValue[1]; 77 | setFilter("gpa", [gpaMin, gpaMax]); 78 | } 79 | }; 80 | 81 | // compute height difference between bar graph and slider 82 | // slider to top - (bar graph to top + height of bar graph) 83 | const top = 14; 84 | const topStr = top.toString() + "px"; 85 | const css = { top: topStr }; 86 | 87 | // TODO: Remove temporary data and add real data from back end 88 | const frequencies = [1, 2, 2.25, 2.5, 1.75, 2.75, 2.25, 2, 2.5, 1.5]; 89 | 90 | return ( 91 |
92 |

93 | GPA Range {`(${value[0]} - ${value[1]})`} 94 |

95 | 100 | 111 |
112 | ); 113 | }; 114 | 115 | export default withStyles(styles)(GpaSliderInput); 116 | -------------------------------------------------------------------------------- /pharmd-app/src/components/Basic/Breadcrumb.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Description: 3 | * Component creates breadcrumbs linking to different routes 4 | */ 5 | 6 | // -------------------------- IMPORTS -------------------------- 7 | 8 | // Function Imports 9 | import React from "react"; 10 | import { Route } from "react-router"; 11 | import PropTypes from "prop-types"; 12 | // Component Imports 13 | import { Link as RouterLink } from "react-router-dom"; 14 | import MuiLink from "@material-ui/core/Link"; 15 | import MuiBreadcrumbs from "@material-ui/core/Breadcrumbs"; 16 | // Style Imports 17 | import tw, { styled } from "twin.macro"; 18 | import { MAP_BREADCRUMB_NAME } from "../../constants/mappers"; 19 | import { 20 | HOME_TITLE, 21 | STUDENT_DETAILS_TITLE, 22 | STUDENT_QUICKVIEW_TITLE 23 | } from "../../constants/text"; 24 | import { HOME_MAIN, STUDENTS_MAIN } from "../../constants/routes"; 25 | 26 | // -------------------------- STYLE -------------------------- 27 | 28 | const Link = styled(MuiLink)` 29 | ${tw`fontStyle-3 m-0 text-gray-600`} 30 | `; 31 | 32 | const CurrentLink = styled.p` 33 | ${tw`fontStyle-3 m-0 text-gray-500`} 34 | `; 35 | 36 | // -------------------------- FUNCTIONS --------------------------\ 37 | 38 | const getName = (to, value) => { 39 | if (!isNaN(value) && to.includes(`${STUDENTS_MAIN}/${value}`)) { 40 | return STUDENT_QUICKVIEW_TITLE; 41 | } 42 | if (to.includes(`show`)) { 43 | return STUDENT_QUICKVIEW_TITLE; 44 | } 45 | if (to.includes(`details`)) { 46 | return STUDENT_DETAILS_TITLE; 47 | } 48 | 49 | // Check if route predefined 50 | const val = MAP_BREADCRUMB_NAME[to]; 51 | return val || value; 52 | }; 53 | 54 | export const BreadcrumbLink = props => ; 55 | 56 | // location Param added just for storybook testing 57 | function RouterBreadcrumb(locTest) { 58 | return ( 59 | 60 | {({ location }) => { 61 | // For Testing 62 | const loc = locTest.locTest ? locTest.locTest : location; 63 | const pathnames = loc.pathname.split("/").filter(x => x); 64 | 65 | const generateCrumbs = () => { 66 | const crumbs = []; 67 | pathnames.forEach((value, index) => { 68 | const isLast = index === pathnames.length - 1; 69 | // Route Location Link 70 | const to = `/${pathnames.slice(0, index + 1).join("/")}`; 71 | 72 | // Filters used to remove certain routes 73 | // if (!isNaN(value)) { // Remove Number IDs Incase if quickview is not included 74 | if (value === "show") { 75 | return; 76 | } 77 | 78 | if (isLast) { 79 | // Does not have a link 80 | crumbs.push({getName(to, value)}); 81 | } else { 82 | // Linkable 83 | crumbs.push( 84 | 85 | {getName(to, value)} 86 | 87 | ); 88 | } 89 | }); 90 | return crumbs; 91 | }; 92 | return ( 93 | 94 | 95 | {HOME_TITLE} 96 | 97 | {generateCrumbs()} 98 | 99 | ); 100 | }} 101 | 102 | ); 103 | } 104 | 105 | RouterBreadcrumb.defaultProps = {}; 106 | 107 | RouterBreadcrumb.propTypes = { 108 | locTest: PropTypes.string 109 | }; 110 | 111 | export default RouterBreadcrumb; 112 | -------------------------------------------------------------------------------- /pharmd-app/src/components/Basic/Autocomplete.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Description: 3 | * This Component creates a multi select drop down / text input to filter options in the drop down. 4 | * 5 | * Date: 03-25-2021 6 | */ 7 | 8 | // -------------------------- IMPORTS -------------------------- 9 | 10 | // Function Imports 11 | import React, { useState } from "react"; 12 | 13 | // Component Imports 14 | import AutocompleteMaterial from "@material-ui/lab/Autocomplete"; 15 | import TextField from "@material-ui/core/TextField"; 16 | import FilterChip from "./FilterChip"; 17 | 18 | // -------------------------- COMPONENT -------------------------- 19 | 20 | const Autocomplete = props => { 21 | const { 22 | classes, 23 | inputClassName, 24 | inputValue, 25 | isOptionSelected, 26 | label, 27 | onChange, 28 | options, 29 | placeholder, 30 | popupIcon, 31 | setInputValue, 32 | setValue, 33 | tagClassName, 34 | value 35 | } = props; 36 | 37 | // the state to keep track of text that is typed into the input text field 38 | let autocompleteInputValue; 39 | let setAutocompleteInputValue; 40 | 41 | if (inputValue == null || setInputValue == null) { 42 | // Values were not given 43 | [autocompleteInputValue, setAutocompleteInputValue] = useState(""); 44 | } else { 45 | // use the variables givens 46 | autocompleteInputValue = inputValue; 47 | setAutocompleteInputValue = setInputValue; 48 | } 49 | 50 | // the state the keep track of the options that have been selected (this is an array object) 51 | let autocompleteValue; 52 | let setAutocompleteValue; 53 | 54 | if (value == null || setValue == null) { 55 | // Values were not given 56 | [autocompleteValue, setAutocompleteValue] = useState([]); 57 | } else { 58 | // use the variables givens 59 | autocompleteValue = value; 60 | setAutocompleteValue = setValue; 61 | } 62 | 63 | // updates the list of tags that have been added 64 | const onChangeFunc = (event, newValue) => { 65 | setAutocompleteValue(newValue); 66 | onChange(event, newValue); 67 | }; 68 | 69 | // display placeholder only if there are no lists added or if there is no text input 70 | const placeholderOutput = () => { 71 | if (autocompleteValue != null && autocompleteValue.length > 0) { 72 | return undefined; 73 | } 74 | return placeholder; 75 | }; 76 | 77 | return ( 78 | { 83 | setAutocompleteInputValue(newInputValue); 84 | }} 85 | classes={{ 86 | root: classes.root, 87 | endAdornment: classes.endAdornment, 88 | listbox: classes.listbox, 89 | noOptions: classes.noOptions, 90 | option: classes.option, 91 | paper: classes.paper, 92 | popper: classes.popper 93 | }} 94 | disableCloseOnSelect 95 | getOptionLabel={option => option.label} 96 | getOptionSelected={isOptionSelected} 97 | multiple 98 | options={options} 99 | popupIcon={popupIcon} 100 | renderInput={params => ( 101 | 110 | )} 111 | renderTags={(tagValue, getTagProps) => { 112 | return tagValue.map((option, index) => ( 113 | 114 | )); 115 | }} 116 | /> 117 | ); 118 | }; 119 | 120 | export default Autocomplete; 121 | -------------------------------------------------------------------------------- /pharmd-app/src/screens/upload/components/SpreadsheetUploader.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import UploadFileChooser from "./UploadFileChooser"; 3 | import UploadDataFieldChooser from "./UploadDataFieldChooser"; 4 | import StudentAssessmentService from '../../../services/StudentAssessmentService'; 5 | import Button from '../../../components/Form/Button'; 6 | 7 | class SpreadsheetUploader extends Component { 8 | 9 | DEFAULT_STATE = { 10 | uploadedDataHeaders: null, 11 | uploadedDataSubheaders: null, 12 | uploadedData: null, 13 | uploadComplete: null 14 | }; 15 | 16 | constructor(props) { 17 | super(props); 18 | this.state = this.DEFAULT_STATE; 19 | 20 | this.uploadFile = this.uploadFile.bind(this); 21 | this.removeFile = this.removeFile.bind(this); 22 | this.confirmData = this.confirmData.bind(this); 23 | } 24 | 25 | /** 26 | * Updates the state with data about the uploaded file 27 | * @param {FileData}fileData 28 | */ 29 | uploadFile(fileData) { 30 | if (fileData.data.length > 0) { 31 | this.setState({ 32 | uploadedDataHeaders: fileData.headers, 33 | uploadedDataSubheaders: fileData.subHeaders, 34 | uploadedData: fileData.data, 35 | uploadComplete: null 36 | }); 37 | } 38 | }; 39 | 40 | removeFile() { 41 | this.setState(this.DEFAULT_STATE) 42 | } 43 | 44 | /** 45 | * Tries to upload the given data to the backend 46 | * @param data {{ 47 | * exams: Array 48 | * }} 49 | */ 50 | confirmData(data) { 51 | const promises = []; 52 | promises.push(StudentAssessmentService.addManyStudentAssessments(data.exams, [])); 53 | Promise.all(promises) 54 | .then(res => { 55 | this.setState({ 56 | ...this.DEFAULT_STATE, 57 | uploadComplete: 'Upload complete!' 58 | }); 59 | }) 60 | .catch(err => { 61 | this.setState({ 62 | ...this.DEFAULT_STATE, 63 | uploadComplete: 'Upload failed--please try again' 64 | }); 65 | }) 66 | } 67 | 68 | /** 69 | * Displays a dialog with help info about the file uploader 70 | */ 71 | showHelp() { 72 | const helpInfo = ` 73 | This tool is used to upload spreadsheet exports from Canvas into the PharmD database 74 | 75 | To use, choose your file, then match the appropriate columns with the value type it holds. 76 | 77 | Value Types (* = required): 78 | - * Student Name (Full): The student's full name (last, first) 79 | - * Student Last Name: The student's surname 80 | - * Student First Name: The student's given name 81 | - * Exam Grade: A grade for an exam 82 | - Homework grade: Self-explanatory 83 | - * Student ID: The student's NUID 84 | - *Class Section: The course these grades correspond to 85 | `; 86 | // TODO: replace with modal dialog 87 | alert(helpInfo) 88 | 89 | } 90 | 91 | render() { 92 | return ( 93 |
94 | 95 | {!this.state.uploadedData && 96 | } 97 | {this.state.uploadedData && } 98 | {this.state.uploadedData && } 103 | {this.state.uploadComplete && this.state.uploadComplete} 104 |
105 | ); 106 | } 107 | } 108 | 109 | export default SpreadsheetUploader; 110 | -------------------------------------------------------------------------------- /pharmd-app/src/components/Inputs/StatusCheckboxInput.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Description: 3 | * This Component creates a group of status filter checkbox buttons. 4 | * When you click on an status checkbox it will filter the table for data with the 5 | * selected status. 6 | * 7 | * Date: 02-18-2021 8 | */ 9 | 10 | // -------------------------- IMPORTS -------------------------- 11 | 12 | // Function Imports 13 | import React from "react"; 14 | import { useInput } from "react-admin"; 15 | 16 | // Component Imports 17 | import FormControlLabel from "@material-ui/core/FormControlLabel"; 18 | import CheckboxButtonGroup from "../Basic/Checkbox Controls/CheckboxButtonGroup"; 19 | 20 | // Style Imports 21 | import { withStyles } from "@material-ui/core/styles"; 22 | import { blue } from "@material-ui/core/colors"; 23 | import { STUDENT_STATUS } from "../../constants/apiObjects"; 24 | import CheckboxFilterButtonGroup from "../Basic/Checkbox Controls/CheckboxFilterButtonGroup"; 25 | 26 | // -------------------------- STYLE -------------------------- 27 | // Use styles to make the label change when the checkbox is checked 28 | const styles = { 29 | checked: { 30 | // when the checkbox is checked change the fontweight of the label 31 | "&, & + $label": { 32 | fontWeight: 600 33 | } 34 | }, 35 | 36 | // checkbox styling 37 | checkboxButton: { 38 | color: blue[700] 39 | }, 40 | 41 | // follow the uniformity: make the label 16px away from the filter 42 | formGroup: { 43 | paddingTop: "7px" 44 | } 45 | }; 46 | 47 | // -------------------------- COMPONENT -------------------------- 48 | 49 | const StatusCheckboxInput = ({ 50 | checkedBoxes, 51 | className, 52 | classes, 53 | color, 54 | label, 55 | setCheckedBoxes, 56 | setFilter, 57 | ...props 58 | }) => { 59 | const { 60 | meta: { error } 61 | } = useInput(props); 62 | 63 | let statusCheckedBoxes; 64 | let setStatusCheckedBoxes; 65 | 66 | // Contains a list of tuples: (String: value, String: label) 67 | const formControlLabels = [ 68 | [STUDENT_STATUS.ENROLLED, "Enrolled"], 69 | [STUDENT_STATUS.COOP, "Co-op"], 70 | [STUDENT_STATUS.GRADUATED, "Graduated"], 71 | [STUDENT_STATUS.LEAVE, "Leave"], 72 | [STUDENT_STATUS.DROP_BACK, "Drop Back"] 73 | ]; 74 | 75 | if (checkedBoxes == null || setCheckedBoxes == null) { 76 | // Values were not given 77 | [statusCheckedBoxes, setStatusCheckedBoxes] = React.useState([min, max]); 78 | } else { 79 | // use the variables givens 80 | statusCheckedBoxes = checkedBoxes; 81 | setStatusCheckedBoxes = setCheckedBoxes; 82 | } 83 | 84 | // Return a function that accepts an event 85 | const createOnClick = item => { 86 | return e => { 87 | const index = statusCheckedBoxes.indexOf(item); 88 | const newCheckedBoxes = [...statusCheckedBoxes]; 89 | if (index < 0) { 90 | // item is not in the list 91 | newCheckedBoxes.push(item); 92 | } else { 93 | // item is already in the list - remove it 94 | newCheckedBoxes.splice(index, 1); 95 | } 96 | 97 | // after getting the updated list add / remove filters 98 | if (newCheckedBoxes.length === 5) { 99 | // all of the checkboxes are checked 100 | setFilter("status", []); 101 | } else { 102 | setFilter("status", newCheckedBoxes); 103 | } 104 | 105 | setStatusCheckedBoxes(newCheckedBoxes); 106 | }; 107 | }; 108 | 109 | return ( 110 | 119 | {formControlLabels.map(([formControlValue, formControlLabel]) => { 120 | return ( 121 | = 0} 123 | value={formControlValue} 124 | label={formControlLabel} 125 | onclick={createOnClick(formControlValue)} 126 | classes={{ label: classes.label }} 127 | /> 128 | ); 129 | })} 130 | 131 | ); 132 | }; 133 | 134 | export default withStyles(styles)(StatusCheckboxInput); 135 | -------------------------------------------------------------------------------- /pharmd-app/src/screens/student/StudentDisplayFilters.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Description: 3 | * This Component creates a list of chips to display which student filter is in use. 4 | * These chips have a close icon that when clicked will remove the respective filter and reset the respective filter 5 | * component. 6 | * 7 | * Format of filters: 8 | * cohort {current: [] } 9 | * gpa_gte: float 10 | * gpa_lte: float 11 | * international: bool 12 | * status: [] 13 | * Date: 03-25-2021 14 | */ 15 | 16 | // -------------------------- IMPORTS -------------------------- 17 | // Function Imports 18 | import React from "react"; 19 | 20 | // Component Imports 21 | import FilterChip from "../../components/Basic/FilterChip"; 22 | 23 | // Style Imports 24 | import { withStyles } from "@material-ui/core/styles"; 25 | import tw from "twin.macro"; 26 | 27 | // -------------------------- STYLE -------------------------- 28 | 29 | const Chip = withStyles({ 30 | root: { 31 | backgroundColor: "#2B2B90", 32 | fontSize: "1rem", 33 | marginRight: ".3rem", 34 | marginBottom: ".3rem", 35 | // when clicked or hovered on nothing changes 36 | "&:hover, &:focus, &:active": { 37 | backgroundColor: "#2B2B90" 38 | } 39 | } 40 | })(FilterChip); 41 | 42 | // -------------------------- COMPONENT -------------------------- 43 | 44 | const StudentDisplayFilters = ({ 45 | deleteFilter, 46 | filterValues, 47 | setAutocompleteInputValue, 48 | setAutocompleteValue, 49 | setFilter, 50 | setOriginCheckedLabels, 51 | setRangeValue, 52 | setStatusCheckedLabels, 53 | ...props 54 | }) => { 55 | let numStatusFilters = 0; 56 | let hasGPAFilters = false; 57 | let numCohortFilters = 0; 58 | let internationalFilter = null; 59 | 60 | if ("cohort" in filterValues) { 61 | numCohortFilters = filterValues.cohort.current.length; 62 | } 63 | 64 | if ("gpa" in filterValues) { 65 | hasGPAFilters = filterValues.gpa[0] > 0 || filterValues.gpa[1] < 4; 66 | } 67 | 68 | if ("hasVisa" in filterValues) { 69 | internationalFilter = filterValues.hasVisa; 70 | } 71 | 72 | if ("status" in filterValues) { 73 | numStatusFilters = filterValues.status.length; 74 | } 75 | 76 | // reset filter and filter component functions 77 | 78 | const resetStatusValues = () => { 79 | deleteFilter("status"); 80 | // this state variable is linked to the status filter component 81 | setStatusCheckedLabels([]); 82 | }; 83 | 84 | const resetGPAValues = () => { 85 | setFilter("gpa", [0, 4]); 86 | // this state variable is linked to the GPA filter component 87 | setRangeValue([0, 4]); 88 | }; 89 | 90 | const resetCohortValue = () => { 91 | setFilter("cohort[current]", []); 92 | // these state variable are linked to the cohort filter component 93 | setAutocompleteInputValue(""); 94 | setAutocompleteValue([]); 95 | }; 96 | 97 | const resetInternationalValues = () => { 98 | // this state variable is linked to the international filter component 99 | deleteFilter("hasVisa"); 100 | setOriginCheckedLabels([]); 101 | }; 102 | 103 | // Create International filter chip 104 | // Displays which filter is on: Domestic or International 105 | const displayInternationalFilter = () => { 106 | if (internationalFilter != null) { 107 | let label = ""; 108 | if (internationalFilter) { 109 | // filters for international 110 | label = "International"; 111 | } else { 112 | // filters for domestic 113 | label = "Domestic"; 114 | } 115 | 116 | return ( 117 | { 120 | resetInternationalValues(); 121 | }} 122 | /> 123 | ); 124 | } 125 | }; 126 | 127 | return ( 128 |
129 | {numStatusFilters > 0 && ( 130 | { 133 | resetStatusValues(); 134 | }} 135 | /> 136 | )} 137 | {hasGPAFilters && ( 138 | { 141 | resetGPAValues(); 142 | }} 143 | /> 144 | )} 145 | {numCohortFilters > 0 && ( 146 | { 149 | resetCohortValue(); 150 | }} 151 | /> 152 | )} 153 | {internationalFilter != null && displayInternationalFilter()} 154 |
155 | ); 156 | }; 157 | 158 | export default StudentDisplayFilters; 159 | -------------------------------------------------------------------------------- /pharmd-app/src/components/Inputs/CohortMultiSelectInput.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Description: 3 | * This Component creates a multi select drop down / text input to filter Student data based on GPA. 4 | * This component also allows the user to type in the option they are looking for to filter the options. 5 | * 6 | * Date: 03-18-2021 7 | */ 8 | 9 | // -------------------------- IMPORTS -------------------------- 10 | 11 | // Function Imports 12 | import React from "react"; 13 | import { useInput } from "react-admin"; 14 | 15 | // Component Imports 16 | import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; 17 | 18 | // Style Imports 19 | import { blue } from "@material-ui/core/colors"; 20 | import { withStyles } from "@material-ui/core/styles"; 21 | import Autocomplete from "../Basic/Autocomplete"; 22 | import tw from "twin.macro"; 23 | 24 | // -------------------------- STYLE -------------------------- 25 | 26 | const styles = { 27 | // style the general formControl Styling 28 | root: { 29 | width: "100%", 30 | // style the default border color and size 31 | "& fieldset": { 32 | borderRadius: ".6rem", 33 | border: ".2rem solid #F0F4FF" 34 | }, 35 | // style the border color and size when user focuses (clicks on) the filter component 36 | "&.Mui-focused fieldset": { 37 | border: ".2rem solid " + blue[700] + " !important" 38 | }, 39 | // style the border color and size when user hovers over the filter component 40 | "&:hover fieldset": { 41 | border: ".2rem solid " + blue[700] + " !important" 42 | } 43 | }, 44 | 45 | // style the container element of toggle popper icon and remove all options icon 46 | endAdornment: { 47 | top: "9px" 48 | }, 49 | 50 | // style the container element of options 51 | listbox: { 52 | border: ".2rem solid " + blue[700], 53 | borderRadius: ".6rem", 54 | marginTop: ".5rem", 55 | padding: 0 56 | }, 57 | 58 | // style the container element of listbox (remove the shadow) 59 | paper: { 60 | boxShadow: "unset" 61 | }, 62 | 63 | // styling for each option that a user can select within pop up 64 | option: { 65 | // styling when an option has been selected 66 | "&[aria-selected=true]": { 67 | backgroundColor: "#F0F4FF" 68 | }, 69 | // styling when an option is hovered on 70 | "&:hover": { 71 | backgroundColor: "#F0F4FF" 72 | } 73 | }, 74 | 75 | // styling for the no options div that shows up when there are no options that match 76 | // the user's input 77 | noOptions: { 78 | border: ".2rem solid " + blue[700], 79 | borderRadius: ".6rem", 80 | marginTop: ".5rem", 81 | 82 | // match the default padding on the options 83 | paddingTop: "6px", 84 | paddingLeft: "16px", 85 | paddingRight: "16px", 86 | paddingBottom: "6px" 87 | }, 88 | 89 | // style the tags (pills) or options that have been selected in the input field 90 | tag: { 91 | backgroundColor: "#4573EE", 92 | marginBottom: ".3rem", 93 | marginRight: ".3rem" 94 | } 95 | }; 96 | 97 | // -------------------------- COMPONENT -------------------------- 98 | 99 | const CohortMultipleSelect = ({ classes, className, inputValue, label, setFilter, setInputValue, setValue, value, ...props }) => { 100 | const { 101 | meta: { error } 102 | } = useInput(props); 103 | 104 | // setFilter (function) requires a list of values. Ex: ["15/20"] 105 | // Autocomplete will get an array of dict. Ex: [{label: "Cohort 22", value: "17/22"}] 106 | const cohortMultiSelect = (event, array) => { 107 | const values = []; 108 | array.forEach(dict => { 109 | values.push(dict.value); 110 | }); 111 | setFilter("cohort[current]", values); 112 | }; 113 | 114 | // determines if an option (dictionary) has been selected based on its value 115 | const isCohortSelected = (option, value) => { 116 | // checks if these two dictionaries have a "value" key 117 | if (!("value" in option || "value" in value)) { 118 | return false; 119 | } 120 | 121 | // checks if these two dictionaries have the same value (no two options should have the 122 | // same value 123 | return option.value === value.value; 124 | }; 125 | 126 | const cohortList = [ 127 | { label: "Cohort 20", value: "15/20" }, 128 | { label: "Cohort 22", value: "17/22" }, 129 | { label: "Cohort 23", value: "18/23" }, 130 | { label: "Cohort 24", value: "19/24" }, 131 | { label: "Cohort 25", value: "20/25" }, 132 | { label: "Cohort 26", value: "20/26" } 133 | ]; 134 | 135 | return ( 136 |
137 |

{label}

138 | } 148 | setInputValue={setInputValue} 149 | setValue={setValue} 150 | value={value} 151 | /> 152 |
153 | ); 154 | }; 155 | 156 | export default withStyles(styles)(CohortMultipleSelect); 157 | --------------------------------------------------------------------------------