├── .eslintrc.json ├── .husky └── pre-commit ├── public ├── favicon.ico └── media │ ├── closeIcon.svg │ ├── saveIcon.svg │ ├── plusIcon.svg │ ├── leftArrow.svg │ ├── rightArrow.svg │ ├── editIcon.svg │ ├── trashIcon.svg │ ├── personIcon.svg │ ├── successCheckmark.svg │ ├── profileDefaultIcon.svg │ ├── infoIcon.svg │ ├── failureWarning.svg │ ├── profileIcon.svg │ └── exclamationTriangle.svg ├── screenshots ├── mc-graphs.png ├── mc-scoring.png ├── faculty-activities.png └── faculty-submit-activity.png ├── postcss.config.js ├── .prettierrc ├── prisma ├── migrations │ ├── 20230205222050_init │ │ └── migration.sql │ ├── 20230223143921_init │ │ └── migration.sql │ ├── 20230312184546_change │ │ └── migration.sql │ ├── 20230319201312_init │ │ └── migration.sql │ ├── 20230319203529_init │ │ └── migration.sql │ ├── migration_lock.toml │ ├── 20230205205435_init │ │ └── migration.sql │ ├── 20230209223002_init │ │ └── migration.sql │ ├── 20230417011735_add_user_date_modified │ │ └── migration.sql │ ├── 20230209221759_init │ │ └── migration.sql │ ├── 20230205210355_init │ │ └── migration.sql │ ├── 20230205215107_init │ │ └── migration.sql │ └── 20230131035956_init │ │ └── migration.sql ├── faker │ ├── models.ts │ └── seed.ts └── schema.prisma ├── .env.example ├── src ├── pages │ ├── invalid-email.tsx │ ├── index.tsx │ ├── api │ │ ├── hello.ts │ │ ├── test.ts │ │ ├── restricted.ts │ │ ├── professor-info │ │ │ ├── [professorId].ts │ │ │ └── index.ts │ │ ├── professor-scores │ │ │ ├── weighted-score │ │ │ │ └── [professorId].ts │ │ │ ├── [professorId].ts │ │ │ └── index.ts │ │ ├── access-codes │ │ │ ├── obtain.ts │ │ │ └── index.ts │ │ ├── users │ │ │ ├── index.ts │ │ │ └── [id].ts │ │ ├── activities │ │ │ ├── [id].ts │ │ │ └── index.ts │ │ └── narratives │ │ │ └── index.ts │ ├── submissions │ │ ├── edit.tsx │ │ ├── index.tsx │ │ └── new.tsx │ ├── dashboard.tsx │ ├── _document.tsx │ ├── _app.tsx │ ├── auth │ │ ├── signout.tsx │ │ └── signin.tsx │ ├── merit │ │ ├── dashboard.tsx │ │ └── professors │ │ │ └── index.tsx │ ├── profile.tsx │ └── account-setup.tsx ├── shared │ ├── utils │ │ ├── date.utils.ts │ │ ├── narrative.util.ts │ │ ├── professorScore.util.ts │ │ ├── misc.util.ts │ │ ├── activity.util.ts │ │ └── user.util.ts │ └── components │ │ ├── ErrorMessage.tsx │ │ ├── StaticSideBarBubble.tsx │ │ ├── Tooltip.tsx │ │ ├── TextField.tsx │ │ ├── Checkbox.tsx │ │ ├── Unauthorized.tsx │ │ ├── InfoTooltip.tsx │ │ ├── TextAreaInput.tsx │ │ ├── Header.tsx │ │ ├── AppLayout.tsx │ │ ├── TextInput.tsx │ │ ├── Button.tsx │ │ ├── Navbar.tsx │ │ ├── SideBarBubble.tsx │ │ ├── InputContainer.tsx │ │ └── DropdownInput.tsx ├── middleware.ts ├── models │ ├── professorScore.model.ts │ ├── narrative.model.ts │ ├── user.model.ts │ ├── professorInfo.model.ts │ └── activity.model.ts ├── components │ ├── Profile │ │ ├── Avatar.tsx │ │ ├── ProfileInfoSection.tsx │ │ ├── BasicInfo.tsx │ │ ├── ProfileInstructions.tsx │ │ ├── PercentageInfo.tsx │ │ ├── ProfileContainer.tsx │ │ └── ContactInfo.tsx │ ├── ActivityForm │ │ ├── Activity │ │ │ └── ActivityCard.tsx │ │ ├── FormContainer.tsx │ │ ├── FormInstructions.tsx │ │ ├── ResultPage.tsx │ │ └── CategorySelector.tsx │ ├── ProfessorScoring │ │ ├── ProfessorScoreItem.tsx │ │ ├── TenureBadge.tsx │ │ ├── ScoringInfo.tsx │ │ ├── NarrativeCard.tsx │ │ ├── ActivityGroup.tsx │ │ └── ProfessorCommentBox.tsx │ ├── ErrorBanner.tsx │ ├── ProfessorSearch │ │ ├── ProfessorCardGroup.tsx │ │ └── CommentBubble.tsx │ ├── Narratives │ │ └── NarrativeInstructions.tsx │ ├── AccountSetup │ │ ├── StepWrapper.tsx │ │ ├── StepIndicator.tsx │ │ └── RoleSetup.tsx │ ├── AdminPage │ │ └── NewUserRow.tsx │ └── Merit │ │ └── ScoreScatterplot.tsx ├── store │ ├── app.store.ts │ ├── submissions.store.ts │ ├── professorScore.store.ts │ └── accountSetup.store.ts ├── services │ ├── accessCode.ts │ ├── professorInfo.ts │ ├── narrative.ts │ ├── user.ts │ └── activity.ts ├── client │ ├── professorInfo.client.ts │ ├── users.client.ts │ ├── narratives.client.ts │ └── accessCodes.client.ts └── styles │ └── index.css ├── docker-compose.yml ├── .github ├── pull_request_template.md └── workflows │ └── main.yml ├── lib └── db.ts ├── next.config.js ├── types └── next-auth.d.ts ├── .gitignore ├── tsconfig.json ├── package.json └── tailwind.config.js /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals", 3 | "ignorePatterns": [".github"] 4 | } 5 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npx lint-staged 5 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sandboxnu/faculty-activity-tracker/main/public/favicon.ico -------------------------------------------------------------------------------- /screenshots/mc-graphs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sandboxnu/faculty-activity-tracker/main/screenshots/mc-graphs.png -------------------------------------------------------------------------------- /screenshots/mc-scoring.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sandboxnu/faculty-activity-tracker/main/screenshots/mc-scoring.png -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all", 4 | "plugins": ["prettier-plugin-tailwindcss"] 5 | } 6 | -------------------------------------------------------------------------------- /screenshots/faculty-activities.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sandboxnu/faculty-activity-tracker/main/screenshots/faculty-activities.png -------------------------------------------------------------------------------- /prisma/migrations/20230205222050_init/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "Activity" ALTER COLUMN "dateModified" SET DATA TYPE TEXT; 3 | -------------------------------------------------------------------------------- /screenshots/faculty-submit-activity.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sandboxnu/faculty-activity-tracker/main/screenshots/faculty-submit-activity.png -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | DATABASE_URL="postgresql://sandbox:chongus@localhost:5432/fat?schema=public" 2 | 3 | GOOGLE_CLIENT_ID= 4 | GOOGLE_CLIENT_SECRET= 5 | 6 | NEXTAUTH_SECRET= -------------------------------------------------------------------------------- /prisma/migrations/20230223143921_init/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "Activity" ALTER COLUMN "dateModified" SET DEFAULT extract(epoch from now())::bigint; 3 | -------------------------------------------------------------------------------- /prisma/migrations/20230312184546_change/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "Activity" ALTER COLUMN "dateModified" SET DEFAULT extract(epoch from now())::bigint; 3 | -------------------------------------------------------------------------------- /prisma/migrations/20230319201312_init/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "Activity" ALTER COLUMN "dateModified" SET DEFAULT extract(epoch from now())::bigint; 3 | -------------------------------------------------------------------------------- /prisma/migrations/20230319203529_init/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "Activity" ALTER COLUMN "dateModified" SET DEFAULT extract(epoch from now())::bigint; 3 | -------------------------------------------------------------------------------- /prisma/migrations/migration_lock.toml: -------------------------------------------------------------------------------- 1 | # Please do not edit this file manually 2 | # It should be added in your version-control system (i.e. Git) 3 | provider = "postgresql" -------------------------------------------------------------------------------- /prisma/migrations/20230205205435_init/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "Activity" ALTER COLUMN "dateModified" SET DATA TYPE TEXT; 3 | 4 | -- AlterTable 5 | ALTER TABLE "Narrative" ALTER COLUMN "dateModified" SET DATA TYPE TEXT; 6 | -------------------------------------------------------------------------------- /prisma/migrations/20230209223002_init/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "Activity" ALTER COLUMN "dateModified" SET DATA TYPE BIGINT; 3 | 4 | -- AlterTable 5 | ALTER TABLE "Narrative" ALTER COLUMN "dateModified" SET DATA TYPE BIGINT; 6 | -------------------------------------------------------------------------------- /src/pages/invalid-email.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const InvalidEmailPage: React.FC = () => { 4 | return ( 5 |
6 |

Invalid Email.

7 |
8 | ); 9 | }; 10 | 11 | export default InvalidEmailPage; 12 | -------------------------------------------------------------------------------- /src/shared/utils/date.utils.ts: -------------------------------------------------------------------------------- 1 | export const createDateFromString = (date: string): Date | null => { 2 | const newDate: Date = new Date(date); 3 | if (newDate.toString() === 'Invalid Date') { 4 | return null; 5 | } 6 | return newDate; 7 | }; 8 | -------------------------------------------------------------------------------- /src/middleware.ts: -------------------------------------------------------------------------------- 1 | export { default } from 'next-auth/middleware'; 2 | 3 | export const config = { 4 | matcher: [ 5 | '/submissions', 6 | '/submissions/(.*)', 7 | '/narratives/(.*)', 8 | '/merit/(.*)', 9 | '/profile', 10 | '/account-setup', 11 | ], 12 | }; 13 | -------------------------------------------------------------------------------- /prisma/migrations/20230417011735_add_user_date_modified/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "Activity" ALTER COLUMN "dateModified" SET DEFAULT extract(epoch from now())::bigint; 3 | 4 | -- AlterTable 5 | ALTER TABLE "User" ADD COLUMN "dateModified" BIGINT NOT NULL DEFAULT extract(epoch from now())::bigint; 6 | -------------------------------------------------------------------------------- /public/media/closeIcon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /public/media/saveIcon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const Home: React.FC = () => { 4 | return ( 5 |
6 | {/* Not really used now since "/" is redirected to "/dashboard" (see next.config.js) */} 7 |

Index file

8 |
9 | ); 10 | }; 11 | 12 | export default Home; 13 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | services: 3 | postgres: 4 | image: postgres 5 | restart: always 6 | environment: 7 | - POSTGRES_USER=sandbox 8 | - POSTGRES_PASSWORD=chongus 9 | - POSTGRES_DB=fat 10 | volumes: 11 | - postgres:/var/lib/postgresql/data 12 | ports: 13 | - '5432:5432' 14 | volumes: 15 | postgres: 16 | -------------------------------------------------------------------------------- /src/pages/api/hello.ts: -------------------------------------------------------------------------------- 1 | // Next.js API route support: https://nextjs.org/docs/api-routes/introduction 2 | import type { NextApiRequest, NextApiResponse } from 'next'; 3 | 4 | type Data = { 5 | name: string; 6 | }; 7 | 8 | export default function handler( 9 | req: NextApiRequest, 10 | res: NextApiResponse, 11 | ) { 12 | res.status(200).json({ name: 'John Doe' }); 13 | } 14 | -------------------------------------------------------------------------------- /src/models/professorScore.model.ts: -------------------------------------------------------------------------------- 1 | import { ProfessorScore } from '@prisma/client'; 2 | 3 | export type ProfessScoreDto = ProfessorScore; 4 | 5 | export type CreateProfessorScoreDto = Omit; 6 | 7 | export type GetProfessorScore = { userId: number }; 8 | 9 | export type UpdateProfessorScoreDto = Partial & { 10 | userId: number; 11 | }; 12 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | Closes #(Ticket Number) 2 | 3 | ## Description 4 | 5 | ## Things you especially want reviewed 6 | 7 | ## Screenshots if Applicable 8 | 9 | ## Checklist 10 | 11 | - [ ] Ticket number in PR title 12 | - [ ] Add ticket number to ("Closes #") 13 | - [ ] Move status to Code Review in GH Board 14 | - [ ] People added to reviewers 15 | - [ ] Asked for Review in Slack 16 | -------------------------------------------------------------------------------- /src/pages/api/test.ts: -------------------------------------------------------------------------------- 1 | // Next.js API route support: https://nextjs.org/docs/api-routes/introduction 2 | import type { NextApiRequest, NextApiResponse } from 'next'; 3 | import prisma from 'lib/db'; 4 | 5 | export default async function handler( 6 | req: NextApiRequest, 7 | res: NextApiResponse, 8 | ) { 9 | const data = await prisma.user.findMany(); 10 | console.table(data); 11 | 12 | res.json(data); 13 | } 14 | -------------------------------------------------------------------------------- /public/media/plusIcon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/shared/components/ErrorMessage.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | interface ErrorMessageProps { 4 | message?: string; 5 | } 6 | 7 | const ErrorMessage: React.FC = ({ 8 | message, 9 | }: ErrorMessageProps) => { 10 | return ( 11 |

12 | Error: {message || 'Unknown Error'} 13 |

14 | ); 15 | }; 16 | 17 | export default ErrorMessage; 18 | -------------------------------------------------------------------------------- /public/media/leftArrow.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | < 5 | rect width=“17" height=“24” fill=“white” transform=“translate(17 24) rotate(-180)“/> 6 | -------------------------------------------------------------------------------- /src/components/Profile/Avatar.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const Avatar: React.FC<{ initials: string }> = ({ initials }) => ( 4 |
5 |
6 |
7 | {initials} 8 |
9 |
10 |
11 | ); 12 | 13 | export default Avatar; 14 | -------------------------------------------------------------------------------- /src/shared/components/StaticSideBarBubble.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const StaticSideBarBubble: React.FC<{ 4 | title: string; 5 | children: JSX.Element; 6 | }> = ({ title, children }) => { 7 | return ( 8 |
9 |
{title}
10 |
{children}
11 |
12 | ); 13 | }; 14 | 15 | export default StaticSideBarBubble; 16 | -------------------------------------------------------------------------------- /lib/db.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from '@prisma/client'; 2 | 3 | const prismaClientSingleton = () => { 4 | return new PrismaClient(); 5 | }; 6 | 7 | type PrismaClientSingleton = ReturnType; 8 | 9 | const globalForPrisma = globalThis as unknown as { 10 | prisma: PrismaClientSingleton | undefined; 11 | }; 12 | 13 | const prisma = globalForPrisma.prisma ?? prismaClientSingleton(); 14 | 15 | export default prisma; 16 | 17 | if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma; 18 | -------------------------------------------------------------------------------- /public/media/rightArrow.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /public/media/editIcon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/shared/components/Tooltip.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | interface TooltipProps { 4 | tooltipTitle: string; 5 | text: string[]; 6 | } 7 | 8 | const Tooltip: React.FC = ({ 9 | tooltipTitle, 10 | text, 11 | }: TooltipProps) => { 12 | return ( 13 |
14 | {tooltipTitle} 15 | 16 | {text.map((item) => { 17 | return

{item}

; 18 | })} 19 |
20 |
21 | ); 22 | }; 23 | 24 | export default Tooltip; 25 | -------------------------------------------------------------------------------- /src/components/ActivityForm/Activity/ActivityCard.tsx: -------------------------------------------------------------------------------- 1 | import { ActivityDto } from '@/models/activity.model'; 2 | import React from 'react'; 3 | 4 | interface ActivityCardProps { 5 | activity: ActivityDto; 6 | } 7 | 8 | const ActivityCard: React.FC = ({ activity }) => { 9 | return ( 10 |
11 |
12 |

{activity.name}

13 |
14 |
15 | ); 16 | }; 17 | 18 | export default ActivityCard; 19 | -------------------------------------------------------------------------------- /src/components/ActivityForm/FormContainer.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useSelector } from 'react-redux'; 3 | import { FormStep, selectStep } from '../../store/form.store'; 4 | import FormInstructions from './FormInstructions'; 5 | 6 | const FormContainer: React.FC<{ children: JSX.Element }> = ({ children }) => { 7 | const step: FormStep = useSelector(selectStep); 8 | return ( 9 |
10 |
{children}
11 |
12 | ); 13 | }; 14 | 15 | export default FormContainer; 16 | -------------------------------------------------------------------------------- /src/shared/components/TextField.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import clsx from 'clsx'; 3 | 4 | interface TextFieldProps { 5 | value: string; 6 | fillContainer?: boolean; 7 | } 8 | 9 | const TextField: React.FC = ({ 10 | value, 11 | fillContainer = false, 12 | }) => ( 13 |

19 | {value || <> } 20 |

21 | ); 22 | 23 | export default TextField; 24 | -------------------------------------------------------------------------------- /src/components/ProfessorScoring/ProfessorScoreItem.tsx: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx'; 2 | 3 | interface ProfessorScoreItemProps { 4 | category: string; 5 | score: number; 6 | className?: string; 7 | } 8 | 9 | const ProfessorScoreItem: React.FC = ({ 10 | category, 11 | score, 12 | className, 13 | }) => { 14 | return ( 15 |
16 |

{category}

17 |

{score}

18 |
19 | ); 20 | }; 21 | 22 | export default ProfessorScoreItem; 23 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | const path = require('node:path'); 2 | /** @type {import('next').NextConfig} */ 3 | 4 | const nextConfig = { 5 | async redirects() { 6 | return [ 7 | { 8 | source: '/', 9 | destination: '/dashboard', 10 | permanent: false, 11 | }, 12 | ]; 13 | }, 14 | 15 | sassOptions: { 16 | includePaths: [path.join(__dirname, 'styles')], 17 | }, 18 | webpack(config) { 19 | config.module.rules.push({ 20 | test: /\.svg$/, 21 | use: ['@svgr/webpack'], 22 | }); 23 | 24 | return config; 25 | }, 26 | }; 27 | 28 | module.exports = nextConfig; 29 | -------------------------------------------------------------------------------- /src/components/Profile/ProfileInfoSection.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | interface ProfileInfoSectionProps { 4 | label: string; 5 | children: JSX.Element; 6 | } 7 | 8 | const ProfileInfoSection: React.FC = ({ 9 | label, 10 | children, 11 | }) => { 12 | return ( 13 |
14 |
15 |

{label}

16 |
17 |
18 | {children} 19 |
20 | ); 21 | }; 22 | 23 | export default ProfileInfoSection; 24 | -------------------------------------------------------------------------------- /src/shared/components/Checkbox.tsx: -------------------------------------------------------------------------------- 1 | import React, { ChangeEventHandler } from 'react'; 2 | 3 | interface CheckboxProps { 4 | label: string; 5 | value: boolean; 6 | onChange: ChangeEventHandler; 7 | } 8 | 9 | export const Checkbox: React.FC = ({ 10 | label, 11 | value, 12 | onChange, 13 | }: CheckboxProps) => { 14 | return ( 15 | 24 | ); 25 | }; 26 | -------------------------------------------------------------------------------- /types/next-auth.d.ts: -------------------------------------------------------------------------------- 1 | import NextAuth, { Account, DefaultSession } from 'next-auth'; 2 | 3 | declare module 'next-auth' { 4 | /** 5 | * Returned by `useSession`, `getSession` and received as a prop on the `SessionProvider` React Context 6 | */ 7 | interface Session { 8 | accessToken?: Account.accessToken; 9 | user: { 10 | id: number | undefined; 11 | admin?: boolean | undefined; 12 | merit?: boolean | undefined; 13 | } & DefaultSession['user']; 14 | } 15 | } 16 | 17 | declare module 'next-auth/jwt' { 18 | interface JWT { 19 | accessToken?: Account.accessToken; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | .vscode 3 | 4 | # dependencies 5 | /node_modules 6 | /.pnp 7 | .pnp.js 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | /prisma/migrations 23 | *.lock 24 | 25 | # debug 26 | npm-debug.log* 27 | yarn-debug.log* 28 | yarn-error.log* 29 | .pnpm-debug.log* 30 | 31 | # local env files 32 | .env 33 | .env*.local 34 | 35 | # vercel 36 | .vercel 37 | 38 | # typescript 39 | *.tsbuildinfo 40 | next-env.d.ts 41 | .env 42 | .env 43 | .vscode -------------------------------------------------------------------------------- /src/shared/components/Unauthorized.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Link from 'next/link'; 3 | import { signIn } from 'next-auth/react'; 4 | 5 | const Unauthorized: React.FC = () => { 6 | return ( 7 |
8 |
9 |

You must be logged in to view this page!

10 | 16 |
17 | ); 18 | }; 19 | 20 | export default Unauthorized; 21 | -------------------------------------------------------------------------------- /src/components/ProfessorScoring/TenureBadge.tsx: -------------------------------------------------------------------------------- 1 | interface TenureBadgeProps { 2 | isTenure: boolean; 3 | } 4 | 5 | const TenureBadge: React.FC = ({ isTenure }) => { 6 | return ( 7 |
12 |
13 | {isTenure && <>TT/T} 14 | {!isTenure && <>NT} 15 |
16 |
17 | ); 18 | }; 19 | 20 | export default TenureBadge; 21 | -------------------------------------------------------------------------------- /src/models/narrative.model.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Narrative, 3 | NarrativeCategory as PrismaNarrativeCategory, 4 | } from '@prisma/client'; 5 | 6 | export type NarrativeCategory = PrismaNarrativeCategory; // "SUMMARY" | "SERVICE" | "RESEARCH" | "TEACHING" 7 | export type NarrativeDto = Narrative; /* 8 | { 9 | id: number; 10 | userId: number; 11 | year: number; 12 | dateModified: bigint; 13 | category: NarrativeCategory; 14 | text: string; 15 | }*/ 16 | 17 | // id is generated by db 18 | export type CreateNarrativeDto = Omit; 19 | export type UpdateNarrativeDto = Partial; 20 | export type DeleteNarrativeDto = Pick; 21 | -------------------------------------------------------------------------------- /public/media/trashIcon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /prisma/migrations/20230209221759_init/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - Changed the type of `dateModified` on the `Activity` table. No cast exists, the column would be dropped and recreated, which cannot be done if there is data, since the column is required. 5 | - Changed the type of `dateModified` on the `Narrative` table. No cast exists, the column would be dropped and recreated, which cannot be done if there is data, since the column is required. 6 | 7 | */ 8 | -- AlterTable 9 | ALTER TABLE "Activity" DROP COLUMN "dateModified", 10 | ADD COLUMN "dateModified" INTEGER NOT NULL; 11 | 12 | -- AlterTable 13 | ALTER TABLE "Narrative" DROP COLUMN "dateModified", 14 | ADD COLUMN "dateModified" INTEGER NOT NULL; 15 | -------------------------------------------------------------------------------- /src/models/user.model.ts: -------------------------------------------------------------------------------- 1 | import { Activity, Narrative, ProfessorInfo, Role, User } from '@prisma/client'; 2 | 3 | export type UserDto = User; 4 | 5 | export type UserWithInfo = User & { professorInfo: ProfessorInfo | null }; 6 | 7 | export type UserWithActivities = User & { activities: Activity[] }; 8 | 9 | export type UserWithAllData = User & { 10 | professorInfo: ProfessorInfo | null; 11 | activities: Activity[]; 12 | narratives: Narrative[]; 13 | }; 14 | 15 | export type CreateUserDto = Omit; 16 | 17 | export type UpdateUserDto = Partial; 18 | 19 | export type SortOrder = 'asc' | 'desc'; 20 | 21 | export type UserOrderByQuery = Partial<{ 22 | [Property in keyof UserDto]: SortOrder; 23 | }>; 24 | -------------------------------------------------------------------------------- /prisma/migrations/20230205210355_init/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - Changed the type of `dateModified` on the `Activity` table. No cast exists, the column would be dropped and recreated, which cannot be done if there is data, since the column is required. 5 | - Changed the type of `dateModified` on the `Narrative` table. No cast exists, the column would be dropped and recreated, which cannot be done if there is data, since the column is required. 6 | 7 | */ 8 | -- AlterTable 9 | ALTER TABLE "Activity" DROP COLUMN "dateModified", 10 | ADD COLUMN "dateModified" TIMESTAMP(3) NOT NULL; 11 | 12 | -- AlterTable 13 | ALTER TABLE "Narrative" DROP COLUMN "dateModified", 14 | ADD COLUMN "dateModified" TIMESTAMP(3) NOT NULL; 15 | -------------------------------------------------------------------------------- /src/store/app.store.ts: -------------------------------------------------------------------------------- 1 | import { configureStore } from '@reduxjs/toolkit'; 2 | import formReducer from './form.store'; 3 | import submissionReducer from './submissions.store'; 4 | import profileReducer from './profile.store'; 5 | import accountSetupReducer from './accountSetup.store'; 6 | import professorScoreReducer from './professorScore.store'; 7 | 8 | export const store = configureStore({ 9 | reducer: { 10 | accountSetup: accountSetupReducer, 11 | form: formReducer, 12 | profile: profileReducer, 13 | submissions: submissionReducer, 14 | professorScore: professorScoreReducer, 15 | }, 16 | }); 17 | 18 | export type RootState = ReturnType; 19 | 20 | export type AppDispatch = typeof store.dispatch; 21 | -------------------------------------------------------------------------------- /src/shared/utils/narrative.util.ts: -------------------------------------------------------------------------------- 1 | import { SignificanceLevel } from '@prisma/client'; 2 | import { 3 | ActivityCategory, 4 | ActivityDto, 5 | Semester, 6 | } from '../../models/activity.model'; 7 | import { NarrativeCategory, NarrativeDto } from '@/models/narrative.model'; 8 | 9 | export const seperateNarrativesByCategory = ( 10 | narratives: NarrativeDto[], 11 | ): Record => { 12 | let narrativesByCategory: Record = { 13 | SUMMARY: [], 14 | TEACHING: [], 15 | RESEARCH: [], 16 | SERVICE: [], 17 | }; 18 | for (let narrative of narratives) { 19 | narrativesByCategory[narrative.category].push(narrative); 20 | } 21 | return narrativesByCategory; 22 | }; 23 | -------------------------------------------------------------------------------- /src/pages/submissions/edit.tsx: -------------------------------------------------------------------------------- 1 | import FormContainer from '@/components/ActivityForm/FormContainer'; 2 | import FormInput from '@/components/ActivityForm/FormInput'; 3 | import Head from 'next/head'; 4 | import React, { useEffect } from 'react'; 5 | 6 | const EditActivityForm: React.FunctionComponent = (props) => { 7 | useEffect(() => { 8 | window.onbeforeunload = () => { 9 | return 'Data will be lost if you leave the page, are you sure?'; 10 | }; 11 | }, []); 12 | 13 | return ( 14 | <> 15 | 16 | Edit Submission 17 | 18 | 19 | 20 | 21 | 22 | ); 23 | }; 24 | 25 | export default EditActivityForm; 26 | -------------------------------------------------------------------------------- /src/pages/submissions/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Link from 'next/link'; 3 | import { toTitleCase } from '@/shared/utils/misc.util'; 4 | 5 | const SubmissionsPage: React.FC = () => { 6 | return ( 7 |
8 |

Activities

9 |
10 | {['teaching', 'service', 'research'].map((category) => ( 11 |
15 | 16 | {toTitleCase(category)} 17 | 18 |
19 | ))} 20 |
21 |
22 | ); 23 | }; 24 | 25 | export default SubmissionsPage; 26 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2017", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "CommonJS", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true, 17 | "plugins": [ 18 | { 19 | "name": "next" 20 | } 21 | ], 22 | "baseUrl": ".", 23 | "paths": { 24 | "@/*": ["./src/*"], 25 | "react": ["./node_modules/@types/react"] 26 | } 27 | }, 28 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 29 | "exclude": ["node_modules"] 30 | } 31 | -------------------------------------------------------------------------------- /src/pages/dashboard.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Unauthorized from '@/shared/components/Unauthorized'; 3 | import { useSession } from 'next-auth/react'; 4 | import Head from 'next/head'; 5 | 6 | const Dashboard = () => { 7 | const { data: session, status } = useSession(); 8 | const name = session?.user?.name; 9 | const email = session?.user?.email; 10 | 11 | if (status === 'loading') { 12 | return

Loading...

; 13 | } 14 | 15 | if (status === 'unauthenticated') { 16 | return ; 17 | } 18 | 19 | return ( 20 |
21 | 22 | Dashboard 23 | 24 |

Dashboard

25 |

Welcome, {name || 'User'}!

26 |
27 | ); 28 | }; 29 | 30 | export default Dashboard; 31 | -------------------------------------------------------------------------------- /prisma/migrations/20230205215107_init/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - The values [SUMMER1,SUMMER2] on the enum `Semester` will be removed. If these variants are still used in the database, this will fail. 5 | - Changed the column `semester` on the `Activity` table from a scalar field to a list field. If there are non-null values in that column, this step will fail. 6 | 7 | */ 8 | -- AlterEnum 9 | BEGIN; 10 | CREATE TYPE "Semester_new" AS ENUM ('FALL', 'SPRING', 'SUMMER', 'OTHER'); 11 | ALTER TABLE "Activity" ALTER COLUMN "semester" TYPE "Semester_new"[] USING ("semester"::text::"Semester_new"[]); 12 | ALTER TYPE "Semester" RENAME TO "Semester_old"; 13 | ALTER TYPE "Semester_new" RENAME TO "Semester"; 14 | DROP TYPE "Semester_old"; 15 | COMMIT; 16 | 17 | -- AlterTable 18 | ALTER TABLE "Activity" ALTER COLUMN "semester" SET DATA TYPE "Semester"[]; 19 | -------------------------------------------------------------------------------- /src/store/submissions.store.ts: -------------------------------------------------------------------------------- 1 | import { createSlice, Selector, PayloadAction } from '@reduxjs/toolkit'; 2 | import { RootState } from './app.store'; 3 | import { ActivityDto } from '../models/activity.model'; 4 | 5 | export interface SubmissionState { 6 | activities: ActivityDto[]; 7 | } 8 | 9 | const initialState: SubmissionState = { 10 | activities: [], 11 | }; 12 | 13 | export const submissionSlice = createSlice({ 14 | name: 'Submission', 15 | initialState, 16 | reducers: { 17 | saveActivities: (state, action: PayloadAction) => { 18 | state.activities = action.payload; 19 | }, 20 | }, 21 | }); 22 | 23 | export const { saveActivities } = submissionSlice.actions; 24 | 25 | export const selectActivities: Selector = (state) => 26 | state.submissions.activities; 27 | 28 | export default submissionSlice.reducer; 29 | -------------------------------------------------------------------------------- /src/pages/_document.tsx: -------------------------------------------------------------------------------- 1 | import { Html, Head, Main, NextScript } from 'next/document'; 2 | 3 | function Document() { 4 | return ( 5 | 6 | 7 | 8 | 13 | 17 | 18 | 19 | 20 |
21 | 22 | 23 | 24 | ); 25 | } 26 | 27 | export default Document; 28 | -------------------------------------------------------------------------------- /src/shared/components/InfoTooltip.tsx: -------------------------------------------------------------------------------- 1 | import Image from 'next/image'; 2 | import React from 'react'; 3 | import { toTitleCase } from '../utils/misc.util'; 4 | 5 | export type TooltipPosition = 'bottom' | 'right'; 6 | 7 | interface InfoTooltipProps { 8 | text: string[]; 9 | tooltipPosition?: TooltipPosition; 10 | } 11 | 12 | const InfoTooltip: React.FC = ({ 13 | text, 14 | tooltipPosition = 'bottom', 15 | }) => { 16 | return ( 17 |
18 | i 19 | 20 | {text.map((item) => { 21 | return ( 22 |

23 | {item} 24 |

25 | ); 26 | })} 27 |
28 |
29 | ); 30 | }; 31 | 32 | export default InfoTooltip; 33 | -------------------------------------------------------------------------------- /src/services/accessCode.ts: -------------------------------------------------------------------------------- 1 | import { Role } from '.prisma/client'; 2 | import { RoleAccessCode } from '@prisma/client'; 3 | import prisma from 'lib/db'; 4 | 5 | export const getAccessCodes = async (): Promise => { 6 | const roleAccessCodes = await prisma.roleAccessCode.findMany(); 7 | 8 | return roleAccessCodes; 9 | }; 10 | 11 | export const setAccessCode = async ( 12 | role: Role, 13 | newCode: string, 14 | ): Promise => { 15 | const roleAccessCodes = await prisma.roleAccessCode.update({ 16 | where: { 17 | role: role, 18 | }, 19 | data: { 20 | accessCode: newCode, 21 | }, 22 | }); 23 | 24 | return roleAccessCodes || null; 25 | }; 26 | 27 | export const obtainRole = async (accessCode: string): Promise => { 28 | const roleAccessCode = await prisma.roleAccessCode.findFirst({ 29 | where: { accessCode }, 30 | }); 31 | return roleAccessCode?.role || null; 32 | }; 33 | -------------------------------------------------------------------------------- /src/store/professorScore.store.ts: -------------------------------------------------------------------------------- 1 | import { createSlice, PayloadAction } from '@reduxjs/toolkit'; 2 | import { RootState } from './app.store'; 3 | import { CreateProfessorScoreDto } from '../models/professorScore.model'; 4 | 5 | export interface ProfessorScoreState { 6 | professorScore: CreateProfessorScoreDto | null; 7 | } 8 | 9 | const initialState: ProfessorScoreState = { 10 | professorScore: null, 11 | }; 12 | 13 | export const professorScoreSlice = createSlice({ 14 | name: 'ProfessorScore', 15 | initialState, 16 | reducers: { 17 | saveProfessorScore: ( 18 | state, 19 | action: PayloadAction, 20 | ) => { 21 | state.professorScore = action.payload; 22 | }, 23 | }, 24 | }); 25 | 26 | export const { saveProfessorScore } = professorScoreSlice.actions; 27 | 28 | export const selectProfessorScores = (state: RootState) => 29 | state.professorScore.professorScore; 30 | 31 | export default professorScoreSlice.reducer; 32 | -------------------------------------------------------------------------------- /src/pages/api/restricted.ts: -------------------------------------------------------------------------------- 1 | // Next.js API route support: https://nextjs.org/docs/api-routes/introduction 2 | import type { NextApiRequest, NextApiResponse } from 'next'; 3 | 4 | import { getServerSession } from 'next-auth'; 5 | import { authOptions } from './auth/[...nextauth]'; 6 | 7 | import { getToken } from 'next-auth/jwt'; 8 | 9 | export default async function handler( 10 | req: NextApiRequest, 11 | res: NextApiResponse, 12 | ) { 13 | // const session = await getServerSession(req, res, authOptions) 14 | // console.log(session) 15 | 16 | // if (session){ 17 | // console.log("user logged in") 18 | // const email = session.user?.email 19 | // console.log("email: ", email) 20 | // res.send(`Logged in as ${email}`) 21 | // } 22 | // else { 23 | // console.log("user not logged in") 24 | // res.send("not logged in") 25 | // } 26 | 27 | const token = await getToken({ req, secret: 'sec' }); 28 | console.log(token); 29 | res.send(token); 30 | } 31 | -------------------------------------------------------------------------------- /src/pages/api/professor-info/[professorId].ts: -------------------------------------------------------------------------------- 1 | import type { NextApiRequest, NextApiResponse } from 'next'; 2 | import { getProfessorInfoForUser } from '@/services/professorInfo'; 3 | 4 | export default async function handler( 5 | req: NextApiRequest, 6 | res: NextApiResponse, 7 | ) { 8 | const { professorId } = req.query; 9 | if (!professorId || isNaN(parseInt(professorId.toString()))) { 10 | res.status(400).json({ error: 'Missing/invalid user id.' }); 11 | } else { 12 | if (req.method === 'GET') { 13 | await handleGet(parseInt(professorId.toString()), res); 14 | } else { 15 | res.setHeader('Allow', ['GET']); 16 | res.status(405).end(`Method ${req.method} Not Allowed`); 17 | } 18 | } 19 | } 20 | 21 | async function handleGet(userId: number, res: NextApiResponse) { 22 | const info = await getProfessorInfoForUser(userId); 23 | if (info) { 24 | res.status(200).json({ data: info }); 25 | } else { 26 | res.status(404).end(`Professor info for user with id: ${userId} Not Found`); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /public/media/personIcon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/shared/components/TextAreaInput.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import clsx from 'clsx'; 3 | import { incompleteBorderClass } from './InputContainer'; 4 | 5 | export interface TextAreaInputProps { 6 | value: string | number; 7 | change: (val: string) => void; 8 | numRows?: number; 9 | placeholder?: string; 10 | fillContainer?: boolean; 11 | addOnClass?: string; 12 | } 13 | 14 | const TextAreaInput: React.FC = ({ 15 | value, 16 | change, 17 | numRows = 3, 18 | placeholder, 19 | fillContainer = false, 20 | addOnClass = '', 21 | }) => { 22 | return ( 23 |