├── README.md ├── public ├── robots.txt ├── manifest.json └── index.html ├── src ├── custom.d.ts ├── containers │ ├── ActivityForm.css │ ├── Dashboard.tsx │ ├── Profile.tsx │ ├── Submissions.tsx │ └── ActivityForm.tsx ├── components │ └── ActivityForm │ │ ├── ResultPage │ │ ├── ResultPage.scss │ │ └── ResultPage.tsx │ │ ├── FormInstructions │ │ ├── FormInstructions.scss │ │ └── FormInstructions.tsx │ │ ├── CategorySelector │ │ ├── CategorySelector.scss │ │ └── CategorySelector.tsx │ │ ├── FormContainer │ │ ├── FormContainer.scss │ │ └── FormContainer.tsx │ │ └── FormInput │ │ ├── FormInput.scss │ │ └── FormInput.tsx ├── shared │ ├── utils │ │ └── date.utils.ts │ └── components │ │ ├── Tooltip │ │ ├── Tooltip.tsx │ │ └── Tooltip.scss │ │ └── Navbar │ │ ├── Navbar.scss │ │ └── Navbar.tsx ├── styles │ ├── utilities.css │ └── index.css ├── app │ ├── app.store.ts │ └── App.tsx ├── reportWebVitals.ts ├── models │ └── activityForm.dto.ts ├── index.tsx ├── api │ └── activityForm.client.ts ├── media │ ├── personIcon.svg │ ├── successCheckmark.svg │ ├── infoIcon.svg │ ├── failureWarning.svg │ ├── profileIcon.svg │ ├── calendarIcon.svg │ └── neuLogo.svg └── store │ └── form.store.ts ├── .gitignore ├── tsconfig.json └── package.json /README.md: -------------------------------------------------------------------------------- 1 | initial README 2 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /src/custom.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.svg" { 2 | const content: any; 3 | export default content; 4 | } -------------------------------------------------------------------------------- /src/containers/ActivityForm.css: -------------------------------------------------------------------------------- 1 | .left-column { 2 | flex: 60%; 3 | padding: 0 92px 32px; 4 | } 5 | 6 | .right-column { 7 | flex: 40%; 8 | border-left: solid 1px black; 9 | padding: 60px; 10 | padding-top: 0px; 11 | } -------------------------------------------------------------------------------- /src/components/ActivityForm/ResultPage/ResultPage.scss: -------------------------------------------------------------------------------- 1 | .result-page-container { 2 | display: flex; 3 | flex-direction: column; 4 | align-items: center; 5 | margin-top: 20vh; 6 | 7 | a { 8 | color: red; 9 | } 10 | } -------------------------------------------------------------------------------- /src/containers/Dashboard.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const Dashboard: React.FC = () => { 4 | 5 | return ( 6 |
7 |

Dashboard

8 |
9 | ); 10 | }; 11 | 12 | export default Dashboard; 13 | -------------------------------------------------------------------------------- /src/shared/utils/date.utils.ts: -------------------------------------------------------------------------------- 1 | export const createDateFromString = (date: string) : Date | null => { 2 | const newDate: Date = new Date(date); 3 | if (newDate.toString() === 'Invalid Date') { 4 | return null; 5 | } 6 | return newDate; 7 | }; -------------------------------------------------------------------------------- /src/containers/Profile.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const ProfilePage: React.FC = () => { 4 | return ( 5 |
6 |

7 | Profile 8 |

9 |
10 | ) 11 | }; 12 | 13 | export default ProfilePage; 14 | -------------------------------------------------------------------------------- /src/styles/utilities.css: -------------------------------------------------------------------------------- 1 | 2 | .ml-auto { 3 | margin-left: auto !important; 4 | } 5 | 6 | .mr-auto { 7 | margin-right: auto !important; 8 | } 9 | 10 | .mt-auto { 11 | margin-top: auto !important; 12 | } 13 | 14 | .mb-auto { 15 | margin-bottom: auto !important; 16 | } 17 | -------------------------------------------------------------------------------- /src/containers/Submissions.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const SubmissionsPage: React.FC = () => { 4 | return ( 5 |
6 |

7 | Your Submissions 8 |

9 |
10 | ); 11 | }; 12 | 13 | export default SubmissionsPage; 14 | -------------------------------------------------------------------------------- /src/app/app.store.ts: -------------------------------------------------------------------------------- 1 | import { configureStore } from '@reduxjs/toolkit'; 2 | import formReducer from '../store/form.store'; 3 | 4 | export const store = configureStore({ 5 | reducer: { 6 | form: formReducer, 7 | }, 8 | }); 9 | 10 | export type RootState = ReturnType; 11 | 12 | export type AppDispatch = typeof store.dispatch; -------------------------------------------------------------------------------- /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 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /.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 | .idea 21 | 22 | npm-debug.log* 23 | yarn-debug.log* 24 | yarn-error.log* 25 | -------------------------------------------------------------------------------- /src/components/ActivityForm/FormInstructions/FormInstructions.scss: -------------------------------------------------------------------------------- 1 | .instructions-container { 2 | ul { 3 | list-style-position: inside; 4 | padding: 0; 5 | *:not(:last-child) { 6 | margin-bottom: 12px; 7 | } 8 | } 9 | 10 | .tooltip-container { 11 | display: flex; 12 | align-items: center; 13 | *:not(:last-child) { 14 | margin-right: 8px; 15 | } 16 | } 17 | } -------------------------------------------------------------------------------- /src/reportWebVitals.ts: -------------------------------------------------------------------------------- 1 | import { ReportHandler } from 'web-vitals'; 2 | 3 | const reportWebVitals = (onPerfEntry?: ReportHandler) => { 4 | if (onPerfEntry && onPerfEntry instanceof Function) { 5 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 6 | getCLS(onPerfEntry); 7 | getFID(onPerfEntry); 8 | getFCP(onPerfEntry); 9 | getLCP(onPerfEntry); 10 | getTTFB(onPerfEntry); 11 | }); 12 | } 13 | }; 14 | 15 | export default reportWebVitals; 16 | -------------------------------------------------------------------------------- /src/models/activityForm.dto.ts: -------------------------------------------------------------------------------- 1 | import { ActivityCategory, ActivityWeight } from "../store/form.store"; 2 | 3 | export type Semester = 'Fall' | 'Spring' | 'Summer 1' | 'Summer 2'; 4 | 5 | export type CreateActivityDto = { 6 | userId: number; 7 | academicYearId: number; 8 | year: number; 9 | semester: Semester; 10 | date?: Date; 11 | name: string; 12 | description: string; 13 | category: ActivityCategory; 14 | significance: ActivityWeight; 15 | isFavorite: boolean; 16 | }; -------------------------------------------------------------------------------- /src/shared/components/Tooltip/Tooltip.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import './Tooltip.scss'; 3 | 4 | interface TooltipProps { 5 | tooltipTitle: string; 6 | text: string[]; 7 | } 8 | 9 | const Tooltip: React.FC = ({tooltipTitle, text}: TooltipProps) => { 10 | return ( 11 |
{tooltipTitle} 12 | 13 | {text.map((item) => {return (

{item}

)})} 14 |
15 |
16 | ); 17 | }; 18 | 19 | export default Tooltip; 20 | -------------------------------------------------------------------------------- /src/components/ActivityForm/CategorySelector/CategorySelector.scss: -------------------------------------------------------------------------------- 1 | @mixin input-box { 2 | border: solid 1px black; 3 | border-radius: 10px; 4 | padding: 8px 12px; 5 | } 6 | 7 | .category-container { 8 | ol { 9 | list-style-position: inside; 10 | padding: 0; 11 | 12 | >*:not(:last-child) { 13 | margin-bottom: 18px; 14 | } 15 | } 16 | 17 | select { 18 | @include input-box; 19 | width: 50%; 20 | } 21 | 22 | button.bottom-right { 23 | float: right; 24 | margin-top: 72px; 25 | } 26 | 27 | label div { 28 | margin-top: 48px; 29 | } 30 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "noFallthroughCasesInSwitch": true, 16 | "module": "esnext", 17 | "moduleResolution": "node", 18 | "resolveJsonModule": true, 19 | "isolatedModules": true, 20 | "noEmit": true, 21 | "jsx": "react-jsx" 22 | }, 23 | "include": [ 24 | "src", 25 | "src/custom.d.ts" 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import App from './app/App'; 4 | import reportWebVitals from './reportWebVitals'; 5 | import './styles/index.css'; 6 | import './styles/utilities.css'; 7 | 8 | const root = ReactDOM.createRoot( 9 | document.getElementById('root') as HTMLElement 10 | ); 11 | root.render( 12 | 13 | 14 | 15 | ); 16 | 17 | // If you want to start measuring performance in your app, pass a function 18 | // to log results (for example: reportWebVitals(console.log)) 19 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 20 | reportWebVitals(); 21 | -------------------------------------------------------------------------------- /src/api/activityForm.client.ts: -------------------------------------------------------------------------------- 1 | import { CreateActivityDto } from "../models/activityForm.dto"; 2 | 3 | const apiRoot = 'http://localhost:3001/activities/' 4 | export const createActivity = async (body: CreateActivityDto): Promise => { 5 | try { 6 | const response = await fetch(apiRoot, { 7 | method: 'POST', 8 | headers: { 9 | 'Content-Type': 'application/json', 10 | 'Access-Control-Allow-Origin': 'http://localhost:3000', 11 | }, 12 | body: JSON.stringify(body), 13 | }); 14 | return (response.ok || response.status === 201) 15 | } 16 | catch { 17 | return false; 18 | } 19 | }; -------------------------------------------------------------------------------- /src/components/ActivityForm/FormContainer/FormContainer.scss: -------------------------------------------------------------------------------- 1 | .form-container { 2 | display: flex; 3 | height: 100%; 4 | } 5 | 6 | .progress-bar-container { 7 | width: 100%; 8 | margin-top: 24px; 9 | 10 | .progress-bar { 11 | background-color: #D9D9D9; 12 | height: 12px; 13 | width: 100%; 14 | border-radius: 5px; 15 | } 16 | 17 | .progress-section { 18 | background-color: #D41B2C; 19 | height: 100%; 20 | border-radius: 5px; 21 | 22 | &-half { 23 | width: 50%; 24 | } 25 | 26 | &-full { 27 | width: 100%; 28 | } 29 | } 30 | 31 | p { 32 | text-align: end; 33 | margin: 18px 0px 0px 0px; 34 | } 35 | } -------------------------------------------------------------------------------- /src/shared/components/Navbar/Navbar.scss: -------------------------------------------------------------------------------- 1 | .navbar-container { 2 | background-color: black; 3 | font-weight: bold; 4 | display: flex; 5 | flex-direction: row; 6 | justify-content: space-between; 7 | align-items: baseline; 8 | padding: 24px 48px; 9 | max-width: 100vw; 10 | 11 | a { 12 | color: white; 13 | font-weight: bolder; 14 | } 15 | 16 | a:hover { 17 | color: #d3d3d3; 18 | } 19 | 20 | .active, .not-active { 21 | padding-bottom: 8px; 22 | border-bottom: 3px solid transparent; 23 | text-decoration: none; 24 | } 25 | 26 | .active { 27 | border-color: red; 28 | } 29 | 30 | .inline-icon { 31 | display: flex; 32 | align-items: center; 33 | img { 34 | margin-right: 8px; 35 | } 36 | } 37 | } -------------------------------------------------------------------------------- /src/shared/components/Tooltip/Tooltip.scss: -------------------------------------------------------------------------------- 1 | .tooltip { 2 | position: relative; 3 | display: inline block; 4 | font-style: italic; 5 | text-decoration-line: underline; 6 | text-decoration-color: #585858; 7 | text-decoration-thickness: 1px; 8 | // max-width: fit-content; 9 | width: 100%; 10 | 11 | .tooltip-text { 12 | font-style: normal; 13 | text-align: justify; 14 | padding: 10px; 15 | width: 100%; 16 | margin: 5px; 17 | visibility: hidden; 18 | background-color: white; 19 | border: 1px solid #C8C8C8; 20 | box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.25); 21 | border-radius: 18px; 22 | position: absolute; 23 | z-index: 1; 24 | top: 100%; 25 | left: 0; 26 | // transform: translateY(-50%); 27 | 28 | p { 29 | margin: 12px; 30 | } 31 | } 32 | } 33 | 34 | .tooltip:hover .tooltip-text { 35 | visibility: visible; 36 | } -------------------------------------------------------------------------------- /src/media/personIcon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/app/App.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Provider } from 'react-redux'; 3 | import Dashboard from '../containers/Dashboard'; 4 | import { 5 | BrowserRouter as Router, 6 | Routes, 7 | Route, 8 | } from 'react-router-dom'; 9 | import ProfilePage from '../containers/Profile'; 10 | import ActivityForm from '../containers/ActivityForm'; 11 | import SubmissionsPage from '../containers/Submissions'; 12 | import Navbar from '../shared/components/Navbar/Navbar'; 13 | import { store } from './app.store'; 14 | 15 | const Home = () => ( 16 |
17 |

Home

18 |
19 | ); 20 | 21 | 22 | function App() { 23 | return ( 24 | 25 | 26 | 27 | 28 | } /> 29 | } /> 30 | } /> 31 | } /> 32 | } /> 33 | 34 | 35 | 36 | 37 | ); 38 | } 39 | 40 | export default App; 41 | -------------------------------------------------------------------------------- /src/styles/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | -webkit-font-smoothing: antialiased; 4 | -moz-osx-font-smoothing: grayscale; 5 | } 6 | 7 | code { 8 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 9 | monospace; 10 | } 11 | 12 | #root { 13 | width: 100vw; 14 | height: 100vh; 15 | } 16 | 17 | * { 18 | font-size: 14px; 19 | font-family: 'Lato', sans-serif !important; 20 | } 21 | 22 | h1 { 23 | font-size: 36px !important; 24 | margin: 1rem 0px; 25 | } 26 | 27 | h2 { 28 | font-size: 24px !important; 29 | margin: 1rem 0px; 30 | } 31 | 32 | input[type="date"]::-webkit-calendar-picker-indicator { 33 | opacity: 0; 34 | } 35 | 36 | /* NOTE: Not totally sure if this is the best fix or way to do this but its definitely something? */ 37 | button { 38 | border: solid 1px black; 39 | border-radius: 10px; 40 | padding: 8px 24px; 41 | background-color: white; 42 | cursor: pointer; 43 | } 44 | 45 | .button-red { 46 | border-color: #a41f2c; 47 | background-color: #D41B2C; 48 | color: white; 49 | } 50 | 51 | .button-red:disabled { 52 | border-color: #ae3e45; 53 | background-color: #ed949c; 54 | } 55 | 56 | .text-red { 57 | color: #D41B2C; 58 | } -------------------------------------------------------------------------------- /src/components/ActivityForm/FormContainer/FormContainer.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useSelector } from 'react-redux'; 3 | import { FormStep, selectStep } from '../../../store/form.store'; 4 | import FormInstructions from '../FormInstructions/FormInstructions'; 5 | import './FormContainer.scss'; 6 | 7 | const FormContainer: React.FC<{children: JSX.Element}> = ({children}) => { 8 | const step: FormStep = useSelector(selectStep); 9 | const stepOne = step === "selection"; 10 | return ( 11 |
12 |
13 |
14 |
15 |
16 |
17 |

Page {stepOne ? '1' : '2'}/2

18 |
19 | {children} 20 |
21 |
22 | 23 |
24 |
25 | ); 26 | } 27 | 28 | export default FormContainer; 29 | -------------------------------------------------------------------------------- /src/media/successCheckmark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/shared/components/Navbar/Navbar.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useDispatch } from 'react-redux'; 3 | import { NavLink } from 'react-router-dom'; 4 | import { resetForm } from '../../../store/form.store'; 5 | import neuLogo from '../../../media/neuLogo.svg'; 6 | import profileIcon from '../../../media/profileIcon.svg'; 7 | import './Navbar.scss'; 8 | 9 | const Navbar: React.FC = () => { 10 | const dispatch = useDispatch() 11 | return ( 12 |
13 | CAMD Logo 14 | isActive ? 'active': 'not-active'} to='/dashboard'>Dashboard 15 | isActive ? 'active': 'not-active'} to='/new-activity' onClick={() => dispatch(resetForm())}>Submit a New Activity 16 | isActive ? 'active': 'not-active'} to='/submissions'>Submissions 17 | (isActive ? 'active': 'not-active') + ' inline-icon'} to='/profile'> Profile Icon My Profile 18 |
19 | ) 20 | }; 21 | 22 | 23 | export default Navbar; -------------------------------------------------------------------------------- /src/containers/ActivityForm.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react'; 2 | import { useSelector } from 'react-redux'; 3 | import CategorySelector from '../components/ActivityForm/CategorySelector/CategorySelector'; 4 | import { FormStep, selectStep } from '../store/form.store'; 5 | import FormInput from '../components/ActivityForm/FormInput/FormInput'; 6 | import ResultPage from '../components/ActivityForm/ResultPage/ResultPage'; 7 | import FormContainer from '../components/ActivityForm/FormContainer/FormContainer'; 8 | import './ActivityForm.css'; 9 | 10 | const StepComponent: Record = { 11 | 'selection': , // TODO: Look into children 12 | 'form': , 13 | 'success': , 14 | 'loading':
, 15 | 'error': 16 | } 17 | 18 | const ActivityForm: React.FC = () => { 19 | const step: FormStep = useSelector(selectStep); 20 | 21 | useEffect(() => { 22 | window.onbeforeunload = () => { 23 | return 'Data will be lost if you leave the page, are you sure?'; 24 | }; 25 | }, []); 26 | 27 | return StepComponent[step]; 28 | }; 29 | 30 | export default ActivityForm; 31 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "prof-event-tracker-frontend", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@reduxjs/toolkit": "^1.8.6", 7 | "@testing-library/jest-dom": "^5.16.5", 8 | "@testing-library/react": "^13.4.0", 9 | "@testing-library/user-event": "^13.5.0", 10 | "@types/jest": "^27.5.2", 11 | "@types/node": "^16.11.62", 12 | "@types/react": "^18.0.21", 13 | "@types/react-dom": "^18.0.6", 14 | "react": "^18.2.0", 15 | "react-dom": "^18.2.0", 16 | "react-redux": "^8.0.4", 17 | "react-router-dom": "^6.4.1", 18 | "react-scripts": "5.0.1", 19 | "sass": "^1.55.0", 20 | "typescript": "^4.8.4", 21 | "web-vitals": "^2.1.4" 22 | }, 23 | "scripts": { 24 | "start": "react-scripts start", 25 | "build": "react-scripts build", 26 | "test": "react-scripts test", 27 | "eject": "react-scripts eject" 28 | }, 29 | "eslintConfig": { 30 | "extends": [ 31 | "react-app", 32 | "react-app/jest" 33 | ] 34 | }, 35 | "browserslist": { 36 | "production": [ 37 | ">0.2%", 38 | "not dead", 39 | "not op_mini all" 40 | ], 41 | "development": [ 42 | "last 1 chrome version", 43 | "last 1 firefox version", 44 | "last 1 safari version" 45 | ] 46 | }, 47 | "devDependencies": { 48 | "@types/react-router-dom": "^5.3.3" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/components/ActivityForm/ResultPage/ResultPage.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { NavLink } from 'react-router-dom'; 3 | import { useDispatch } from 'react-redux'; 4 | import { setStep } from '../../../store/form.store'; 5 | import successCheckmark from '../../../media/successCheckmark.svg'; 6 | import failureWarning from '../../../media/failureWarning.svg'; 7 | import './ResultPage.scss'; 8 | 9 | const ResultPage: React.FC<{success: boolean}> = ({success}) => { 10 | const dispatch = useDispatch(); 11 | return ( 12 |
13 | Icon 14 | { 15 | success? 16 | <> 17 |

Your activity was submitted!

18 | 19 | If you'd like to view or edit previous submissions, navigate to Submissions 20 | 21 | : 22 | <> 23 |

There was an error with your submission.

24 |

Please try again.

25 | 26 | 27 | } 28 |
29 | ) 30 | 31 | }; 32 | 33 | export default ResultPage; -------------------------------------------------------------------------------- /src/media/infoIcon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/media/failureWarning.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/components/ActivityForm/CategorySelector/CategorySelector.tsx: -------------------------------------------------------------------------------- 1 | import React, { ChangeEventHandler } from 'react'; 2 | import { useDispatch, useSelector } from 'react-redux'; 3 | import { ActivityCategory, selectCategory, setCategory, setStep } from '../../../store/form.store'; 4 | import './CategorySelector.scss' 5 | 6 | const CategorySelector: React.FC = () => { 7 | const category: ActivityCategory | null = useSelector(selectCategory); 8 | const dispatch = useDispatch(); 9 | 10 | 11 | const handleChange: ChangeEventHandler = (event) => { 12 | const newCategory: ActivityCategory = event.target.value as ActivityCategory; 13 | if (newCategory) { 14 | dispatch(setCategory(newCategory)); 15 | } 16 | }; 17 | 18 | const submit = () => { 19 | if (category) { 20 | dispatch(setStep('form')); 21 | } 22 | } 23 | 24 | return ( 25 |
26 |

Category

27 |
    28 |
  1. Teaching: Educational activities that benefit NU students.
  2. 29 |
  3. Creative Activity, Scholarship and Research/Professional Development.
  4. 30 |
  5. Service: Activities outside of NU community.
  6. 31 |
32 | 42 | 43 |
44 | ); 45 | }; 46 | 47 | export default CategorySelector; 48 | -------------------------------------------------------------------------------- /src/components/ActivityForm/FormInput/FormInput.scss: -------------------------------------------------------------------------------- 1 | @mixin input-box { 2 | border: solid 1px black; 3 | border-radius: 10px; 4 | padding: 8px 12px; 5 | } 6 | .form-input-container { 7 | display: flex; 8 | flex-direction: column; 9 | 10 | .button-container { 11 | display: flex; 12 | justify-content: space-between; 13 | align-items: center; 14 | cursor: pointer; 15 | margin: 36px 0px; 16 | } 17 | 18 | .input-container { 19 | display: flex; 20 | flex-direction: column; 21 | margin: 8px 0; 22 | >*:not(:last-child) { 23 | margin-bottom: 12px; 24 | } 25 | 26 | > div { 27 | display: flex; 28 | align-items: center; 29 | } 30 | 31 | .date-input { 32 | background-image: url('../../../media/calendarIcon.svg'); 33 | background-repeat: no-repeat; 34 | background-position: 98%; 35 | } 36 | 37 | } 38 | 39 | .input-status { 40 | display: flex; 41 | align-items: center; 42 | margin-left: 12px; 43 | padding: 14px 0px; 44 | 45 | p { 46 | margin-top: 0px; 47 | margin-bottom: 0px; 48 | } 49 | 50 | *:not(:last-child) { 51 | margin-right: 8px; 52 | } 53 | } 54 | 55 | .year-semester-container { 56 | display: flex; 57 | 58 | > *:not(:last-child) { 59 | margin-right: 24px; 60 | } 61 | } 62 | 63 | .tooltip-container { 64 | display: flex; 65 | align-items: center; 66 | *:not(:last-child) { 67 | margin-right: 8px; 68 | } 69 | } 70 | 71 | label { 72 | font-size: 24px; 73 | font-weight: bold; 74 | } 75 | 76 | #weight-guidelines-label { 77 | margin-top: 16px; 78 | } 79 | 80 | input, select { 81 | width: 50%; 82 | min-width: 200px; 83 | box-sizing: border-box; 84 | 85 | } 86 | 87 | 88 | input, select, textarea { 89 | @include input-box; 90 | outline: none !important; 91 | } 92 | } -------------------------------------------------------------------------------- /src/media/profileIcon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Faculty Activity Tracker 5 | 6 | 7 | 8 | 9 | 13 | 14 | 18 | 19 | 28 | React App 29 | 30 | 31 | 32 | 33 | 34 | 35 |
36 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /src/components/ActivityForm/FormInstructions/FormInstructions.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import "./FormInstructions.scss"; 3 | import personIcon from '../../../media/personIcon.svg'; 4 | import Tooltip from "../../../shared/components/Tooltip/Tooltip"; 5 | 6 | interface FormInstructionsProps { 7 | showWeightGuidelines?: boolean; // whether to include weight guidelines 8 | } 9 | 10 | const FormInstructions: React.FC = ({ showWeightGuidelines }) => { 11 | return ( 12 |
13 |
14 |

Instructions:

15 |
    16 |
  • For each activity, select a category, insert information about each activity, and provide a concise description that provides context.
  • 17 |
  • Each activity should have a weight of major, significant, or minor.
  • 18 |
  • Guidelines are provided but are not strictly enforced in the score calculation.
  • 19 |
  • If you would like to make a weight claim that is different than listed, it must be justified in the description.
  • 20 |
  • If you would like to make a bonus claim meaning that your work in one category should overflow into another, then justify it in the description.
  • 21 |
  • The committee may ask for evidence for extra support and context.
  • 22 |
23 |
24 | { 25 | showWeightGuidelines && 26 |
27 |

Weight Guidelines:

28 |
29 |

8-10 Major Activity: 2 and above + Significant and Minor Activities: 10 and above

30 |

7-8 Major Activity: 1-2 + Significant and Minor Activities: 6-10

31 |

6-7 Major Activity: 0-1 + Significant and Minor Activities: 2-6

32 |

6 Fulfilling required course load

33 |
34 |
35 | Little person icon 36 | 41 |
42 |
43 | } 44 |
45 | ); 46 | }; 47 | 48 | export default FormInstructions; -------------------------------------------------------------------------------- /src/store/form.store.ts: -------------------------------------------------------------------------------- 1 | import { createSlice, Selector } from "@reduxjs/toolkit"; 2 | import type { PayloadAction } from "@reduxjs/toolkit"; 3 | import { RootState } from "../app/app.store"; 4 | import { Semester } from "../models/activityForm.dto"; 5 | 6 | export type FormStep = 'selection' | 'form' | 'success' | 'loading' | 'error' 7 | export type ActivityCategory = 'TEACHING' | 'RESEARCH' | 'SERVICE'; 8 | export type ActivityWeight = 'MAJOR' | 'SIGNIFICANT' | 'MINOR'; 9 | 10 | 11 | // TODO: We might want to make this string or null? and do the a null check inside the component? Or we can check for null and validate 12 | // that it fits the intended format before sending to the backend? Either way we have to check for correct format before sending to backend 13 | export interface FormState { 14 | step: FormStep, 15 | activityName: string | null 16 | category: ActivityCategory | null, 17 | weight: ActivityWeight | null, 18 | semester: Semester | null, 19 | year: number | null, 20 | date: string, 21 | description: string, 22 | }; 23 | 24 | const initialState: FormState = { 25 | step: 'selection', 26 | activityName: null, 27 | category: null, 28 | weight: null, 29 | semester: null, 30 | year: null, 31 | date: '', 32 | description: '', 33 | }; 34 | 35 | export const formSlice = createSlice({ 36 | name: 'Form', 37 | initialState, 38 | reducers: { 39 | setStep: (state, action: PayloadAction) => { 40 | state.step = action.payload; 41 | }, 42 | setName: (state, action: PayloadAction) => { 43 | state.activityName = action.payload; 44 | }, 45 | setCategory: (state, action: PayloadAction) => { 46 | state.category = action.payload; 47 | }, 48 | setWeight: (state, action: PayloadAction) => { 49 | state.weight = action.payload; 50 | }, 51 | setSemester: (state, action: PayloadAction) => { 52 | state.semester = action.payload; 53 | }, 54 | setYear: (state, action: PayloadAction) => { 55 | state.year = action.payload; 56 | }, 57 | setDate: (state, action: PayloadAction) => { 58 | state.date = action.payload; 59 | }, 60 | setDescription: (state, action: PayloadAction) => { 61 | state.description = action.payload; 62 | }, 63 | resetForm: (state) => { 64 | state.step = 'selection'; 65 | state.activityName = null; 66 | state.category = null; 67 | state.weight = null; 68 | state.semester = null; 69 | state.year = null; 70 | state.date = ''; 71 | state.description = ''; 72 | }, 73 | }, 74 | }); 75 | 76 | export const { setName, setStep, setCategory, setWeight, setSemester, setYear, setDate, setDescription, resetForm } = formSlice.actions; 77 | 78 | export const selectName: Selector = (state) => state.form.activityName; 79 | 80 | export const selectStep: Selector = (state) => state.form.step; 81 | 82 | export const selectCategory: Selector = (state) => state.form.category; 83 | 84 | export const selectWeight: Selector = (state) => state.form.weight; 85 | 86 | export const selectSemester: Selector = (state) => state.form.semester; 87 | 88 | export const selectYear: Selector = (state) => state.form.year; 89 | 90 | export const selectDate: Selector = (state) => state.form.date; 91 | 92 | export const selectDescription: Selector = (state) => state.form.description; 93 | 94 | export default formSlice.reducer; 95 | -------------------------------------------------------------------------------- /src/media/calendarIcon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/components/ActivityForm/FormInput/FormInput.tsx: -------------------------------------------------------------------------------- 1 | import React, { ChangeEventHandler, FocusEventHandler, useState } from "react"; 2 | import { useDispatch, useSelector } from "react-redux"; 3 | import { ActivityCategory, ActivityWeight, selectCategory, selectDate, selectDescription, selectName, selectSemester, selectWeight, selectYear, setDate, setDescription, setName, setSemester, setStep, setWeight, setYear } from "../../../store/form.store"; 4 | import { createDateFromString } from "../../../shared/utils/date.utils"; 5 | import { CreateActivityDto, Semester } from "../../../models/activityForm.dto"; 6 | import { createActivity } from "../../../api/activityForm.client"; 7 | import Tooltip from "../../../shared/components/Tooltip/Tooltip"; 8 | import infoIcon from '../../../media/infoIcon.svg'; 9 | import successCheckmark from '../../../media/successCheckmark.svg'; 10 | import failureWarning from '../../../media/failureWarning.svg'; 11 | import './FormInput.scss'; 12 | 13 | const categoryLabels: Record = { 14 | TEACHING: "Teaching", 15 | RESEARCH: "Creative Activity, Scholarship and Research/Professional Development", 16 | SERVICE: "Service", 17 | }; 18 | 19 | const FormInput: React.FC = () => { 20 | const category: ActivityCategory | null = useSelector(selectCategory); 21 | const name: string | null = useSelector(selectName); 22 | const weight: ActivityWeight | null = useSelector(selectWeight); 23 | const semester: Semester | null = useSelector(selectSemester); 24 | const year: number | null = useSelector(selectYear); 25 | const date: string = useSelector(selectDate); 26 | const description: string = useSelector(selectDescription); 27 | const [ specifyDate, setSpecifyDate ] = useState(false); 28 | 29 | const dispatch = useDispatch(); 30 | 31 | const handleWeightChange: ChangeEventHandler = (event) => { 32 | const newWeight: ActivityWeight = event.target.value as ActivityWeight; 33 | if (newWeight) { 34 | dispatch(setWeight(newWeight)); 35 | } 36 | }; 37 | 38 | const handleDateChange: ChangeEventHandler = (event) => { 39 | const newDate: string = event.target.value; 40 | dispatch(setDate(newDate)); 41 | }; 42 | 43 | const handleDescriptionChange: ChangeEventHandler = (event) => { 44 | const newDescription: string = event.target.value; 45 | dispatch(setDescription(newDescription)); 46 | }; 47 | 48 | const handleNameChange: ChangeEventHandler = (event) => { 49 | const newName: string = event.target.value; 50 | dispatch(setName(newName)); 51 | }; 52 | 53 | const handleSemesterChange: ChangeEventHandler = (event) => { 54 | const newSemester: Semester = event.target.value as Semester; 55 | if (newSemester) { 56 | dispatch(setSemester(newSemester)); 57 | } 58 | }; 59 | 60 | const handleYearChange: ChangeEventHandler = (event) => { 61 | // delete entire input => reset year 62 | if (event.target.value === '') { 63 | dispatch(setYear(null)); 64 | } else { 65 | const newYear: number = parseInt(event.target.value); 66 | if (!isNaN(newYear)) { 67 | dispatch(setYear(newYear)); 68 | } 69 | } 70 | }; 71 | 72 | const changeToDate: FocusEventHandler = (event) => { 73 | event.target.type="date"; 74 | }; 75 | 76 | const changeToText: FocusEventHandler = (event) => { 77 | if(!event.target.value) { 78 | event.target.type="text" 79 | } 80 | }; 81 | 82 | const submitActivity = () => { 83 | if (!description || !category || !weight || !name || !semester || !year || (specifyDate && !date)) return; 84 | const dateObject = createDateFromString(date); 85 | if (specifyDate && !dateObject) return; 86 | 87 | const newActivityDto: CreateActivityDto = { 88 | userId : 1, 89 | academicYearId : 1, 90 | semester : semester, 91 | year : year, 92 | date : dateObject || undefined, 93 | name : name, 94 | description : description, 95 | category : category, 96 | significance : weight, 97 | isFavorite : true 98 | }; 99 | dispatch(setStep('loading')); 100 | createActivity(newActivityDto).then((res) => { 101 | dispatch(setStep(res? 'success' : 'error')); 102 | }); 103 | }; 104 | 105 | if (category === null) return (
Category must be selected
); 106 | return ( 107 |
108 |

{categoryLabels[category]}

109 |
110 | 111 |
112 | 113 |
114 | Icon 115 | { !name &&

Enter an activity name.

} 116 |
117 |
118 |
119 |
120 | 121 |
122 | Little information icon 123 | 128 |
129 |
130 | 136 |
137 | Icon 138 | { !weight &&

Select a weight.

} 139 |
140 |
141 |
142 |
143 |
144 | 145 | 151 |
152 |
153 | 154 | 155 |
156 |
157 | Icon 158 | { (!semester || !year) &&

Enter a semester and year.

} 159 |
160 |
161 | { 162 | specifyDate && 163 |
164 | 165 | 173 |
174 | 175 | } 176 |
177 |
178 | 179 |
180 | Icon 181 | { !description &&

Enter a description.

} 182 |
183 |
184 |