Skip to content

Commit

Permalink
Merge pull request #687 from pennlabs/alert-2.0-sms
Browse files Browse the repository at this point in the history
Add SMS alerts to PCAv2
  • Loading branch information
esinx authored Nov 19, 2024
2 parents 007a2fd + b6ce58e commit cec310e
Show file tree
Hide file tree
Showing 126 changed files with 10,828 additions and 0 deletions.
24 changes: 24 additions & 0 deletions frontend/alert-2.0/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*

node_modules
dist
dist-ssr
*.local

# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
50 changes: 50 additions & 0 deletions frontend/alert-2.0/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# React + TypeScript + Vite

This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.

Currently, two official plugins are available:

- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh

## Expanding the ESLint configuration

If you are developing a production application, we recommend updating the configuration to enable type aware lint rules:

- Configure the top-level `parserOptions` property like this:

```js
export default tseslint.config({
languageOptions: {
// other options...
parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'],
tsconfigRootDir: import.meta.dirname,
},
},
})
```

- Replace `tseslint.configs.recommended` to `tseslint.configs.recommendedTypeChecked` or `tseslint.configs.strictTypeChecked`
- Optionally add `...tseslint.configs.stylisticTypeChecked`
- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and update the config:

```js
// eslint.config.js
import react from 'eslint-plugin-react'

export default tseslint.config({
// Set the react version
settings: { react: { version: '18.3' } },
plugins: {
// Add the react plugin
react,
},
rules: {
// other rules...
// Enable its recommended rules
...react.configs.recommended.rules,
...react.configs['jsx-runtime'].rules,
},
})
```
20 changes: 20 additions & 0 deletions frontend/alert-2.0/components.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": false,
"tsx": true,
"tailwind": {
"config": "tailwind.config.js",
"css": "src/index.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
}
}
3 changes: 3 additions & 0 deletions frontend/alert-2.0/eslint.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import config from "@esinx/eslint-config";

export default [...config]
13 changes: 13 additions & 0 deletions frontend/alert-2.0/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React + TS</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
68 changes: 68 additions & 0 deletions frontend/alert-2.0/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
{
"name": "alert-2.0",
"private": true,
"version": "0.0.0",
"type": "module",
"prettier": "@esinx/prettier-config",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"@hookform/resolvers": "^3.9.0",
"@radix-ui/react-avatar": "^1.1.0",
"@radix-ui/react-dialog": "^1.1.1",
"@radix-ui/react-icons": "^1.3.0",
"@radix-ui/react-label": "^2.1.0",
"@radix-ui/react-navigation-menu": "^1.2.0",
"@radix-ui/react-popover": "^1.1.1",
"@radix-ui/react-slot": "^1.1.0",
"@radix-ui/react-switch": "^1.1.0",
"@tanstack/react-query": "^5.53.3",
"@tanstack/react-router": "^1.51.6",
"@trpc/client": "^11.0.0-rc.498",
"@trpc/react-query": "^11.0.0-rc.498",
"@trpc/server": "^11.0.0-rc.498",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"cmdk": "1.0.0",
"lucide-react": "^0.436.0",
"next-themes": "^0.3.0",
"oidc-client-ts": "^3.0.1",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-error-boundary": "^4.0.13",
"react-hook-form": "^7.53.0",
"react-oidc-context": "^3.1.0",
"sonner": "^1.5.0",
"tailwind-merge": "^2.5.2",
"tailwindcss-animate": "^1.0.7",
"ts-pattern": "^5.3.1",
"use-debounce": "^10.0.3",
"zod": "^3.23.8",
"zustand": "^4.5.5"
},
"devDependencies": {
"@esinx/eslint-config": "^2.0.1",
"@esinx/prettier-config": "^1.0.0-3",
"@eslint/js": "^9.9.0",
"@tanstack/router-devtools": "^1.51.6",
"@tanstack/router-plugin": "^1.51.6",
"@types/node": "^22.5.1",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"@vitejs/plugin-react": "^4.3.1",
"autoprefixer": "^10.4.20",
"eslint": "^9.9.0",
"eslint-plugin-react-hooks": "^5.1.0-rc.0",
"eslint-plugin-react-refresh": "^0.4.9",
"globals": "^15.9.0",
"postcss": "^8.4.41",
"tailwindcss": "^3.4.10",
"typescript": "^5.5.3",
"typescript-eslint": "^8.0.1",
"vite": "^5.4.1"
}
}
6 changes: 6 additions & 0 deletions frontend/alert-2.0/postcss.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}
1 change: 1 addition & 0 deletions frontend/alert-2.0/public/assets/icons/PCA_logo.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions frontend/alert-2.0/public/assets/icons/abell.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions frontend/alert-2.0/public/assets/icons/bang.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions frontend/alert-2.0/public/assets/icons/bell-off.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions frontend/alert-2.0/public/assets/icons/bell.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 6 additions & 0 deletions frontend/alert-2.0/public/assets/icons/blue-trash.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions frontend/alert-2.0/public/assets/icons/check.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions frontend/alert-2.0/public/assets/icons/close.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions frontend/alert-2.0/public/assets/icons/down-arrow.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions frontend/alert-2.0/public/assets/icons/search.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 6 additions & 0 deletions frontend/alert-2.0/public/assets/icons/trash.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions frontend/alert-2.0/public/assets/icons/up-arrow.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions frontend/alert-2.0/public/assets/icons/x.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
65 changes: 65 additions & 0 deletions frontend/alert-2.0/src/components/CourseSection.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { match } from 'ts-pattern'

import { StatusIndicator } from '@/components/StatusIndicator'
import { Badge } from '@/components/ui/badge'
import { Activity, type CourseSection } from '@/core/types'
import { cn } from '@/lib/utils'

interface Props {
section: CourseSection
className?: string
}

const classOfActivity = (activity: Activity) =>
match(activity)
.with(Activity.LECTURE, () =>
cn('bg-blue-100', 'hover:bg-blue-100', 'text-blue-600'),
)
.with(Activity.RECITATION, () =>
cn('bg-yellow-100', 'hover:bg-yellow-100', 'text-yellow-600'),
)
.with(Activity.LAB, () =>
cn('bg-green-100', 'hover:bg-green-100', 'text-green-600'),
)
.otherwise(() => cn('bg-gray-100', 'hover:bg-gray-100', 'text-gray-600'))

export const CourseSectionContent: React.FC<Props> = ({
section,
className,
}) => {
return (
<div
className={cn(
'flex',
'justify-between',
'items-center',
'text-sm',
className,
)}
>
<div className={cn('flex', 'items-center', 'space-x-4')}>
<StatusIndicator status={section.status} />
<div>
<div className={cn('text-slate-600')}>{section.course_title}</div>
<div className={cn('font-bold')}>{section.section_id}</div>
<div className={cn('text-slate-600')}>
{section.instructors.length > 0
? section.instructors.map(inst => inst.name).join(', ')
: 'N/A'}
</div>
</div>
</div>
<div className={cn('flex', 'flex-col', 'space-y-2', 'items-end')}>
<Badge
className={cn(
classOfActivity(section.activity),
'shadow-none',
'hover:none',
)}
>
{section.activity}
</Badge>
</div>
</div>
)
}
67 changes: 67 additions & 0 deletions frontend/alert-2.0/src/components/CourseSectionAutocomplete.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { keepPreviousData } from '@tanstack/react-query'
import { useState } from 'react'
import { useDebounce } from 'use-debounce'

import { AutoComplete, ItemEntry } from '@/components/autocomplete'
import { CourseSectionContent } from '@/components/CourseSection'
import { trpc } from '@/core/trpc'
import { CourseSection } from '@/core/types'
import { cn } from '@/lib/utils'

interface Props {
onCourseSectionSelected?: (courseSection: CourseSection) => void
}

const ItemComponent: React.FC<{ item: ItemEntry<CourseSection> }> = ({
item: { value },
}) => {
return (
<CourseSectionContent
section={value}
className={cn('w-full', 'hover:cursor-pointer')}
/>
)
}

export const CourseSectionAutocomplete: React.FC<Props> = ({
onCourseSectionSelected,
}) => {
const [query, setQuery] = useState('')
const [debouncedQuery] = useDebounce(query, 500)
const {
data: sections,
isLoading,
isPlaceholderData,
} = trpc.course.searchSection.useQuery(
{
query: debouncedQuery,
limit: 20,
},
{
placeholderData: keepPreviousData,
},
)
return (
<div>
<AutoComplete
items={
sections?.map(section => ({
id: section.section_id,
value: section,
})) ?? []
}
ItemComponent={ItemComponent}
searchValue={query}
onSearchValueChange={setQuery}
placeholder="CIS-1200"
onSelect={({ value }) => onCourseSectionSelected?.(value)}
isLoading={isLoading}
emptyMessage={
debouncedQuery.length > 0 && !isPlaceholderData
? 'No courses found'
: 'Start typing to search!'
}
/>
</div>
)
}
Loading

0 comments on commit cec310e

Please sign in to comment.