├── .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 |
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 |
--------------------------------------------------------------------------------
/public/media/saveIcon.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/public/media/editIcon.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/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 |
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 |
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 |
--------------------------------------------------------------------------------
/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 |
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 |
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 |