├── .tool-versions
├── src
├── react-app-env.d.ts
├── mocks
│ ├── browser.ts
│ ├── server.ts
│ └── handlers.ts
├── models
│ ├── Student.ts
│ ├── User.ts
│ ├── SurveyResponse.ts
│ └── StudentSurveyResponse.ts
├── components
│ ├── partials
│ │ ├── QueryError.tsx
│ │ ├── Avatar.tsx
│ │ ├── UserItem.tsx
│ │ └── SessionItem.tsx
│ ├── icons
│ │ ├── vector.svg
│ │ ├── timer.svg
│ │ ├── homeSelected.svg
│ │ ├── personSelected.svg
│ │ ├── home.svg
│ │ ├── person.svg
│ │ └── celebrating.svg
│ ├── SurveyShow
│ │ ├── TextQuestion.tsx
│ │ ├── SingleSelectionOption.tsx
│ │ ├── SurveyProgressBar.tsx
│ │ ├── MultipleSelectionOption.tsx
│ │ └── index.tsx
│ ├── Landing.tsx
│ ├── EditAccountSuccess.tsx
│ ├── WelcomeDashboard
│ │ ├── StudentSection.tsx
│ │ ├── index.tsx
│ │ ├── SessionSection.tsx
│ │ └── noSessions.svg
│ ├── SessionDuration
│ │ ├── style.scss
│ │ └── index.tsx
│ ├── App.tsx
│ ├── Account.tsx
│ ├── EditAccount.tsx
│ ├── Login.tsx
│ ├── EditAccountPassword.tsx
│ ├── StudentShow.tsx
│ └── MainNav.tsx
├── graphql
│ ├── queries
│ │ ├── GetStudents.ts
│ │ ├── GetCurrentUser.ts
│ │ ├── GetSurvey.ts
│ │ ├── GetStudentSurveyResponses.ts
│ │ ├── GetStudent.ts
│ │ ├── GetSurveyResponses.ts
│ │ ├── GetSurveyResponse.ts
│ │ └── __generated__
│ │ │ ├── GetStudents.ts
│ │ │ ├── GetCurrentUser.ts
│ │ │ ├── GetStudent.ts
│ │ │ ├── GetSurvey.ts
│ │ │ ├── GetSurveyResponses.ts
│ │ │ ├── GetStudentSurveyResponses.ts
│ │ │ └── GetSurveyResponse.ts
│ └── mutations
│ │ ├── SignInMutation.ts
│ │ ├── CreateSurveyResponse.ts
│ │ ├── CreateSupportTicket.ts
│ │ ├── UpsertSurveyQuestionResponse.ts
│ │ ├── UpdateUserMutation.ts
│ │ └── __generated__
│ │ ├── UpdateUserPassword.ts
│ │ ├── UpdateUser.ts
│ │ ├── SignIn.ts
│ │ ├── CreateSupportTicket.ts
│ │ ├── CreateSurveyResponse.ts
│ │ ├── CreateSurveyQuestionResponse.ts
│ │ └── UpsertSurveyQuestionResponse.ts
├── utils
│ ├── test.tsx
│ └── getAge.ts
├── index.css
├── reportWebVitals.ts
├── lib
│ └── authentication.ts
├── index.tsx
├── __test__
│ └── components
│ │ ├── VolunteerProfilePage.test.tsx
│ │ ├── StudentSection.test.tsx
│ │ ├── EditVolunteerPasswordPage.test.tsx
│ │ ├── EditVolunteerPage.test.tsx
│ │ └── Landing.test.tsx
└── setupTests.ts
├── public
├── robots.txt
├── favicon.ico
├── logo192.png
├── logo512.png
├── manifest.json
├── index.html
└── mockServiceWorker.js
├── craco.config.js
├── .gitignore
├── .github
├── workflows
│ ├── test.yml
│ └── lint.yml
├── PULL_REQUEST_TEMPLATE.md
└── ISSUE_TEMPLATE.md
├── tsconfig.json
├── __generated__
└── globalTypes.ts
├── external
└── graphql
│ └── schema.graphql
├── LICENSE.md
├── .eslintrc.json
├── tailwind.config.js
├── package.json
└── README.md
/.tool-versions:
--------------------------------------------------------------------------------
1 | nodejs 16.13.0
2 | yarn 1.22.17
3 |
--------------------------------------------------------------------------------
/src/react-app-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rubyforgood/inkind-volunteer/HEAD/public/favicon.ico
--------------------------------------------------------------------------------
/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rubyforgood/inkind-volunteer/HEAD/public/logo192.png
--------------------------------------------------------------------------------
/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rubyforgood/inkind-volunteer/HEAD/public/logo512.png
--------------------------------------------------------------------------------
/src/mocks/browser.ts:
--------------------------------------------------------------------------------
1 | import { setupWorker } from "msw"
2 | import { handlers } from "./handlers"
3 |
4 | export const worker = setupWorker(...handlers)
5 |
--------------------------------------------------------------------------------
/src/models/Student.ts:
--------------------------------------------------------------------------------
1 | import { GetStudents_students } from './../graphql/queries/__generated__/GetStudents'
2 | export type Student = GetStudents_students
3 |
--------------------------------------------------------------------------------
/src/models/User.ts:
--------------------------------------------------------------------------------
1 | import { GetCurrentUser_currentUser } from 'graphql/queries/__generated__/GetCurrentUser'
2 | export type User = GetCurrentUser_currentUser
3 |
--------------------------------------------------------------------------------
/craco.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | style: {
3 | postcss: {
4 | plugins: [require("tailwindcss"), require("autoprefixer")],
5 | },
6 | },
7 | }
8 |
--------------------------------------------------------------------------------
/src/models/SurveyResponse.ts:
--------------------------------------------------------------------------------
1 | import { GetSurveyResponses_surveyResponses } from './../graphql/queries/__generated__/GetSurveyResponses'
2 | export type SurveyResponse = GetSurveyResponses_surveyResponses
3 |
--------------------------------------------------------------------------------
/src/components/partials/QueryError.tsx:
--------------------------------------------------------------------------------
1 | export const QueryError = ({ error }: { error: unknown }): JSX.Element => (
2 |
3 | ERROR: {JSON.stringify(error, null, " ")}
4 |
5 | )
6 |
--------------------------------------------------------------------------------
/src/mocks/server.ts:
--------------------------------------------------------------------------------
1 | import { setupServer } from "msw/node"
2 | import { handlers } from "./handlers"
3 |
4 | // Setup requests interception using the given handlers.
5 | export const server = setupServer(...handlers)
6 |
--------------------------------------------------------------------------------
/src/models/StudentSurveyResponse.ts:
--------------------------------------------------------------------------------
1 | import { GetStudentSurveyResponses_studentSurveyResponses } from './../graphql/queries/__generated__/GetStudentSurveyResponses'
2 | export type StudentSurveyResponse = GetStudentSurveyResponses_studentSurveyResponses
3 |
--------------------------------------------------------------------------------
/src/graphql/queries/GetStudents.ts:
--------------------------------------------------------------------------------
1 | import { gql } from '@apollo/client'
2 |
3 | export const GetStudentsQuery = gql`
4 | query GetStudents {
5 | students {
6 | id
7 | dateOfBirth
8 | email
9 | name
10 | initials
11 | createdAt
12 | updatedAt
13 | }
14 | }
15 | `
16 |
--------------------------------------------------------------------------------
/src/graphql/queries/GetCurrentUser.ts:
--------------------------------------------------------------------------------
1 | import { gql } from "@apollo/client"
2 |
3 | export const GetCurrentUser = gql`
4 | query GetCurrentUser {
5 | currentUser {
6 | id
7 | name
8 | firstName
9 | lastName
10 | phoneNumber
11 | email
12 | initials
13 | role
14 | }
15 | }
16 | `
17 |
--------------------------------------------------------------------------------
/src/graphql/mutations/SignInMutation.ts:
--------------------------------------------------------------------------------
1 | import { gql } from "@apollo/client"
2 |
3 | export const SignInMutation = gql`
4 | mutation SignIn($credentials: AuthProviderCredentialsInput!) {
5 | signInUser(credentials: $credentials) {
6 | user {
7 | id
8 | name
9 | email
10 | }
11 | token
12 | }
13 | }
14 | `
15 |
--------------------------------------------------------------------------------
/src/graphql/mutations/CreateSurveyResponse.ts:
--------------------------------------------------------------------------------
1 | import { gql } from "@apollo/client"
2 |
3 | export const CreateSurveyResponse = gql`
4 | mutation CreateSurveyResponse($surveyId: ID!, $studentId: ID!, $userId: ID!) {
5 | createSurveyResponse(surveyId: $surveyId, studentId: $studentId, userId: $userId) {
6 | response {
7 | id
8 | }
9 | }
10 | }
11 | `
12 |
--------------------------------------------------------------------------------
/src/utils/test.tsx:
--------------------------------------------------------------------------------
1 | import { render, RenderResult } from "@testing-library/react"
2 | import { QueryClient, QueryClientProvider } from "react-query"
3 |
4 | const queryClient = new QueryClient()
5 |
6 | export const renderWithQueryProvider = (children: JSX.Element): RenderResult =>
7 | render(
8 | {children}
9 | )
10 |
--------------------------------------------------------------------------------
/src/graphql/queries/GetSurvey.ts:
--------------------------------------------------------------------------------
1 | import { gql } from '@apollo/client'
2 |
3 | export const GetSurveyQuery = gql`
4 | query GetSurvey($id: ID!) {
5 | survey(id: $id) {
6 | id
7 | name
8 | questions {
9 | id
10 | prompt
11 | type
12 | options {
13 | id
14 | label
15 | }
16 | }
17 | }
18 | }
19 | `
20 |
--------------------------------------------------------------------------------
/src/utils/getAge.ts:
--------------------------------------------------------------------------------
1 | export const getAge = (date: Date): number => {
2 | const today = new Date()
3 | const birthDate = new Date(date)
4 | const age = today.getFullYear() - birthDate.getFullYear()
5 | const month = today.getMonth() - birthDate.getMonth()
6 |
7 | if (month < 0 || (month === 0 && today.getDate() < birthDate.getDate())) {
8 | return age - 1
9 | }
10 |
11 | return age
12 | }
--------------------------------------------------------------------------------
/src/graphql/mutations/CreateSupportTicket.ts:
--------------------------------------------------------------------------------
1 | import { gql } from "@apollo/client"
2 |
3 | export const CreateSupportTicketMutation = gql`
4 | mutation CreateSupportTicket($surveyResponseId: ID!, $description: String) {
5 | createSupportTicket(
6 | surveyResponseId: $surveyResponseId,
7 | description: $description
8 | ) {
9 | supportTicket {
10 | id
11 | }
12 | }
13 | }
14 | `
15 |
--------------------------------------------------------------------------------
/src/graphql/queries/GetStudentSurveyResponses.ts:
--------------------------------------------------------------------------------
1 | import { gql } from '@apollo/client'
2 |
3 | export const GetStudentSurveyResponsesQuery = gql`
4 | query GetStudentSurveyResponses($id: ID!) {
5 | studentSurveyResponses(id: $id) {
6 | id
7 | student {
8 | name
9 | initials
10 | }
11 | meetingDuration {
12 | minutes
13 | startedAt
14 | }
15 | }
16 | }
17 | `
18 |
--------------------------------------------------------------------------------
/src/components/icons/vector.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/src/graphql/queries/GetStudent.ts:
--------------------------------------------------------------------------------
1 | import { gql } from '@apollo/client'
2 |
3 | export const GetStudentQuery = gql`
4 | query GetStudent($id: ID!) {
5 | student(id: $id) {
6 | id
7 | dateOfBirth
8 | email
9 | name
10 | createdAt
11 | updatedAt
12 | guardianName
13 | guardianPhoneNumber
14 | emergencyContactName
15 | emergencyContactPhoneNumber
16 | }
17 | }
18 | `
19 |
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | .btn {
6 | @apply px-10 py-5 uppercase bg-purple rounded inline-block text-gray-200;
7 | }
8 | .btn.disabled {
9 | @apply opacity-50 cursor-not-allowed pointer-events-none;
10 | }
11 |
12 | .form-field {
13 | @apply px-4 py-3 border-gray-400 border rounded bg-white text-gray-600 w-full;
14 | }
15 | .link {
16 | @apply text-secondary underline;
17 | }
18 |
--------------------------------------------------------------------------------
/.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 | .vscode/settings.json
26 |
--------------------------------------------------------------------------------
/src/graphql/queries/GetSurveyResponses.ts:
--------------------------------------------------------------------------------
1 | import { gql } from '@apollo/client'
2 |
3 | export const GetSurveyResponsesQuery = gql`
4 | query GetSurveyResponses {
5 | surveyResponses {
6 | id
7 | student {
8 | name
9 | initials
10 | }
11 | volunteer {
12 | id
13 | name
14 | }
15 | meetingDuration {
16 | minutes
17 | startedAt
18 | }
19 | }
20 | }
21 | `
22 |
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | name: jest
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | pull_request:
8 | branches:
9 | - main
10 |
11 | jobs:
12 | test:
13 | runs-on: ubuntu-latest
14 |
15 | steps:
16 | - uses: actions/checkout@v2.3.4
17 | - name: Setup Node
18 | uses: actions/setup-node@v2
19 | with:
20 | node-version: 16.x
21 | cache: 'yarn'
22 | - run: yarn install
23 | - run: yarn test
24 |
--------------------------------------------------------------------------------
/.github/workflows/lint.yml:
--------------------------------------------------------------------------------
1 | name: eslint & check-types
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | pull_request:
8 | branches:
9 | - main
10 |
11 | jobs:
12 | lint:
13 | runs-on: ubuntu-latest
14 |
15 | steps:
16 | - uses: actions/checkout@v2.3.4
17 | - name: Setup Node
18 | uses: actions/setup-node@v2
19 | with:
20 | node-version: 16.x
21 | cache: 'yarn'
22 | - run: yarn install
23 | - run: yarn lint
24 |
--------------------------------------------------------------------------------
/src/components/icons/timer.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/src/components/partials/Avatar.tsx:
--------------------------------------------------------------------------------
1 | export declare type AvatarPros = {
2 | initials: string;
3 | size?: number;
4 | };
5 |
6 | export const Avatar = ({
7 | initials,
8 | size = 10,
9 | }: AvatarPros): JSX.Element => {
10 | const sizeClasses = `w-${size} h-${size}`
11 |
12 | return (
13 |
14 | {initials}
15 |
16 | )
17 | }
18 |
--------------------------------------------------------------------------------
/src/graphql/queries/GetSurveyResponse.ts:
--------------------------------------------------------------------------------
1 | import { gql } from '@apollo/client'
2 |
3 | export const GetSurveyResponseQuery = gql`
4 | query GetSurveyResponse($id: ID!) {
5 | surveyResponse(id: $id) {
6 | id
7 | survey {
8 | id
9 | name
10 | questions {
11 | id
12 | heading
13 | prompt
14 | description
15 | type
16 | options {
17 | id
18 | label
19 | }
20 | }
21 | }
22 | }
23 | }
24 | `
25 |
--------------------------------------------------------------------------------
/src/graphql/mutations/UpsertSurveyQuestionResponse.ts:
--------------------------------------------------------------------------------
1 | import { gql } from "@apollo/client"
2 |
3 | export const UpsertSurveyQuestionResponseMutation = gql`
4 | mutation UpsertSurveyQuestionResponse($surveyResponseId: ID!, $questionId: ID!, $optionIds: [ID!], $reply: String) {
5 | upsertSurveyQuestionResponse(
6 | surveyResponseId: $surveyResponseId,
7 | questionId: $questionId,
8 | optionIds: $optionIds,
9 | reply: $reply
10 | ) {
11 | questionResponse {
12 | id
13 | }
14 | }
15 | }
16 | `
17 |
--------------------------------------------------------------------------------
/src/components/SurveyShow/TextQuestion.tsx:
--------------------------------------------------------------------------------
1 | interface TextQuestionProps {
2 | onAnswer: (value: string) => void,
3 | }
4 |
5 | export const TextQuestion = ({ onAnswer }: TextQuestionProps): JSX.Element => {
6 | const onChange = (event: React.ChangeEvent) => {
7 | onAnswer(event.target.value)
8 | }
9 |
10 | return (
11 |
19 | )
20 | }
21 |
--------------------------------------------------------------------------------
/src/graphql/mutations/UpdateUserMutation.ts:
--------------------------------------------------------------------------------
1 | import { gql } from "@apollo/client"
2 |
3 | export const updateUserMutation = gql`
4 | mutation UpdateUser($data: UpdateUserInput!) {
5 | user: updateUser(data: $data) {
6 | id
7 | firstName
8 | lastName
9 | email
10 | phoneNumber
11 | }
12 | }
13 | `
14 |
15 | export const updateUserPasswordMutation = gql`
16 | mutation UpdateUserPassword($oldPassword: String!, $password: String!) {
17 | success: updateUserPassword(oldPassword: $oldPassword, password: $password)
18 | }
19 | `
20 |
--------------------------------------------------------------------------------
/src/graphql/mutations/__generated__/UpdateUserPassword.ts:
--------------------------------------------------------------------------------
1 | /* tslint:disable */
2 | /* eslint-disable */
3 | // @generated
4 | // This file was automatically generated and should not be edited.
5 |
6 |
7 | // ====================================================
8 | // GraphQL mutation operation: UpdateUserPassword
9 | // ====================================================
10 |
11 |
12 | export interface UpdateUserPassword {
13 | success: boolean;
14 | }
15 |
16 | export interface UpdateUserPasswordVariables {
17 | oldPassword: string;
18 | password: string;
19 | }
20 |
--------------------------------------------------------------------------------
/src/reportWebVitals.ts:
--------------------------------------------------------------------------------
1 | import { ReportHandler } from "web-vitals"
2 |
3 | // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
4 | const reportWebVitals = (onPerfEntry?: ReportHandler) => {
5 | if (onPerfEntry && onPerfEntry instanceof Function) {
6 | import("web-vitals").then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
7 | getCLS(onPerfEntry)
8 | getFID(onPerfEntry)
9 | getFCP(onPerfEntry)
10 | getLCP(onPerfEntry)
11 | getTTFB(onPerfEntry)
12 | })
13 | }
14 | }
15 |
16 | export default reportWebVitals
17 |
--------------------------------------------------------------------------------
/src/lib/authentication.ts:
--------------------------------------------------------------------------------
1 | import { setContext } from '@apollo/client/link/context'
2 |
3 | const TokenKey = 'in-kind-app-token'
4 | const getToken = (): string | null => localStorage.getItem(TokenKey)
5 | const setToken = (token: string): void => localStorage.setItem(TokenKey, token)
6 |
7 | const authenticatedHttpLink = setContext((_, { headers }) => {
8 | const token = getToken()
9 | return {
10 | headers: {
11 | ...headers,
12 | authorization: token ? `Bearer ${token}` : "",
13 | }
14 | }
15 | })
16 |
17 | export { getToken, setToken, authenticatedHttpLink }
18 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": true,
6 | "skipLibCheck": true,
7 | "esModuleInterop": true,
8 | "allowSyntheticDefaultImports": true,
9 | "strict": true,
10 | "forceConsistentCasingInFileNames": true,
11 | "noFallthroughCasesInSwitch": true,
12 | "module": "esnext",
13 | "moduleResolution": "node",
14 | "resolveJsonModule": true,
15 | "isolatedModules": true,
16 | "noEmit": true,
17 | "jsx": "react-jsx",
18 | "baseUrl": "./src"
19 | },
20 | "include": ["src"]
21 | }
22 |
--------------------------------------------------------------------------------
/src/components/partials/UserItem.tsx:
--------------------------------------------------------------------------------
1 | import { Avatar } from "./Avatar"
2 |
3 | interface UserItemProps {
4 | header: string,
5 | initials: string,
6 | subHeader: string,
7 | }
8 |
9 | export const UserItem = ({ header, initials, subHeader }: UserItemProps): JSX.Element => (
10 |
11 |
12 |
13 |
14 |
{header}
15 |
{subHeader}
16 |
17 |
18 |
19 | )
--------------------------------------------------------------------------------
/src/components/Landing.tsx:
--------------------------------------------------------------------------------
1 | import { useQuery } from "@apollo/client"
2 |
3 | import { Login } from "components/Login"
4 | import { MainNav } from "components/MainNav"
5 | import { GetCurrentUser } from "graphql/queries/GetCurrentUser"
6 |
7 | import { QueryError } from "./partials/QueryError"
8 |
9 | export const Landing = (): JSX.Element => {
10 | const { data, loading, error } = useQuery(GetCurrentUser)
11 |
12 | if (loading || !data) return loading...
13 | if (error) return
14 |
15 | return (
16 |
17 | {data.currentUser ? (
18 |
19 | ) : (
20 |
21 | )}
22 |
23 | )
24 | }
25 |
--------------------------------------------------------------------------------
/src/index.tsx:
--------------------------------------------------------------------------------
1 | import React from "react"
2 | import ReactDOM from "react-dom"
3 | import "./index.css"
4 | import App from "components/App"
5 | import reportWebVitals from "./reportWebVitals"
6 |
7 | if (process.env.NODE_ENV === "development") {
8 | // eslint-disable-next-line @typescript-eslint/no-var-requires
9 | const { worker } = require("./mocks/browser")
10 | worker.start()
11 | }
12 |
13 | ReactDOM.render(
14 |
15 |
16 | ,
17 | document.getElementById("root")
18 | )
19 |
20 | // If you want to start measuring performance in your app, pass a function
21 | // to log results (for example: reportWebVitals(console.log))
22 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
23 | reportWebVitals()
24 |
--------------------------------------------------------------------------------
/src/graphql/queries/__generated__/GetStudents.ts:
--------------------------------------------------------------------------------
1 | /* tslint:disable */
2 | /* eslint-disable */
3 | // @generated
4 | // This file was automatically generated and should not be edited.
5 |
6 |
7 | // ====================================================
8 | // GraphQL query operation: GetStudents
9 | // ====================================================
10 |
11 |
12 | export interface GetStudents_students {
13 | __typename: "Student";
14 | id: string;
15 | dateOfBirth: any | null;
16 | email: string | null;
17 | name: string;
18 | initials: string;
19 | createdAt: any;
20 | updatedAt: any;
21 | }
22 |
23 | export interface GetStudents {
24 | /**
25 | * All students associated with signed in volunteer
26 | */
27 | students: GetStudents_students[] | null;
28 | }
29 |
--------------------------------------------------------------------------------
/__generated__/globalTypes.ts:
--------------------------------------------------------------------------------
1 | /* tslint:disable */
2 | /* eslint-disable */
3 | // @generated
4 | // This file was automatically generated and should not be edited.
5 |
6 |
7 | //==============================================================
8 | // START Enums and Input Objects
9 | //==============================================================
10 |
11 |
12 | export interface AuthProviderCredentialsInput {
13 | email: string;
14 | password: string;
15 | }
16 |
17 | export interface UpdateUserInput {
18 | firstName: string;
19 | lastName: string;
20 | email?: string | null;
21 | phoneNumber?: string | null;
22 | }
23 |
24 | //==============================================================
25 | // END Enums and Input Objects
26 | //==============================================================
27 |
28 |
--------------------------------------------------------------------------------
/src/graphql/mutations/__generated__/UpdateUser.ts:
--------------------------------------------------------------------------------
1 | /* tslint:disable */
2 | /* eslint-disable */
3 | // @generated
4 | // This file was automatically generated and should not be edited.
5 |
6 |
7 | import { UpdateUserInput } from "./../../../../__generated__/globalTypes";
8 |
9 | // ====================================================
10 | // GraphQL mutation operation: UpdateUser
11 | // ====================================================
12 |
13 |
14 | export interface UpdateUser_user {
15 | __typename: "User";
16 | id: string;
17 | firstName: string;
18 | lastName: string;
19 | email: string | null;
20 | phoneNumber: string | null;
21 | }
22 |
23 | export interface UpdateUser {
24 | user: UpdateUser_user;
25 | }
26 |
27 | export interface UpdateUserVariables {
28 | data: UpdateUserInput;
29 | }
30 |
--------------------------------------------------------------------------------
/src/graphql/queries/__generated__/GetCurrentUser.ts:
--------------------------------------------------------------------------------
1 | /* tslint:disable */
2 | /* eslint-disable */
3 | // @generated
4 | // This file was automatically generated and should not be edited.
5 |
6 |
7 | // ====================================================
8 | // GraphQL query operation: GetCurrentUser
9 | // ====================================================
10 |
11 |
12 | export interface GetCurrentUser_currentUser {
13 | __typename: "User";
14 | id: string;
15 | name: string;
16 | firstName: string;
17 | lastName: string;
18 | phoneNumber: string | null;
19 | email: string | null;
20 | initials: string;
21 | role: string | null;
22 | }
23 |
24 | export interface GetCurrentUser {
25 | /**
26 | * Currently logged in user
27 | */
28 | currentUser: GetCurrentUser_currentUser | null;
29 | }
30 |
--------------------------------------------------------------------------------
/src/components/partials/SessionItem.tsx:
--------------------------------------------------------------------------------
1 | import { SurveyResponse } from "models/SurveyResponse"
2 | import { StudentSurveyResponse } from "models/StudentSurveyResponse"
3 |
4 | import { UserItem } from "./UserItem"
5 |
6 | interface SessionItemProps {
7 | response: SurveyResponse | StudentSurveyResponse,
8 | }
9 |
10 | export const SessionItem = ({ response }: SessionItemProps): JSX.Element => {
11 | const name = `${response.student.name}`
12 | const date = new Date(response?.meetingDuration?.startedAt || "2021")
13 | const formattedDate = `${date.toLocaleString('default', { month: 'long' })} ${date.getDate()}, ${date.getFullYear()}`
14 |
15 | return (
16 |
22 | )
23 | }
24 |
--------------------------------------------------------------------------------
/src/components/EditAccountSuccess.tsx:
--------------------------------------------------------------------------------
1 | import { Link, useLocation } from "react-router-dom"
2 | import celebrating from "./icons/celebrating.svg"
3 |
4 | export const EditAccountSuccess = (): JSX.Element => {
5 | const location = useLocation()
6 | const query = new URLSearchParams(location.search)
7 | const text = query.get("text")
8 |
9 | if (!text) {
10 | throw "Please, specify the text to be shown in a query string param named `text` - i.e. ?text=The Text"
11 | }
12 |
13 | return (
14 |
15 |
Success!
16 |
{text}
17 |
Return to your profile.
18 |
19 |
20 | )
21 | }
22 |
--------------------------------------------------------------------------------
/src/components/WelcomeDashboard/StudentSection.tsx:
--------------------------------------------------------------------------------
1 | import React from "react"
2 | import { Link } from "react-router-dom"
3 |
4 | import { Student } from "models/Student"
5 | import { getAge } from "utils/getAge"
6 |
7 | import { UserItem } from "../partials/UserItem"
8 |
9 | interface StudentSectionProps {
10 | students: Student[];
11 | }
12 |
13 | export const StudentSection = ({
14 | students,
15 | }: StudentSectionProps): JSX.Element => {
16 | return (
17 |
18 | {students.map((student: Student) => {
19 | return (
20 |
21 |
26 |
27 | )
28 | })}
29 |
30 | )
31 | }
32 |
--------------------------------------------------------------------------------
/src/graphql/mutations/__generated__/SignIn.ts:
--------------------------------------------------------------------------------
1 | /* tslint:disable */
2 | /* eslint-disable */
3 | // @generated
4 | // This file was automatically generated and should not be edited.
5 |
6 |
7 | import { AuthProviderCredentialsInput } from "./../../../../__generated__/globalTypes";
8 |
9 | // ====================================================
10 | // GraphQL mutation operation: SignIn
11 | // ====================================================
12 |
13 |
14 | export interface SignIn_signInUser_user {
15 | __typename: "User";
16 | id: string;
17 | name: string;
18 | email: string | null;
19 | }
20 |
21 | export interface SignIn_signInUser {
22 | __typename: "SignInUserPayload";
23 | user: SignIn_signInUser_user | null;
24 | token: string | null;
25 | }
26 |
27 | export interface SignIn {
28 | signInUser: SignIn_signInUser;
29 | }
30 |
31 | export interface SignInVariables {
32 | credentials: AuthProviderCredentialsInput;
33 | }
34 |
--------------------------------------------------------------------------------
/src/graphql/queries/__generated__/GetStudent.ts:
--------------------------------------------------------------------------------
1 | /* tslint:disable */
2 | /* eslint-disable */
3 | // @generated
4 | // This file was automatically generated and should not be edited.
5 |
6 |
7 | // ====================================================
8 | // GraphQL query operation: GetStudent
9 | // ====================================================
10 |
11 |
12 | export interface GetStudent_student {
13 | __typename: "Student";
14 | id: string;
15 | dateOfBirth: any | null;
16 | email: string | null;
17 | name: string;
18 | createdAt: any;
19 | updatedAt: any;
20 | guardianName: string | null;
21 | guardianPhoneNumber: string | null;
22 | emergencyContactName: string | null;
23 | emergencyContactPhoneNumber: string | null;
24 | }
25 |
26 | export interface GetStudent {
27 | /**
28 | * A single student
29 | */
30 | student: GetStudent_student;
31 | }
32 |
33 | export interface GetStudentVariables {
34 | id: string;
35 | }
36 |
--------------------------------------------------------------------------------
/src/graphql/mutations/__generated__/CreateSupportTicket.ts:
--------------------------------------------------------------------------------
1 | /* tslint:disable */
2 | /* eslint-disable */
3 | // @generated
4 | // This file was automatically generated and should not be edited.
5 |
6 |
7 | // ====================================================
8 | // GraphQL mutation operation: CreateSupportTicket
9 | // ====================================================
10 |
11 |
12 | export interface CreateSupportTicket_createSupportTicket_supportTicket {
13 | __typename: "SupportTicket";
14 | id: string;
15 | }
16 |
17 | export interface CreateSupportTicket_createSupportTicket {
18 | __typename: "CreateSupportTicketPayload";
19 | supportTicket: CreateSupportTicket_createSupportTicket_supportTicket;
20 | }
21 |
22 | export interface CreateSupportTicket {
23 | createSupportTicket: CreateSupportTicket_createSupportTicket | null;
24 | }
25 |
26 | export interface CreateSupportTicketVariables {
27 | surveyResponseId: string;
28 | description?: string | null;
29 | }
30 |
--------------------------------------------------------------------------------
/src/graphql/mutations/__generated__/CreateSurveyResponse.ts:
--------------------------------------------------------------------------------
1 | /* tslint:disable */
2 | /* eslint-disable */
3 | // @generated
4 | // This file was automatically generated and should not be edited.
5 |
6 |
7 | // ====================================================
8 | // GraphQL mutation operation: CreateSurveyResponse
9 | // ====================================================
10 |
11 |
12 | export interface CreateSurveyResponse_createSurveyResponse_response {
13 | __typename: "SurveyResponse";
14 | id: string;
15 | }
16 |
17 | export interface CreateSurveyResponse_createSurveyResponse {
18 | __typename: "CreateSurveyResponsePayload";
19 | response: CreateSurveyResponse_createSurveyResponse_response;
20 | }
21 |
22 | export interface CreateSurveyResponse {
23 | createSurveyResponse: CreateSurveyResponse_createSurveyResponse | null;
24 | }
25 |
26 | export interface CreateSurveyResponseVariables {
27 | surveyId: string;
28 | studentId: string;
29 | userId: string;
30 | }
31 |
--------------------------------------------------------------------------------
/src/components/SessionDuration/style.scss:
--------------------------------------------------------------------------------
1 | input,
2 | input::placeholder {
3 | color: #546E7A;
4 | font-size: 16px;
5 | }
6 |
7 | div.react-datepicker__header {
8 | background-color: white;
9 | border-bottom: none;
10 |
11 | .react-datepicker__current-month {
12 | color: #FF3B30
13 | }
14 |
15 | .react-datepicker__day-name {
16 | color: rgba(60, 60, 67, 0.3);
17 | text-transform: uppercase;
18 | }
19 |
20 | }
21 |
22 | div.react-datepicker__week {
23 | & > div.react-datepicker__day {
24 | color: #FF3B30;
25 |
26 | &.react-datepicker__day--outside-month {
27 | color: white;
28 | }
29 |
30 | &.react-datepicker__day--selected {
31 | background-color: rgba(60, 60, 67, 0.3);
32 | }
33 | }
34 | }
35 |
36 | .submit-button {
37 | position: absolute;
38 | left: 0;
39 | right: 0;
40 | bottom: 100px;
41 | margin-left: auto;
42 | margin-right: auto;
43 | }
44 |
45 | .hide { visibility: collapse }
46 | .show { visibility: visible }
--------------------------------------------------------------------------------
/src/components/icons/homeSelected.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | ic_fluent_home_24_filled
5 | Created with Sketch.
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/external/graphql/schema.graphql:
--------------------------------------------------------------------------------
1 | input AuthProviderCredentialsInput {
2 | email: String!
3 | password: String!
4 | }
5 |
6 | """
7 | An ISO 8601-encoded datetime
8 | """
9 | scalar ISO8601DateTime
10 |
11 | type Mutation {
12 | signInUser(credentials: AuthProviderCredentialsInput): SignInUserPayload
13 | }
14 |
15 | type Query {
16 | currentUser: User
17 |
18 | """
19 | A single student
20 | """
21 | student(id: ID!): Student!
22 |
23 | """
24 | All students associated with signed in volunteer
25 | """
26 | students: [Student!]
27 | }
28 |
29 | """
30 | Autogenerated return type of SignInUser
31 | """
32 | type SignInUserPayload {
33 | token: String
34 | user: User
35 | }
36 |
37 | type Student {
38 | createdAt: ISO8601DateTime!
39 | dateOfBirth: ISO8601DateTime
40 | email: String
41 | id: ID!
42 | name: String!
43 | nickname: String
44 | updatedAt: ISO8601DateTime!
45 | }
46 |
47 | type User {
48 | createdAt: ISO8601DateTime!
49 | email: String
50 | id: ID!
51 | name: String
52 | role: String
53 | updatedAt: ISO8601DateTime!
54 | }
55 |
--------------------------------------------------------------------------------
/src/graphql/queries/__generated__/GetSurvey.ts:
--------------------------------------------------------------------------------
1 | /* tslint:disable */
2 | /* eslint-disable */
3 | // @generated
4 | // This file was automatically generated and should not be edited.
5 |
6 |
7 | // ====================================================
8 | // GraphQL query operation: GetSurvey
9 | // ====================================================
10 |
11 |
12 | export interface GetSurvey_survey_questions_options {
13 | __typename: "QuestionOption";
14 | id: string;
15 | label: string;
16 | }
17 |
18 | export interface GetSurvey_survey_questions {
19 | __typename: "Question";
20 | id: string;
21 | prompt: string;
22 | type: string;
23 | options: GetSurvey_survey_questions_options[] | null;
24 | }
25 |
26 | export interface GetSurvey_survey {
27 | __typename: "Survey";
28 | id: string;
29 | name: string;
30 | questions: GetSurvey_survey_questions[] | null;
31 | }
32 |
33 | export interface GetSurvey {
34 | /**
35 | * A single survey
36 | */
37 | survey: GetSurvey_survey;
38 | }
39 |
40 | export interface GetSurveyVariables {
41 | id: string;
42 | }
43 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2021 Ruby for Good
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6 |
7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8 |
9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
10 |
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "parser": "@typescript-eslint/parser",
3 | "parserOptions": {
4 | "ecmaVersion": 2019,
5 | "sourceType": "module",
6 | "ecmaFeatures": {
7 | "jsx": true
8 | }
9 | },
10 | "settings": {
11 | "react": {
12 | "version": "detect"
13 | }
14 | },
15 | "extends": ["eslint:recommended"],
16 | "rules": {
17 | "semi": ["error", "never"],
18 | "strict": ["error", "never"],
19 | "react/prop-types": ["off"]
20 | },
21 | "env": {
22 | "jest": true,
23 | "browser": true,
24 | "node": true
25 | },
26 | "overrides": [
27 | {
28 | "files": "**/*.+(ts|tsx)",
29 | "parser": "@typescript-eslint/parser",
30 | "parserOptions": {
31 | "project": "./tsconfig.json"
32 | },
33 | "plugins": ["@typescript-eslint/eslint-plugin"],
34 | "extends": [
35 | "plugin:@typescript-eslint/eslint-recommended",
36 | "plugin:@typescript-eslint/recommended"
37 | ],
38 | "rules": {
39 | "@typescript-eslint/ban-ts-comment": ["off"]
40 | }
41 | }
42 | ]
43 | }
44 |
--------------------------------------------------------------------------------
/src/__test__/components/VolunteerProfilePage.test.tsx:
--------------------------------------------------------------------------------
1 | // @ts-nocheck
2 |
3 | import { render, screen, waitFor } from "@testing-library/react"
4 | import { Account } from "components/Account"
5 | import { User } from "models/User"
6 | import { BrowserRouter as Router } from 'react-router-dom'
7 |
8 | test("renders the volunteer's profile page", async () => {
9 | const fillAccountIcon = (a: boolean) => undefined
10 | const user: User = { firstName: "John", lastName: "Smith", phoneNumber: "111-222-3333", email: "john@smith.com" }
11 |
12 | render(
13 |
14 |
15 |
16 | )
17 | await waitFor(() => {
18 | const firstName = screen.getByText(/John/)
19 | const lastName = screen.getByText(/Smith/)
20 | const email = screen.getByText(/john@smith/)
21 | const phoneNumber = screen.getByText(/111-222-3333/)
22 |
23 | expect(firstName).toBeInTheDocument()
24 | expect(lastName).toBeInTheDocument()
25 | expect(email).toBeInTheDocument()
26 | expect(phoneNumber).toBeInTheDocument()
27 | })
28 | })
29 |
--------------------------------------------------------------------------------
/src/setupTests.ts:
--------------------------------------------------------------------------------
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"
6 | import "@testing-library/jest-dom/extend-expect"
7 | import { server } from "./mocks/server"
8 |
9 | beforeAll(() => {
10 | // Enable the mocking in tests.
11 | server.listen()
12 | })
13 |
14 | afterEach(() => {
15 | // Reset any runtime handlers tests may use.
16 | server.resetHandlers()
17 | })
18 |
19 | afterAll(() => {
20 | // Clean up once the tests are done.
21 | server.close()
22 | })
23 |
24 | // The last line is commented out because it causes a Typescript error.
25 | // src/setupTests.ts:24:1 - error TS2322: Type 'undefined' is not assignable to type '{ new (): XMLHttpRequest; prototype: XMLHttpRequest; readonly DONE: number; readonly HEADERS_RECEIVED: number; readonly LOADING: number; readonly OPENED: number; readonly UNSENT: number; }'.
26 |
27 | // Evaluate reenabling if needed.
28 | // global.XMLHttpRequest = undefined;
29 |
--------------------------------------------------------------------------------
/src/components/SurveyShow/SingleSelectionOption.tsx:
--------------------------------------------------------------------------------
1 | import { GetSurvey_survey_questions_options } from "graphql/queries/__generated__/GetSurvey"
2 |
3 | interface SingleSelectionOptionProps {
4 | options: GetSurvey_survey_questions_options[],
5 | onAnswer: (value: string) => void,
6 | }
7 |
8 | export const SingleSelectionOption = ({ options, onAnswer }: SingleSelectionOptionProps): JSX.Element | null => {
9 | const onSelectAnswer = (event: React.ChangeEvent) => {
10 | onAnswer(event.target.value)
11 | }
12 |
13 | return (
14 |
30 | )
31 | }
32 |
33 |
--------------------------------------------------------------------------------
/src/graphql/mutations/__generated__/CreateSurveyQuestionResponse.ts:
--------------------------------------------------------------------------------
1 | /* tslint:disable */
2 | /* eslint-disable */
3 | // @generated
4 | // This file was automatically generated and should not be edited.
5 |
6 |
7 | // ====================================================
8 | // GraphQL mutation operation: CreateSurveyQuestionResponse
9 | // ====================================================
10 |
11 |
12 | export interface CreateSurveyQuestionResponse_createSurveyQuestionResponse_questionResponse {
13 | __typename: "SurveyQuestionResponse";
14 | id: string;
15 | }
16 |
17 | export interface CreateSurveyQuestionResponse_createSurveyQuestionResponse {
18 | __typename: "CreateSurveyQuestionResponsePayload";
19 | questionResponse: CreateSurveyQuestionResponse_createSurveyQuestionResponse_questionResponse;
20 | }
21 |
22 | export interface CreateSurveyQuestionResponse {
23 | createSurveyQuestionResponse: CreateSurveyQuestionResponse_createSurveyQuestionResponse | null;
24 | }
25 |
26 | export interface CreateSurveyQuestionResponseVariables {
27 | surveyResponseId: string;
28 | questionId: string;
29 | optionIds?: string[] | null;
30 | reply?: string | null;
31 | }
32 |
--------------------------------------------------------------------------------
/src/graphql/mutations/__generated__/UpsertSurveyQuestionResponse.ts:
--------------------------------------------------------------------------------
1 | /* tslint:disable */
2 | /* eslint-disable */
3 | // @generated
4 | // This file was automatically generated and should not be edited.
5 |
6 |
7 | // ====================================================
8 | // GraphQL mutation operation: UpsertSurveyQuestionResponse
9 | // ====================================================
10 |
11 |
12 | export interface UpsertSurveyQuestionResponse_upsertSurveyQuestionResponse_questionResponse {
13 | __typename: "SurveyQuestionResponse";
14 | id: string;
15 | }
16 |
17 | export interface UpsertSurveyQuestionResponse_upsertSurveyQuestionResponse {
18 | __typename: "UpsertSurveyQuestionResponsePayload";
19 | questionResponse: UpsertSurveyQuestionResponse_upsertSurveyQuestionResponse_questionResponse;
20 | }
21 |
22 | export interface UpsertSurveyQuestionResponse {
23 | upsertSurveyQuestionResponse: UpsertSurveyQuestionResponse_upsertSurveyQuestionResponse | null;
24 | }
25 |
26 | export interface UpsertSurveyQuestionResponseVariables {
27 | surveyResponseId: string;
28 | questionId: string;
29 | optionIds?: string[] | null;
30 | reply?: string | null;
31 | }
32 |
--------------------------------------------------------------------------------
/src/__test__/components/StudentSection.test.tsx:
--------------------------------------------------------------------------------
1 | // @ts-nocheck
2 |
3 | import { screen, waitFor } from "@testing-library/react"
4 | import { StudentSection } from "components/WelcomeDashboard/StudentSection"
5 | import { Student } from "models/Student"
6 | import { renderWithQueryProvider } from "utils/test"
7 | import { Router } from 'react-router-dom'
8 | import { createMemoryHistory } from 'history'
9 |
10 | test("renders student section", async () => {
11 | const date = new Date()
12 | const subtractionOfYears = 13
13 | date.setFullYear(date.getFullYear() - subtractionOfYears)
14 | const formattedDate = date.toISOString().slice(0,10)
15 |
16 | const students: Student[] = [{ name: "John Smith", dateOfBirth: formattedDate }]
17 | const history = createMemoryHistory()
18 |
19 | renderWithQueryProvider(
20 |
21 |
22 |
23 | )
24 | await waitFor(() => {
25 | const studentName = screen.getByText(/John Smith/)
26 | const studentDateOfBirth = screen.getByText(/Age 13/)
27 |
28 | expect(studentName).toBeInTheDocument()
29 | expect(studentDateOfBirth).toBeInTheDocument()
30 | })
31 | })
32 |
--------------------------------------------------------------------------------
/src/__test__/components/EditVolunteerPasswordPage.test.tsx:
--------------------------------------------------------------------------------
1 | // @ts-nocheck
2 |
3 | import { render, screen, waitFor } from "@testing-library/react"
4 | import { MockedProvider } from '@apollo/client/testing'
5 | import { QueryClient, QueryClientProvider } from "react-query"
6 | import { EditAccountPassword } from "components/EditAccountPassword"
7 | import { User } from "models/User"
8 | import { BrowserRouter as Router } from 'react-router-dom'
9 |
10 | test("renders the edit volunteer password page", async () => {
11 | const fillAccountIcon = (a: boolean) => undefined
12 | const user: User = {}
13 |
14 | render(
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 | )
23 | await waitFor(() => {
24 | expect(screen.getByPlaceholderText(/Old Password/)).toBeInTheDocument()
25 | expect(screen.getByPlaceholderText(/^New Password$/)).toBeInTheDocument()
26 | expect(screen.getByPlaceholderText(/Reenter New Password/)).toBeInTheDocument()
27 | expect(screen.getByText(/Done/)).toBeInTheDocument()
28 | })
29 | })
30 |
--------------------------------------------------------------------------------
/src/components/icons/personSelected.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | ic_fluent_person_24_filled
5 | Created with Sketch.
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/src/components/SurveyShow/SurveyProgressBar.tsx:
--------------------------------------------------------------------------------
1 | import { GetSurvey_survey_questions } from "graphql/queries/__generated__/GetSurvey"
2 | import Vector from "../icons/vector.svg"
3 |
4 | interface SurveyProgressBarProps {
5 | questions: GetSurvey_survey_questions[],
6 | currentQuestionIndex: number,
7 | goToPreviousQuestion: () => void,
8 | }
9 |
10 | export const SurveyProgressBar = ({ questions, currentQuestionIndex, goToPreviousQuestion }: SurveyProgressBarProps): JSX.Element => {
11 | const progress = ((currentQuestionIndex + 1)/ questions.length) * 100
12 | return (
13 |
14 |
15 |
16 |
17 |
18 |
19 | {`${currentQuestionIndex + 1} of ${questions.length}`}
20 |
21 |
22 |
23 |
30 |
31 | )
32 | }
33 |
--------------------------------------------------------------------------------
/src/graphql/queries/__generated__/GetSurveyResponses.ts:
--------------------------------------------------------------------------------
1 | /* tslint:disable */
2 | /* eslint-disable */
3 | // @generated
4 | // This file was automatically generated and should not be edited.
5 |
6 |
7 | // ====================================================
8 | // GraphQL query operation: GetSurveyResponses
9 | // ====================================================
10 |
11 |
12 | export interface GetSurveyResponses_surveyResponses_student {
13 | __typename: "Student";
14 | name: string;
15 | initials: string;
16 | }
17 |
18 | export interface GetSurveyResponses_surveyResponses_volunteer {
19 | __typename: "User";
20 | id: string;
21 | name: string;
22 | }
23 |
24 | export interface GetSurveyResponses_surveyResponses_meetingDuration {
25 | __typename: "MeetingDuration";
26 | minutes: number;
27 | startedAt: any;
28 | }
29 |
30 | export interface GetSurveyResponses_surveyResponses {
31 | __typename: "SurveyResponse";
32 | id: string;
33 | student: GetSurveyResponses_surveyResponses_student;
34 | volunteer: GetSurveyResponses_surveyResponses_volunteer;
35 | meetingDuration: GetSurveyResponses_surveyResponses_meetingDuration | null;
36 | }
37 |
38 | export interface GetSurveyResponses {
39 | /**
40 | * All survey_responses associated with signed in volunteer
41 | */
42 | surveyResponses: GetSurveyResponses_surveyResponses[] | null;
43 | }
44 |
--------------------------------------------------------------------------------
/src/graphql/queries/__generated__/GetStudentSurveyResponses.ts:
--------------------------------------------------------------------------------
1 | /* tslint:disable */
2 | /* eslint-disable */
3 | // @generated
4 | // This file was automatically generated and should not be edited.
5 |
6 |
7 | // ====================================================
8 | // GraphQL query operation: GetStudentSurveyResponses
9 | // ====================================================
10 |
11 |
12 | export interface GetStudentSurveyResponses_studentSurveyResponses_student {
13 | __typename: "Student";
14 | name: string;
15 | initials: string;
16 | }
17 |
18 | export interface GetStudentSurveyResponses_studentSurveyResponses_meetingDuration {
19 | __typename: "MeetingDuration";
20 | minutes: number;
21 | startedAt: any;
22 | }
23 |
24 | export interface GetStudentSurveyResponses_studentSurveyResponses {
25 | __typename: "SurveyResponse";
26 | id: string;
27 | student: GetStudentSurveyResponses_studentSurveyResponses_student;
28 | meetingDuration: GetStudentSurveyResponses_studentSurveyResponses_meetingDuration | null;
29 | }
30 |
31 | export interface GetStudentSurveyResponses {
32 | /**
33 | * All survey_responses for a specific student associated with signed in volunteer
34 | */
35 | studentSurveyResponses: GetStudentSurveyResponses_studentSurveyResponses[] | null;
36 | }
37 |
38 | export interface GetStudentSurveyResponsesVariables {
39 | id: string;
40 | }
41 |
--------------------------------------------------------------------------------
/src/components/App.tsx:
--------------------------------------------------------------------------------
1 | import { QueryClient, QueryClientProvider } from "react-query"
2 | import {
3 | ApolloClient,
4 | ApolloProvider,
5 | createHttpLink,
6 | InMemoryCache,
7 | NormalizedCacheObject,
8 | } from "@apollo/client"
9 | import { authenticatedHttpLink, setToken } from "lib/authentication"
10 | import { Landing } from "components/Landing"
11 |
12 | const urlSearchParams = new URLSearchParams(window.location.search)
13 | const params = Object.fromEntries(urlSearchParams.entries())
14 | if (params.token) {
15 | setToken(params.token)
16 | }
17 |
18 | const queryClient = new QueryClient()
19 |
20 | const uriLink = () => {
21 | // if (window.location.href.indexOf("inkind-volunteer.herokuapp.com") > -1) {
22 | return "https://inkind-admining.herokuapp.com/graphql"
23 | // } else {
24 | // return "http://localhost:3001/graphql"
25 | // }
26 | }
27 |
28 | const link = createHttpLink({
29 | uri: uriLink(),
30 | credentials: "include",
31 | })
32 |
33 | const apolloClient: ApolloClient = new ApolloClient({
34 | cache: new InMemoryCache(),
35 | link: authenticatedHttpLink.concat(link),
36 | })
37 |
38 | const App = (): JSX.Element => {
39 | return (
40 |
41 |
42 |
43 |
44 |
45 | )
46 | }
47 |
48 | export default App
49 |
--------------------------------------------------------------------------------
/src/components/WelcomeDashboard/index.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect } from "react"
2 | import { useQuery } from "@apollo/client"
3 |
4 | import { GetStudentsQuery } from "graphql/queries/GetStudents"
5 | import { GetStudents } from "graphql/queries/__generated__/GetStudents"
6 |
7 | import { User } from "models/User"
8 |
9 | import { QueryError } from "../partials/QueryError"
10 | import { StudentSection } from "./StudentSection"
11 | import { SessionSection } from "./SessionSection"
12 |
13 | interface WelcomeDashboardProps {
14 | fillHomeIcon: (a: boolean) => void;
15 | user: User;
16 | }
17 |
18 | export const WelcomeDashboard = ({
19 | fillHomeIcon,
20 | user,
21 | }: WelcomeDashboardProps): JSX.Element => {
22 | useEffect(() => {
23 | fillHomeIcon(true)
24 | })
25 |
26 | const { data, loading, error } = useQuery(GetStudentsQuery)
27 |
28 | if (loading) { return Loading ....
}
29 | if (error) { return }
30 |
31 | return (
32 |
33 |
34 |
Hi {user.name?.split(" ")[0]}!
35 |
My Students
36 |
37 |
38 |
39 |
40 | )
41 | }
42 |
--------------------------------------------------------------------------------
/src/graphql/queries/__generated__/GetSurveyResponse.ts:
--------------------------------------------------------------------------------
1 | /* tslint:disable */
2 | /* eslint-disable */
3 | // @generated
4 | // This file was automatically generated and should not be edited.
5 |
6 |
7 | // ====================================================
8 | // GraphQL query operation: GetSurveyResponse
9 | // ====================================================
10 |
11 |
12 | export interface GetSurveyResponse_surveyResponse_survey_questions_options {
13 | __typename: "QuestionOption";
14 | id: string;
15 | label: string;
16 | }
17 |
18 | export interface GetSurveyResponse_surveyResponse_survey_questions {
19 | __typename: "Question";
20 | id: string;
21 | heading: string | null;
22 | prompt: string;
23 | description: string | null;
24 | type: string;
25 | options: GetSurveyResponse_surveyResponse_survey_questions_options[] | null;
26 | }
27 |
28 | export interface GetSurveyResponse_surveyResponse_survey {
29 | __typename: "Survey";
30 | id: string;
31 | name: string;
32 | questions: GetSurveyResponse_surveyResponse_survey_questions[] | null;
33 | }
34 |
35 | export interface GetSurveyResponse_surveyResponse {
36 | __typename: "SurveyResponse";
37 | id: string;
38 | survey: GetSurveyResponse_surveyResponse_survey;
39 | }
40 |
41 | export interface GetSurveyResponse {
42 | /**
43 | * A single survey response
44 | */
45 | surveyResponse: GetSurveyResponse_surveyResponse;
46 | }
47 |
48 | export interface GetSurveyResponseVariables {
49 | id: string;
50 | }
51 |
--------------------------------------------------------------------------------
/.github/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | Resolves #000
2 |
3 |
16 |
17 | ### Description
18 |
19 | - Include summary of change and what issue is fixed, including relevant motivation and context.
20 | - Explain the need for any added dependencies for this change.
21 | - How have you tested this?
22 | - Anything else we should know about?
23 |
24 | ### Type of change
25 |
26 |
27 |
28 | - Bug fix (non-breaking change which fixes an issue)
29 | - New feature (non-breaking change which adds functionality)
30 | - Breaking change (fix or feature that would cause existing functionality to not work as expected)
31 | - This change requires a documentation update
32 | - Documentation update
33 |
34 | ### Screenshots
35 |
36 |
37 |
--------------------------------------------------------------------------------
/src/components/SurveyShow/MultipleSelectionOption.tsx:
--------------------------------------------------------------------------------
1 | import { GetSurvey_survey_questions_options } from "graphql/queries/__generated__/GetSurvey"
2 |
3 | interface MultipleSelectionOptionProps {
4 | options: GetSurvey_survey_questions_options[],
5 | onAnswer: any,
6 | }
7 |
8 | export const MultipleSelectionOption = ({ options, onAnswer }: MultipleSelectionOptionProps): JSX.Element | null => {
9 | const onSelectAnswer = (event: React.ChangeEvent) => {
10 | const selectedOption = event.target.value
11 |
12 | onAnswer((currentOptions: string) => {
13 | const currentOptionsSet = new Set(currentOptions)
14 | if (currentOptionsSet.has(selectedOption)) {
15 | currentOptionsSet.delete(selectedOption)
16 | } else {
17 | currentOptionsSet.add(selectedOption)
18 | }
19 |
20 | return Array.from(currentOptionsSet)
21 | })
22 | }
23 |
24 | return (
25 |
41 | )
42 | }
43 |
44 |
--------------------------------------------------------------------------------
/src/__test__/components/EditVolunteerPage.test.tsx:
--------------------------------------------------------------------------------
1 | // @ts-nocheck
2 |
3 | import { render, screen, waitFor } from "@testing-library/react"
4 | import { MockedProvider } from '@apollo/client/testing'
5 | import { QueryClient, QueryClientProvider } from "react-query"
6 | import { EditAccount } from "components/EditAccount"
7 | import { User } from "models/User"
8 | import { BrowserRouter as Router } from 'react-router-dom'
9 |
10 | test("renders the edit volunteer information page", async () => {
11 | const user: User = { firstName: "John", lastName: "Smith", phoneNumber: "111-222-3333", email: "john@smith.com" }
12 | const fillAccountIcon = (a: boolean) => undefined
13 |
14 | render(
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 | )
23 | await waitFor(() => {
24 | const firstName = screen.getByDisplayValue(/John/)
25 | const lastName = screen.getByDisplayValue(/Smith/)
26 | const email = screen.getByDisplayValue(/john@smith/)
27 | const phoneNumber = screen.getByDisplayValue(/111-222-3333/)
28 | const doneBtn = screen.getByText(/Done/)
29 |
30 | expect(firstName).toBeInTheDocument()
31 | expect(lastName).toBeInTheDocument()
32 | expect(email).toBeInTheDocument()
33 | expect(phoneNumber).toBeInTheDocument()
34 | expect(doneBtn).toBeInTheDocument()
35 | })
36 | })
37 |
--------------------------------------------------------------------------------
/src/components/icons/home.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | ic_fluent_home_24_regular
5 | Created with Sketch.
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/src/mocks/handlers.ts:
--------------------------------------------------------------------------------
1 | import { rest } from "msw"
2 |
3 | export const handlers = [
4 | rest.post("/login", (req, res, ctx) => {
5 | return res(
6 | ctx.json({
7 | authToken: "f79e82e8-c34a-4dc7-a49e-9fadc0979fda",
8 | user: {
9 | firstName: "Valerie",
10 | lastName: "Volunteer",
11 | },
12 | })
13 | )
14 | }),
15 |
16 | rest.get("/students", (req, res, ctx) => {
17 | return res(
18 | ctx.json({
19 | students: [
20 | {
21 | id: 1,
22 | firstName: "Joe",
23 | lastName: "Student",
24 | dateOfBirth: "03/22/07"
25 | },
26 | {
27 | id: 2,
28 | firstName: "Jane",
29 | lastName: "Student",
30 | dateOfBirth: "10/19/08"
31 | },
32 | ],
33 | })
34 | )
35 | }),
36 |
37 | rest.get("/sessions", (req, res, ctx) => {
38 | return res(
39 | ctx.json({
40 | sessions: [
41 | {
42 | id: 1,
43 | student: "Joe Student",
44 | date: "03/22/21",
45 | },
46 | {
47 | id: 2,
48 | student: "Jane Student",
49 | date: "10/22/21",
50 | },
51 | {
52 | id: 3,
53 | student: "Joe Student",
54 | date: "02/17/21",
55 | },
56 | {
57 | id: 4,
58 | student: "Jane Student",
59 | date: "1/19/21",
60 | },
61 | ],
62 | })
63 | )
64 | }),
65 | ]
66 |
--------------------------------------------------------------------------------
/src/components/WelcomeDashboard/SessionSection.tsx:
--------------------------------------------------------------------------------
1 | import React from "react"
2 | import { useQuery } from "@apollo/client"
3 |
4 | import { SurveyResponse } from "models/SurveyResponse"
5 | import { GetSurveyResponsesQuery } from "graphql/queries/GetSurveyResponses"
6 | import { GetSurveyResponses } from "graphql/queries/__generated__/GetSurveyResponses"
7 |
8 | import { QueryError } from "../partials/QueryError"
9 | import { SessionItem } from "../partials/SessionItem"
10 | import NoSessionSVG from "./noSessions.svg"
11 |
12 | export const SessionSection = (): JSX.Element => {
13 | const { data, loading, error } = useQuery(GetSurveyResponsesQuery)
14 | if (loading) { return Loading ....
}
15 | if (error) { return }
16 |
17 | return (
18 |
19 |
Recent Sessions
20 | {data?.surveyResponses?.length === 0 &&
21 |
22 |
23 | You haven't completed a session survey yet!
24 | Log a session now!
25 |
26 |
27 |
28 |
29 |
30 | }
31 |
32 | {data?.surveyResponses?.map((response: SurveyResponse) => (
33 |
37 | ))}
38 |
39 | )
40 | }
41 |
--------------------------------------------------------------------------------
/src/components/icons/person.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | ic_fluent_person_24_regular
5 | Created with Sketch.
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | *Before filing the ticket you should replace all text between the horizontal rules with your own words. We recommend keeping the headings (optional headings can be left off if you don't need them)*
2 |
3 | --------
4 |
5 | # Summary
6 |
7 | Describe an overview of the problem. Provide a general description of the problem it's solving or why this issue should be addressed.
8 |
9 | ## Things to Consider
10 | > **OPTIONAL** If you happen to know that this particular issue will touch other parts of the application in non-obvious ways (maybe there are side effects, or there are other parts of the application that depend on the thing that needs to be changed), you can list them out here. This is an opportunity to share *domain knowledge* that you might have about the problemset.
11 |
12 | # Criteria for Completion
13 | > This should be a bulleted list of items that, if satisfied, would result in an acceptably complete Pull Request. Be as specific as you need to be. For example:
14 |
15 | - [ ] When a user clicks on {this} it should now do {that}
16 | - [ ] This feature should only be available to volunteers
17 | - [ ] Add a test proving that it works
18 |
19 | # Bonus Round
20 | > **OPTIONAL** This can be additional work that is related to the work performed but might be more challenging or require a little extra effort to resolve. It's NOT required for the PR to be accepted and merged. Usually if this work isn't performed it would be created in a separate issue later. For example:
21 |
22 | - [ ] Present more data from the GraphQL endpoint on the backend
23 | - [ ] Refactor this widget away because it is no longer needed
24 |
25 | -------
26 |
27 | The goal here is to minimize the amount of clarification that a user needs in order to get started on the work, as well as reducing the likelihood that a lengthy PR discussion about the scope of work ensues. This whole project is volunteer-driven, on both sides, so we want to communicate expectations and needs as clearly as possible up front.
28 |
--------------------------------------------------------------------------------
/src/components/Account.tsx:
--------------------------------------------------------------------------------
1 | import { User } from "models/User"
2 | import { Link } from "react-router-dom"
3 |
4 | import { setToken } from "lib/authentication"
5 | import { Avatar } from "./partials/Avatar"
6 |
7 | interface AccountProps {
8 | fillAccountIcon: (a: boolean) => void;
9 | user: User;
10 | }
11 |
12 | export const Account = ({
13 | fillAccountIcon,
14 | user,
15 | }: AccountProps): JSX.Element => {
16 | fillAccountIcon(true)
17 |
18 | const clearSession = () => {
19 | setToken("")
20 | window.location.reload()
21 | }
22 |
23 | return (
24 | <>
25 |
26 |
27 |
32 | Logout
33 |
34 |
35 |
36 |
Profile
37 |
38 |
39 | Edit
40 |
41 |
42 |
43 |
44 |
45 |
46 | {user.firstName}
47 | {user.lastName}
48 | {user.email}
49 | {user.phoneNumber}
50 |
51 |
52 | Security
53 | Change Password
54 | >
55 | )
56 | }
57 |
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | purge: ["./src/**/*.{js,jsx,ts,tsx}", "./public/index.html"],
3 | darkMode: false,
4 | theme: {
5 | boxShadow: {
6 | md: "0 20px 20px -1px rgba(120, 144, 156, .5), 0 2px 20px -1px rgba(120, 144, 156, .5)",
7 | },
8 | extend: {
9 | colors: {
10 | 'purple': '#673AB7',
11 | 'purple-light': '#D1C4E9',
12 | 'gray-lightest': '#F8FBFE',
13 | 'gray-light': '#CFD8DC',
14 | 'gray-medium': '#B0BEC5',
15 | 'gray-dark': '#37474F',
16 | 'login-gradient-top': '#512da8',
17 | 'login-gradient-middle': '#9575cd',
18 | 'login-gradient-bottom': '#1ed4eb',
19 | primary: {
20 | DEFAULT: '#673ab7',
21 | '50': '#ede7f6',
22 | '100': '#d1c4e9',
23 | '200': '#b39ddb',
24 | '300': '#9575cd',
25 | '400': '#7e57c2',
26 | '500': '#673ab7',
27 | '600': '#5e35b1',
28 | '700': '#512da8',
29 | '800': '#4527a0',
30 | '900': '#311b92',
31 | },
32 | secondary: {
33 | DEFAULT: '#00bcd4',
34 | '50': '#e0f7fa',
35 | '100': '#b2ebf2',
36 | '200': '#80deea',
37 | '300': '#4dd0e1',
38 | '400': '#26c6da',
39 | '500': '#00bcd4',
40 | '600': '#00acc1',
41 | '700': '#0097a7',
42 | '800': '#00838f',
43 | '900': '#006064',
44 | },
45 | neutral: {
46 | DEFAULT: '#607d8b',
47 | '50': '#eceff1',
48 | '100': '#cfd8dc',
49 | '200': '#b0bec5',
50 | '300': '#9a44ae',
51 | '400': '#78909c',
52 | '500': '#607d8b',
53 | '600': '#546e7a',
54 | '700': '#455a64',
55 | '800': '#37474f',
56 | '900': '#263238',
57 | },
58 | },
59 | },
60 | fontFamily: {
61 | 'nunito': ['nunito', 'sans-serif'],
62 | 'tenor': ['Tenor Sans', 'sans-serif'],
63 | },
64 | },
65 | variants: {
66 | extend: {},
67 | },
68 | plugins: [],
69 | }
70 |
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
17 |
18 |
19 |
20 |
29 | InKind
30 |
31 |
32 | You need to enable JavaScript to run this app.
33 |
34 |
44 |
45 |
46 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "inkind-volunteer",
3 | "version": "0.1.0",
4 | "private": true,
5 | "engines": {
6 | "node": "^16.13",
7 | "yarn": "^1.12.15"
8 | },
9 | "dependencies": {
10 | "@apollo/client": "^3.4.17",
11 | "@craco/craco": "^6.1.2",
12 | "@testing-library/jest-dom": "^5.15",
13 | "@testing-library/react": "^12.1",
14 | "@testing-library/user-event": "^13.5",
15 | "@types/jest": "^27.0",
16 | "@types/node": "^16.11",
17 | "@types/react": "^17.0.34",
18 | "@types/react-datepicker": "^4.3",
19 | "@types/react-dom": "^17.0.11",
20 | "apollo": "^2.33.9",
21 | "autoprefixer": "^9",
22 | "classnames": "^2.3.1",
23 | "graphql": "15.7.2",
24 | "msw": "^0.35.0",
25 | "node-sass": "6.0.1",
26 | "postcss": "^7",
27 | "react": "^17.0.2",
28 | "react-datepicker": "^4.3.0",
29 | "react-dom": "^17.0.2",
30 | "react-hook-form": "^7.15.4",
31 | "react-query": "^3.16.0",
32 | "react-router": "^6.0.2",
33 | "react-router-dom": "^6.0.2",
34 | "react-scripts": "4.0.3",
35 | "tailwindcss": "npm:@tailwindcss/postcss7-compat",
36 | "typescript": "4.4.4",
37 | "web-vitals": "^2.1.2"
38 | },
39 | "scripts": {
40 | "start": "craco start",
41 | "build": "craco build",
42 | "test": "craco test",
43 | "eject": "react-scripts eject",
44 | "eslint": "eslint --ignore-path .gitignore --ext .js,.ts,.tsx .",
45 | "check-types": "tsc",
46 | "lint": "yarn run eslint && yarn run check-types",
47 | "prettier": "prettier --ignore-path .gitignore \"**/*.+(js|json|ts|tsx)\"",
48 | "format": "yarn run prettier -- --write",
49 | "check-format": "yarn run prettier -- --list-different",
50 | "validate": "yarn-run-all --parallel check-types check-format lint build",
51 | "schema": "apollo service:download --endpoint=https://inkind-admining.herokuapp.com/graphql external/graphql/schema.json; apollo codegen:generate --localSchemaFile=./external/graphql/schema.json --target=typescript"
52 | },
53 | "eslintConfig": {
54 | "extends": [
55 | "react-app",
56 | "react-app/jest"
57 | ]
58 | },
59 | "browserslist": {
60 | "production": [
61 | ">0.2%",
62 | "not dead",
63 | "not op_mini all"
64 | ],
65 | "development": [
66 | "last 1 chrome version",
67 | "last 1 firefox version",
68 | "last 1 safari version"
69 | ]
70 | },
71 | "devDependencies": {
72 | "@types/react-router": "^5.1.17",
73 | "@types/react-router-dom": "^5.3.2",
74 | "eslint-config-prettier": "^8.3.0",
75 | "eslint-plugin-sonarjs": "^0.10.0"
76 | },
77 | "msw": {
78 | "workerDirectory": "public"
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/src/components/EditAccount.tsx:
--------------------------------------------------------------------------------
1 | import { useForm } from "react-hook-form"
2 | import { Link, useNavigate } from "react-router-dom"
3 |
4 | import { useMutation } from "@apollo/client"
5 | import { updateUserMutation } from "graphql/mutations/UpdateUserMutation"
6 | import { User } from "models/User"
7 | import { Avatar } from "./partials/Avatar"
8 |
9 | interface AccountProps {
10 | fillAccountIcon: (a: boolean) => void;
11 | user: User;
12 | }
13 |
14 | interface VolunteerInput {
15 | firstName: string;
16 | lastName: string;
17 | email: string;
18 | phoneNumber: string;
19 | }
20 |
21 | export const EditAccount = ({
22 | fillAccountIcon,
23 | user,
24 | }: AccountProps): JSX.Element => {
25 | fillAccountIcon(false)
26 | const { register, handleSubmit, formState: { errors } } = useForm()
27 | const [updateUser, { loading }] = useMutation(updateUserMutation)
28 | const navigate = useNavigate()
29 | const onSubmit = (data: VolunteerInput) => {
30 | updateUser({ variables: { data }}).then(() => {
31 | navigate("/account/edit/success?text=Your profile was successfully updated.")
32 | }).catch((err) => {
33 | console.log(err)
34 | })
35 | }
36 |
37 | return (
38 |
70 | )
71 | }
72 |
--------------------------------------------------------------------------------
/src/components/Login.tsx:
--------------------------------------------------------------------------------
1 | import { useState } from "react"
2 | import { useMutation } from "@apollo/client"
3 | import { setToken } from "lib/authentication"
4 |
5 | import { SignInMutation } from "graphql/mutations/SignInMutation"
6 |
7 | export const Login = (): JSX.Element => {
8 | const [userEmail, setUserEmail] = useState("")
9 | const [password, setPassword] = useState("")
10 |
11 | const [login, { client, error }] = useMutation(SignInMutation, {
12 | variables: {
13 | credentials: {
14 | email: userEmail,
15 | password: password,
16 | },
17 | },
18 | onCompleted: ({ signInUser }) => {
19 | setToken(signInUser.token)
20 | client.clearStore()
21 | window.location.reload()
22 | },
23 | onError: (error) => {
24 | console.error("[failed login]", error)
25 | },
26 | })
27 |
28 | return (
29 |
30 |
31 |
32 | InKind
33 |
34 |
35 | {error &&
{error.message}
}
36 |
37 |
72 |
73 |
74 | )
75 | }
76 |
--------------------------------------------------------------------------------
/src/components/EditAccountPassword.tsx:
--------------------------------------------------------------------------------
1 | import { useForm } from "react-hook-form"
2 | import { Link, useNavigate } from "react-router-dom"
3 |
4 | import { useMutation } from "@apollo/client"
5 | import { updateUserPasswordMutation } from "graphql/mutations/UpdateUserMutation"
6 | import { User } from "models/User"
7 | import { Avatar } from "./partials/Avatar"
8 |
9 | interface AccountProps {
10 | fillAccountIcon: (a: boolean) => void;
11 | user: User;
12 | }
13 |
14 | interface GraphqlInput {
15 | oldPassword: string;
16 | password: string;
17 | }
18 |
19 | interface FormFields {
20 | oldPassword: string;
21 | password: string;
22 | passwordConfirmation: string;
23 | }
24 |
25 | const getGraphqlInput = (data: FormFields): GraphqlInput => {
26 | return {
27 | oldPassword: data.oldPassword,
28 | password: data.password,
29 | }
30 | }
31 |
32 | export const EditAccountPassword = ({
33 | fillAccountIcon,
34 | user,
35 | }: AccountProps): JSX.Element => {
36 | fillAccountIcon(false)
37 | const { register, handleSubmit, formState: { errors }, watch } = useForm()
38 | const navigate = useNavigate()
39 | const [updateUser, { loading, error }] = useMutation(updateUserPasswordMutation, {
40 | onCompleted: () => {
41 | navigate("/account/edit/success?text=Your password was successfully updated.")
42 | }
43 | })
44 | const newPassword = watch("password")
45 | const onSubmit = (data: FormFields) => {
46 | updateUser({ variables: getGraphqlInput(data) })
47 | .catch((err) => console.log(err))
48 | }
49 | const equalsNewPassord = (newPasswordConfirmation: string): boolean | string => {
50 | return newPassword === newPasswordConfirmation ? true : "Password confirmation does not match"
51 | }
52 |
53 | return (
54 |
86 | )
87 | }
88 |
--------------------------------------------------------------------------------
/src/components/StudentShow.tsx:
--------------------------------------------------------------------------------
1 | import { useQuery, useMutation } from "@apollo/client"
2 | import { useParams, useNavigate } from "react-router-dom"
3 |
4 | import { GetStudentQuery } from "graphql/queries/GetStudent"
5 | import { GetStudentSurveyResponsesQuery } from "graphql/queries/GetStudentSurveyResponses"
6 | import { GetStudent } from "graphql/queries/__generated__/GetStudent"
7 | import { GetStudentSurveyResponses } from "graphql/queries/__generated__/GetStudentSurveyResponses"
8 | import { StudentSurveyResponse } from "models/StudentSurveyResponse"
9 | import { getAge } from "utils/getAge"
10 |
11 | import { SessionItem } from "./partials/SessionItem"
12 | import { CreateSurveyResponse } from "graphql/mutations/CreateSurveyResponse"
13 |
14 | interface StudentShowProps {
15 | fillHomeIcon: (a: boolean) => void;
16 | }
17 |
18 | export const StudentShow = ({ fillHomeIcon }: StudentShowProps): JSX.Element | null => {
19 | fillHomeIcon(false)
20 | const { id } = useParams<'id'>()
21 | const navigate = useNavigate()
22 | const { data, loading } = useQuery(GetStudentQuery, { variables: { id }})
23 | const { data: surveyData, loading: surveyLoading } = useQuery(GetStudentSurveyResponsesQuery, { variables: { id }})
24 | const [ createSurveyResponse ] = useMutation(CreateSurveyResponse)
25 |
26 | const onBeginSurvey = () => {
27 |
28 | createSurveyResponse({
29 | variables: {
30 | surveyId: 1,
31 | userId: 1,
32 | studentId: id,
33 | }
34 | }).then(({ data: { createSurveyResponse }}) => {
35 | navigate(`/student/${id}/survey/${createSurveyResponse.response.id}`)
36 | })
37 | }
38 |
39 | if (loading || surveyLoading || !data || !surveyData) return null
40 |
41 | const {
42 | name,
43 | guardianName,
44 | guardianPhoneNumber,
45 | emergencyContactName,
46 | emergencyContactPhoneNumber,
47 | dateOfBirth
48 | } = data.student
49 |
50 | return (
51 |
52 | {name}
53 | Student Information
54 |
55 |
56 |
57 | Guardian Name
58 | {guardianName}
59 |
60 |
61 |
62 | Guardian Phone
63 | {guardianPhoneNumber}
64 |
65 |
66 |
67 | Emergency Contact
68 | {emergencyContactName}
69 |
70 |
71 |
72 | Emergency Phone
73 | {emergencyContactPhoneNumber}
74 |
75 |
76 |
77 | Student's Age
78 | {getAge(dateOfBirth)}
79 |
80 |
81 |
82 |
83 |
84 |
88 | BEGIN SESSION SURVEY
89 |
90 |
91 | Session History
92 | {surveyData?.studentSurveyResponses?.map((response: StudentSurveyResponse) => (
93 |
97 | ))}
98 |
99 | )
100 | }
101 |
102 |
--------------------------------------------------------------------------------
/src/components/SessionDuration/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react"
2 | import DatePicker from "react-datepicker"
3 | import classnames from "classnames"
4 | import { Controller, useForm } from "react-hook-form"
5 |
6 | import "./style.scss"
7 | import "react-datepicker/dist/react-datepicker.css"
8 |
9 | import TimerIcon from "../icons/timer.svg"
10 |
11 | interface DurationInput {
12 | date: Date | null;
13 | duration: string;
14 | }
15 |
16 | export const SessionDuration = (): JSX.Element => {
17 | const [showDurationInput, setShowDurationInput] = useState(false)
18 | const { control, register, handleSubmit, formState: { isValid } } = useForm({ mode: "onChange" })
19 |
20 | const toggleShowDuration = () => setShowDurationInput(!showDurationInput)
21 | const onSubmit = (data: DurationInput) => console.log(data)
22 |
23 | const durationOptions = ["30", "45", "60", "75", "90"]
24 |
25 | return (
26 |
27 |
28 |
Enter Date and Time
29 |
Enter your session date and duration. Round up to the nearest 15 mins, if necessary.
30 |
96 |
97 |
98 | )
99 | }
100 |
--------------------------------------------------------------------------------
/src/components/MainNav.tsx:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from "react"
2 | import {
3 | BrowserRouter as Router,
4 | Link,
5 | Route,
6 | Routes,
7 | } from "react-router-dom"
8 |
9 | import HomeIcon from "./icons/home.svg"
10 | import HomeSelectedIcon from "./icons/homeSelected.svg"
11 | import PersonIcon from "./icons/person.svg"
12 | import PersonSelectedIcon from "./icons/personSelected.svg"
13 |
14 | import { User } from "models/User"
15 |
16 | import { WelcomeDashboard } from "./WelcomeDashboard/index"
17 | import { StudentShow } from "./StudentShow"
18 | import { SurveyShow } from "./SurveyShow/index"
19 | import { Account } from "./Account"
20 | import { EditAccount } from "./EditAccount"
21 | import { EditAccountPassword } from "./EditAccountPassword"
22 | import { EditAccountSuccess } from "./EditAccountSuccess"
23 | import { SessionDuration } from "./SessionDuration/index"
24 |
25 | interface MainNavProps {
26 | user: User;
27 | }
28 |
29 | export const MainNav = ({ user }: MainNavProps): JSX.Element => {
30 | const [filledHomeIcon, fillHomeIcon] = useState(false)
31 | const [filledAccountIcon, fillAccountIcon] = useState(false)
32 |
33 | useEffect(() => {
34 | if (!(window.location.pathname == "/")) {
35 | fillHomeIcon(false)
36 | }
37 |
38 | if (!(window.location.pathname.startsWith("/account"))) {
39 | fillAccountIcon(false)
40 | }
41 | })
42 |
43 | return (
44 |
45 |
46 |
47 |
48 |
49 | fillHomeIcon(true)}
51 | to="/"
52 | >
53 |
54 | { filledHomeIcon ? (
55 |
56 | ) : (
57 |
58 | )
59 | }
60 |
Home
61 |
62 |
63 |
64 |
65 | fillAccountIcon(true)}
67 | to="/account"
68 | >
69 |
70 | { filledAccountIcon ? (
71 |
72 | ) : (
73 |
74 | )
75 | }
76 |
Account
77 |
78 |
79 |
80 |
81 |
82 |
83 | }
85 | path="/student/:studentId/survey/:surveyResponseId/duration"
86 | />
87 | }
89 | path="/student/:studentId/survey/:surveyResponseId"
90 | />
91 | }
93 | path="/student/:id"
94 | />
95 | }
97 | path="/"
98 | />
99 | }
101 | path="/account"
102 | />
103 | }
105 | path="/account/edit"
106 | />
107 | }
109 | path="/account/edit-password"
110 | />
111 | }
113 | path="/account/edit/success"
114 | />
115 |
116 |
117 |
118 | )
119 | }
120 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://github.com/rubyforgood/inkind-volunteer/actions/workflows/rspec.yml)
2 | [](https://github.com/rubyforgood/inkind-volunteer/actions/workflows/lint.yml)
3 |
4 | # InKind - Supporting Community Education Partnerships
5 |
6 | This repository is the frontend code for an application named InKind, which is a [Ruby For Good](https://rubyforgood.org/) project serving [Community Education Partnerships](https://www.cep.ngo/) (CEP).
7 |
8 | Community Education Partnerships' mission is to increase the learning opportunities and enhance the academic achievement of students experiencing homelessness and housing insecurity. Community Education Partnerships offers its students: individualized tutoring and mentoring, backpacks, school supplies, books, learning resources, enrollment assistance, and opportunities to participate in extracurricular enrichment activities.
9 |
10 | This app allows CEP volunteers to provide feedback from the mentoring sessions that are a core part of CEP's programming. These sessions provide vital stability to young people who lack access to learning opportunities and face a multitude of challenges that affect their educational outcomes. CEP volunteers form lasting relationships with students, and communicate their needs of food insecurity and other necessities to CEP, ensuring that staff members are able to facilitate the appropriate support for students, and by extension, their families. This app will enable CEP to collect and analyze critical data, facilitate quicker response times to requests for support, and enable them to apply for grant funding, ensuring that their work remains sustainable.
11 |
12 | The counterpart to this codebase is [`inkind-admin`](https://github.com/rubyforgood/inkind-admin), which is a Ruby on Rails GraphlQL API (and desktop first admin portal).
13 |
14 | # Welcome Contributors!
15 |
16 | Thanks for checking us out!
17 | - Check the `CONTRIBUTING.md` file for a guide on how to get started
18 | - This is a 100% volunteer-supported project, please be patient with your correspondence. Most (all?) of us have day jobs and so responses to questions / pending PRs may not be immediate. Please be patient, we'll get to you! :)
19 |
20 | Please feel encouraged to join us on Slack! You can sign up at https://rubyforgood.herokuapp.com
21 |
22 | We're in the `#team-inkind` channel.
23 |
24 | # Development
25 |
26 | This is a TypeScript/React project that uses GraphQL as it's single endpoint. GraphQL schema is checked in to both `inkind` codebases.
27 |
28 | This project was initially setup with [Create React App](https://github.com/facebook/create-react-app). You can learn more this in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). To learn more about React, check out the [React documentation](https://reactjs.org/).
29 |
30 | If you are unfamiliar with GraphQL, there is no better time to learn. [HowToGraphQL](https://www.howtographql.com/) is a wonderful interactive tutorial to get acquainted. This project uses [Apollo](https://www.apollographql.com/) to communicate between the backend and fronted.
31 |
32 | ## Installation
33 |
34 | ### Node
35 |
36 | 1. Install Node.js. We recommend [asdf](https://asdf-vm.com/guide/getting-started.html#_1-install-dependencies) because you can also use this version manager to install Ruby, which is necessary to pull data into this client-side application. [nvm](https://github.com/nvm-sh/nvm) works too!
37 |
38 | ### Yarn
39 |
40 | 1. Install yarn. With [asdf](https://github.com/twuni/asdf-yarn) this can be accomplished with `asdf plugin-add yarn` & `asdf install yarn latest`
41 | 1. Run `yarn install`
42 |
43 | ### Install/Setup InKind Backend
44 |
45 | 1. Proceed to the [installation instructions on `inkind-admin`](https://github.com/rubyforgood/inkind-admin#installation) and work your way down to the server instructions.
46 | 1. After you've completed backend installations, start a rails server with `bin/server` and leave it running.
47 |
48 | ### Start project
49 |
50 | 1. Run `yarn start` in a new shell session. This will open [http://localhost:3000](http://localhost:3000) to view the project in your browser. The page will reload as you make edits to TypeScript. You will also see type & lint offenses in the console.
51 |
52 | ### Tests
53 |
54 | 1. Run `yarn test` This will launch the test runner in an interactive watch mode.
55 |
56 | ### Login
57 |
58 | To log in as a volunteer:
59 |
60 | Email: volunteer@cep.dev
61 | Password: password
62 |
63 | # Working with GraphQL
64 |
65 | When you make a change that grabs additional fields from a GraphQL query, OR makes a new request to a mutation, you need to regenerate the GraphQL schema in this repository.
66 |
67 | ## Available Scripts
68 |
69 | ### `yarn schema`
70 |
71 | 1. Make sure the [inkind-admin project](https://github.com/rubyforgood/inkind-admin) is running on your local development machine (`bin/server`).
72 | 2. Run `yarn schema` to regenerate all graphql Typescript files.
73 |
--------------------------------------------------------------------------------
/src/__test__/components/Landing.test.tsx:
--------------------------------------------------------------------------------
1 | // @ts-nocheck
2 | import { render, screen, waitFor } from "@testing-library/react"
3 | import userEvent from "@testing-library/user-event"
4 | import { QueryClient, QueryClientProvider } from "react-query"
5 | import { MockedProvider } from '@apollo/client/testing'
6 | import { Landing } from "components/Landing"
7 | import { GetCurrentUser } from "graphql/queries/GetCurrentUser"
8 | import { SignInMutation } from "graphql/mutations/SignInMutation"
9 | import { GetStudentsQuery } from "graphql/queries/GetStudents"
10 | import { GetSurveyResponsesQuery } from "graphql/queries/GetSurveyResponses"
11 |
12 | const mocksAuthenticated = [
13 | {
14 | request: {
15 | query: GetCurrentUser,
16 | },
17 | result: {
18 | data: {
19 | currentUser: {
20 | id: 1,
21 | name: "Violet Volunteer",
22 | firstName: "Violet",
23 | lastName: "Volunteer",
24 | phoneNumber: "111-222-3333",
25 | email: "volunteer@cep.dev",
26 | initials: "VV",
27 | role: "volunteer",
28 | },
29 | }
30 | },
31 | }, {
32 | request: {
33 | query: GetStudentsQuery,
34 | },
35 | result: {
36 | data: {
37 | students: [
38 | {
39 | id: 1,
40 | dateOfBirth: "03/22/07",
41 | email: "joe@cep.ngo",
42 | name: "Joe Student",
43 | initials: "JS",
44 | createdAt: "2021-10-12 15:00:00",
45 | updatedAt: "2021-10-12 15:00:00"
46 | },
47 | {
48 | id: 2,
49 | dateOfBirth: "10/19/08",
50 | email: "jane@cep.ngo",
51 | name: "Jane Student",
52 | initials: "JS",
53 | createdAt: "2021-10-12 15:00:00",
54 | updatedAt: "2021-10-12 15:00:00"
55 | },
56 | ],
57 | }
58 | }
59 | }, {
60 | request: {
61 | query: GetSurveyResponsesQuery
62 | },
63 | result: {
64 | data: {
65 | surveyResponses: [
66 | {
67 | id: 1,
68 | student: {
69 | name: "Joe Session",
70 | initials: "JS",
71 | },
72 | volunteer: {
73 | id: 1,
74 | name: "Volunteer",
75 | initials: "VV",
76 | },
77 | meetingDuration: {
78 | minutes: 29,
79 | startedAt: "2021-10-13- 08:31:47",
80 | }
81 | },
82 | {
83 | id: 2,
84 | student: {
85 | name: "Jane Session",
86 | initials: "JS",
87 | },
88 | volunteer: {
89 | id: 1,
90 | name: "Volunteer",
91 | initials: "VV",
92 | },
93 | meetingDuration: {
94 | minutes: 27,
95 | startedAt: "2021-10-13- 09:50:23",
96 | }
97 | },
98 | ]
99 | }
100 | }
101 | }
102 | ]
103 |
104 | const mocksAnonymous = [
105 | {
106 | request: {
107 | query: GetCurrentUser,
108 | },
109 | result: {
110 | data: {
111 | currentUser: null
112 | }
113 | },
114 | }, {
115 | request: {
116 | query: SignInMutation,
117 | variables: {
118 | credentials: {
119 | email: "volunteer@cep.dev",
120 | password: "password",
121 | }
122 | },
123 | },
124 | result: {
125 | data: {
126 | signInUser: {
127 | user: {
128 | id: 1,
129 | name: "Volunteer",
130 | email: "volunteer@cep.dev",
131 | },
132 | token: "f79e82e8-c34a-4dc7-a49e-9fadc0979fda",
133 | }
134 | }
135 | }
136 | },
137 | ]
138 |
139 | test("user logs in", async () => {
140 | const location: Location = window.location
141 | delete window.location
142 |
143 | window.location = {
144 | ...location,
145 | reload: jest.fn()
146 | }
147 |
148 | render(
149 |
150 |
151 |
152 |
153 |
154 | )
155 |
156 | // Loading State
157 | await waitFor(() => {
158 | expect(screen.getByText(/loading.../)).toBeInTheDocument()
159 | })
160 |
161 | // Sign-In State
162 | await waitFor(() => {
163 | const signInButton = screen.getByText(/Sign In/)
164 |
165 | expect(signInButton).toBeInTheDocument()
166 |
167 | userEvent.type(screen.getByPlaceholderText(/email address/i), "volunteer@cep.dev")
168 | userEvent.type(screen.getByPlaceholderText(/password/i), "password")
169 | userEvent.click(signInButton)
170 | })
171 |
172 | jest.restoreAllMocks()
173 | window.location = location
174 | })
175 |
176 | test("volunteer is logged in", async () => {
177 | render(
178 |
179 |
180 |
181 |
182 |
183 | )
184 |
185 | // Welcome Dashboard State
186 | await waitFor(() => {
187 | expect(screen.getByText(/My Students/)).toBeInTheDocument
188 | expect(screen.getByText(/Joe Student/)).toBeInTheDocument
189 | expect(screen.getByText(/Jane Student/)).toBeInTheDocument
190 | expect(screen.getByText(/Recent Sessions/)).toBeInTheDocument
191 | expect(screen.getByText(/Joe Session/)).toBeInTheDocument
192 | expect(screen.getByText(/Jane Session/)).toBeInTheDocument
193 | })
194 | })
195 |
--------------------------------------------------------------------------------
/src/components/SurveyShow/index.tsx:
--------------------------------------------------------------------------------
1 | import { useState } from "react"
2 | import { useQuery, useMutation } from "@apollo/client"
3 | import { useParams, useNavigate } from "react-router-dom"
4 |
5 | import { UpsertSurveyQuestionResponseMutation } from "graphql/mutations/UpsertSurveyQuestionResponse"
6 | import { CreateSupportTicketMutation } from "graphql/mutations/CreateSupportTicket"
7 | import { GetSurveyResponseQuery } from "graphql/queries/GetSurveyResponse"
8 | import { GetSurveyResponse, GetSurveyResponse_surveyResponse_survey_questions } from "graphql/queries/__generated__/GetSurveyResponse"
9 |
10 | import { SurveyProgressBar } from "./SurveyProgressBar"
11 | import { SingleSelectionOption } from "./SingleSelectionOption"
12 | import { MultipleSelectionOption } from "./MultipleSelectionOption"
13 | import { TextQuestion } from "./TextQuestion"
14 |
15 | export const SurveyShow = (): JSX.Element | null => {
16 | const navigate = useNavigate()
17 | const [ answer, setAnswer ] = useState<[string] | string>()
18 | const [ currentQuestionIndex, setCurrentQuestionIndex ] = useState(0)
19 | const [ supportTicketQuestion, showSupportTicketQuestion ] = useState(false)
20 | const { surveyResponseId, studentId } = useParams<'surveyResponseId' | 'studentId'>()
21 | const { data, loading } = useQuery(GetSurveyResponseQuery, { variables: { id: surveyResponseId }})
22 | const [ upsertSurveyQuestionResponse ] = useMutation(UpsertSurveyQuestionResponseMutation)
23 | const [ createSupportTicket ] = useMutation(CreateSupportTicketMutation)
24 |
25 | if (loading || !data) return null
26 |
27 | const { surveyResponse } = data
28 | const { questions } = surveyResponse.survey
29 |
30 | if (!questions) return null
31 |
32 | const currentQuestion = questions[currentQuestionIndex]
33 |
34 | const goToNextQuestion = (queueSupportTicket:boolean) => {
35 | if (queueSupportTicket) {
36 | showSupportTicketQuestion(true)
37 | } else if (currentQuestionIndex + 1 == questions.length) {
38 | navigate(`/student/${studentId}/survey/${surveyResponseId}/duration`)
39 | } else {
40 | setCurrentQuestionIndex(currentQuestionIndex + 1)
41 | setAnswer(undefined)
42 | }
43 | }
44 |
45 | const goToPreviousQuestion = () => {
46 | showSupportTicketQuestion(false)
47 | if(currentQuestionIndex == 0) {
48 | navigate(`/student/${studentId}`)
49 | } else {
50 | setCurrentQuestionIndex(currentQuestionIndex - 1)
51 | setAnswer(undefined)
52 | }
53 | }
54 |
55 | const onNext = () => {
56 | let reply, options
57 | let queueSupportTicket = false
58 |
59 | if (currentQuestion.type == "SurveyTextQuestion") {
60 | reply = answer
61 | options = []
62 | } else {
63 | reply = null
64 | options = answer
65 | }
66 |
67 | if (currentQuestion.type == "SurveySupportTicketQuestion") {
68 | queueSupportTicket = currentQuestion?.options?.find(option => option.id === answer)?.label === "Yes"
69 | }
70 |
71 | upsertSurveyQuestionResponse({
72 | variables: {
73 | surveyResponseId: surveyResponseId,
74 | questionId: currentQuestion.id,
75 | optionIds: options,
76 | reply: reply
77 | }
78 | })
79 |
80 | goToNextQuestion(queueSupportTicket)
81 | }
82 |
83 | const createTicket = () => {
84 | createSupportTicket({
85 | variables: {
86 | surveyResponseId: surveyResponseId,
87 | description: answer
88 | }
89 | })
90 |
91 | goToNextQuestion(false)
92 | }
93 |
94 | const onSupportTicketResponse = (event: React.ChangeEvent) => {
95 | setAnswer(event.target.value)
96 | }
97 |
98 | const renderQuestion = (question: GetSurveyResponse_surveyResponse_survey_questions) => {
99 | if (question.options && question.options.length > 0) {
100 | if (question.type == "SurveySingleSelectQuestion" || question.type == "SurveySupportTicketQuestion") {
101 | return
102 | }
103 | if (question.type == "SurveyMultiSelectQuestion") {
104 | return
105 | }
106 | } else {
107 | return
108 | }
109 | }
110 |
111 | return (
112 |
113 |
118 |
119 | { supportTicketQuestion ?
120 | <>
121 | {"Please describe the reason for this request in a few words:"}
122 |
123 |
129 |
130 |
131 | NEXT
132 |
133 | >
134 | :
135 | <>
136 | {currentQuestion.heading}
137 | {currentQuestion.prompt}
138 | {currentQuestion.description}
139 |
140 | {renderQuestion(currentQuestion)}
141 |
142 |
143 | goToNextQuestion(false)}>SKIP
144 | NEXT
145 |
146 | >
147 | }
148 |
149 | )
150 | }
151 |
152 |
--------------------------------------------------------------------------------
/src/components/icons/celebrating.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/mockServiceWorker.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | /* tslint:disable */
3 |
4 | /**
5 | * Mock Service Worker (0.35.0).
6 | * @see https://github.com/mswjs/msw
7 | * - Please do NOT modify this file.
8 | * - Please do NOT serve this file on production.
9 | */
10 |
11 | const INTEGRITY_CHECKSUM = 'f0a916b13c8acc2b526a03a6d26df85f'
12 | const bypassHeaderName = 'x-msw-bypass'
13 | const activeClientIds = new Set()
14 |
15 | self.addEventListener('install', function () {
16 | return self.skipWaiting()
17 | })
18 |
19 | self.addEventListener('activate', async function (event) {
20 | return self.clients.claim()
21 | })
22 |
23 | self.addEventListener('message', async function (event) {
24 | const clientId = event.source.id
25 |
26 | if (!clientId || !self.clients) {
27 | return
28 | }
29 |
30 | const client = await self.clients.get(clientId)
31 |
32 | if (!client) {
33 | return
34 | }
35 |
36 | const allClients = await self.clients.matchAll()
37 |
38 | switch (event.data) {
39 | case 'KEEPALIVE_REQUEST': {
40 | sendToClient(client, {
41 | type: 'KEEPALIVE_RESPONSE',
42 | })
43 | break
44 | }
45 |
46 | case 'INTEGRITY_CHECK_REQUEST': {
47 | sendToClient(client, {
48 | type: 'INTEGRITY_CHECK_RESPONSE',
49 | payload: INTEGRITY_CHECKSUM,
50 | })
51 | break
52 | }
53 |
54 | case 'MOCK_ACTIVATE': {
55 | activeClientIds.add(clientId)
56 |
57 | sendToClient(client, {
58 | type: 'MOCKING_ENABLED',
59 | payload: true,
60 | })
61 | break
62 | }
63 |
64 | case 'MOCK_DEACTIVATE': {
65 | activeClientIds.delete(clientId)
66 | break
67 | }
68 |
69 | case 'CLIENT_CLOSED': {
70 | activeClientIds.delete(clientId)
71 |
72 | const remainingClients = allClients.filter((client) => {
73 | return client.id !== clientId
74 | })
75 |
76 | // Unregister itself when there are no more clients
77 | if (remainingClients.length === 0) {
78 | self.registration.unregister()
79 | }
80 |
81 | break
82 | }
83 | }
84 | })
85 |
86 | // Resolve the "master" client for the given event.
87 | // Client that issues a request doesn't necessarily equal the client
88 | // that registered the worker. It's with the latter the worker should
89 | // communicate with during the response resolving phase.
90 | async function resolveMasterClient(event) {
91 | const client = await self.clients.get(event.clientId)
92 |
93 | if (client.frameType === 'top-level') {
94 | return client
95 | }
96 |
97 | const allClients = await self.clients.matchAll()
98 |
99 | return allClients
100 | .filter((client) => {
101 | // Get only those clients that are currently visible.
102 | return client.visibilityState === 'visible'
103 | })
104 | .find((client) => {
105 | // Find the client ID that's recorded in the
106 | // set of clients that have registered the worker.
107 | return activeClientIds.has(client.id)
108 | })
109 | }
110 |
111 | async function handleRequest(event, requestId) {
112 | const client = await resolveMasterClient(event)
113 | const response = await getResponse(event, client, requestId)
114 |
115 | // Send back the response clone for the "response:*" life-cycle events.
116 | // Ensure MSW is active and ready to handle the message, otherwise
117 | // this message will pend indefinitely.
118 | if (client && activeClientIds.has(client.id)) {
119 | ;(async function () {
120 | const clonedResponse = response.clone()
121 | sendToClient(client, {
122 | type: 'RESPONSE',
123 | payload: {
124 | requestId,
125 | type: clonedResponse.type,
126 | ok: clonedResponse.ok,
127 | status: clonedResponse.status,
128 | statusText: clonedResponse.statusText,
129 | body:
130 | clonedResponse.body === null ? null : await clonedResponse.text(),
131 | headers: serializeHeaders(clonedResponse.headers),
132 | redirected: clonedResponse.redirected,
133 | },
134 | })
135 | })()
136 | }
137 |
138 | return response
139 | }
140 |
141 | async function getResponse(event, client, requestId) {
142 | const { request } = event
143 | const requestClone = request.clone()
144 | const getOriginalResponse = () => fetch(requestClone)
145 |
146 | // Bypass mocking when the request client is not active.
147 | if (!client) {
148 | return getOriginalResponse()
149 | }
150 |
151 | // Bypass initial page load requests (i.e. static assets).
152 | // The absence of the immediate/parent client in the map of the active clients
153 | // means that MSW hasn't dispatched the "MOCK_ACTIVATE" event yet
154 | // and is not ready to handle requests.
155 | if (!activeClientIds.has(client.id)) {
156 | return await getOriginalResponse()
157 | }
158 |
159 | // Bypass requests with the explicit bypass header
160 | if (requestClone.headers.get(bypassHeaderName) === 'true') {
161 | const cleanRequestHeaders = serializeHeaders(requestClone.headers)
162 |
163 | // Remove the bypass header to comply with the CORS preflight check.
164 | delete cleanRequestHeaders[bypassHeaderName]
165 |
166 | const originalRequest = new Request(requestClone, {
167 | headers: new Headers(cleanRequestHeaders),
168 | })
169 |
170 | return fetch(originalRequest)
171 | }
172 |
173 | // Send the request to the client-side MSW.
174 | const reqHeaders = serializeHeaders(request.headers)
175 | const body = await request.text()
176 |
177 | const clientMessage = await sendToClient(client, {
178 | type: 'REQUEST',
179 | payload: {
180 | id: requestId,
181 | url: request.url,
182 | method: request.method,
183 | headers: reqHeaders,
184 | cache: request.cache,
185 | mode: request.mode,
186 | credentials: request.credentials,
187 | destination: request.destination,
188 | integrity: request.integrity,
189 | redirect: request.redirect,
190 | referrer: request.referrer,
191 | referrerPolicy: request.referrerPolicy,
192 | body,
193 | bodyUsed: request.bodyUsed,
194 | keepalive: request.keepalive,
195 | },
196 | })
197 |
198 | switch (clientMessage.type) {
199 | case 'MOCK_SUCCESS': {
200 | return delayPromise(
201 | () => respondWithMock(clientMessage),
202 | clientMessage.payload.delay,
203 | )
204 | }
205 |
206 | case 'MOCK_NOT_FOUND': {
207 | return getOriginalResponse()
208 | }
209 |
210 | case 'NETWORK_ERROR': {
211 | const { name, message } = clientMessage.payload
212 | const networkError = new Error(message)
213 | networkError.name = name
214 |
215 | // Rejecting a request Promise emulates a network error.
216 | throw networkError
217 | }
218 |
219 | case 'INTERNAL_ERROR': {
220 | const parsedBody = JSON.parse(clientMessage.payload.body)
221 |
222 | console.error(
223 | `\
224 | [MSW] Uncaught exception in the request handler for "%s %s":
225 |
226 | ${parsedBody.location}
227 |
228 | This exception has been gracefully handled as a 500 response, however, it's strongly recommended to resolve this error, as it indicates a mistake in your code. If you wish to mock an error response, please see this guide: https://mswjs.io/docs/recipes/mocking-error-responses\
229 | `,
230 | request.method,
231 | request.url,
232 | )
233 |
234 | return respondWithMock(clientMessage)
235 | }
236 | }
237 |
238 | return getOriginalResponse()
239 | }
240 |
241 | self.addEventListener('fetch', function (event) {
242 | const { request } = event
243 | const accept = request.headers.get('accept') || ''
244 |
245 | // Bypass server-sent events.
246 | if (accept.includes('text/event-stream')) {
247 | return
248 | }
249 |
250 | // Bypass navigation requests.
251 | if (request.mode === 'navigate') {
252 | return
253 | }
254 |
255 | // Opening the DevTools triggers the "only-if-cached" request
256 | // that cannot be handled by the worker. Bypass such requests.
257 | if (request.cache === 'only-if-cached' && request.mode !== 'same-origin') {
258 | return
259 | }
260 |
261 | // Bypass all requests when there are no active clients.
262 | // Prevents the self-unregistered worked from handling requests
263 | // after it's been deleted (still remains active until the next reload).
264 | if (activeClientIds.size === 0) {
265 | return
266 | }
267 |
268 | const requestId = uuidv4()
269 |
270 | return event.respondWith(
271 | handleRequest(event, requestId).catch((error) => {
272 | if (error.name === 'NetworkError') {
273 | console.warn(
274 | '[MSW] Successfully emulated a network error for the "%s %s" request.',
275 | request.method,
276 | request.url,
277 | )
278 | return
279 | }
280 |
281 | // At this point, any exception indicates an issue with the original request/response.
282 | console.error(
283 | `\
284 | [MSW] Caught an exception from the "%s %s" request (%s). This is probably not a problem with Mock Service Worker. There is likely an additional logging output above.`,
285 | request.method,
286 | request.url,
287 | `${error.name}: ${error.message}`,
288 | )
289 | }),
290 | )
291 | })
292 |
293 | function serializeHeaders(headers) {
294 | const reqHeaders = {}
295 | headers.forEach((value, name) => {
296 | reqHeaders[name] = reqHeaders[name]
297 | ? [].concat(reqHeaders[name]).concat(value)
298 | : value
299 | })
300 | return reqHeaders
301 | }
302 |
303 | function sendToClient(client, message) {
304 | return new Promise((resolve, reject) => {
305 | const channel = new MessageChannel()
306 |
307 | channel.port1.onmessage = (event) => {
308 | if (event.data && event.data.error) {
309 | return reject(event.data.error)
310 | }
311 |
312 | resolve(event.data)
313 | }
314 |
315 | client.postMessage(JSON.stringify(message), [channel.port2])
316 | })
317 | }
318 |
319 | function delayPromise(cb, duration) {
320 | return new Promise((resolve) => {
321 | setTimeout(() => resolve(cb()), duration)
322 | })
323 | }
324 |
325 | function respondWithMock(clientMessage) {
326 | return new Response(clientMessage.payload.body, {
327 | ...clientMessage.payload,
328 | headers: clientMessage.payload.headers,
329 | })
330 | }
331 |
332 | function uuidv4() {
333 | return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
334 | const r = (Math.random() * 16) | 0
335 | const v = c == 'x' ? r : (r & 0x3) | 0x8
336 | return v.toString(16)
337 | })
338 | }
339 |
--------------------------------------------------------------------------------
/src/components/WelcomeDashboard/noSessions.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------