diff --git a/.gitignore b/.gitignore index 7b191358..deed5cc4 100644 --- a/.gitignore +++ b/.gitignore @@ -23,4 +23,5 @@ pcr-backup* ./Pipfile ./Pipfile.lock ./package.json -./yarn.lock \ No newline at end of file +./yarn.lock +*.pem diff --git a/frontend/alert-2.0/.gitignore b/frontend/alert-2.0/.gitignore new file mode 100644 index 00000000..731dcaf0 --- /dev/null +++ b/frontend/alert-2.0/.gitignore @@ -0,0 +1,31 @@ +# 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? + +*.env + +.terraform +*.tfstate +*.tfstate.backup +>>>>>>> alert-2.0 diff --git a/frontend/alert-2.0/README.md b/frontend/alert-2.0/README.md new file mode 100644 index 00000000..74872fd4 --- /dev/null +++ b/frontend/alert-2.0/README.md @@ -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, + }, +}) +``` diff --git a/frontend/alert-2.0/components.json b/frontend/alert-2.0/components.json new file mode 100644 index 00000000..eaa263eb --- /dev/null +++ b/frontend/alert-2.0/components.json @@ -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" + } +} diff --git a/frontend/alert-2.0/eslint.config.js b/frontend/alert-2.0/eslint.config.js new file mode 100644 index 00000000..0c2f237f --- /dev/null +++ b/frontend/alert-2.0/eslint.config.js @@ -0,0 +1,3 @@ +import config from "@esinx/eslint-config"; + +export default [...config] \ No newline at end of file diff --git a/frontend/alert-2.0/index.html b/frontend/alert-2.0/index.html new file mode 100644 index 00000000..ce6da5a7 --- /dev/null +++ b/frontend/alert-2.0/index.html @@ -0,0 +1,13 @@ + + + + + + + Penn Course Alert 2.0 + + +
+ + + diff --git a/frontend/alert-2.0/package.json b/frontend/alert-2.0/package.json new file mode 100644 index 00000000..dbce9a1a --- /dev/null +++ b/frontend/alert-2.0/package.json @@ -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.608", + "@trpc/react-query": "^11.0.0-rc.608", + "@trpc/server": "^11.0.0-rc.608", + "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" + } +} diff --git a/frontend/alert-2.0/postcss.config.js b/frontend/alert-2.0/postcss.config.js new file mode 100644 index 00000000..2e7af2b7 --- /dev/null +++ b/frontend/alert-2.0/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/frontend/alert-2.0/public/assets/icons/PCA_logo.svg b/frontend/alert-2.0/public/assets/icons/PCA_logo.svg new file mode 100644 index 00000000..9b20fc69 --- /dev/null +++ b/frontend/alert-2.0/public/assets/icons/PCA_logo.svg @@ -0,0 +1 @@ +Asset 3 \ No newline at end of file diff --git a/frontend/alert-2.0/public/assets/icons/abell.svg b/frontend/alert-2.0/public/assets/icons/abell.svg new file mode 100644 index 00000000..6deac43b --- /dev/null +++ b/frontend/alert-2.0/public/assets/icons/abell.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/alert-2.0/public/assets/icons/bang.svg b/frontend/alert-2.0/public/assets/icons/bang.svg new file mode 100644 index 00000000..8bf69d02 --- /dev/null +++ b/frontend/alert-2.0/public/assets/icons/bang.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/alert-2.0/public/assets/icons/bell-off.svg b/frontend/alert-2.0/public/assets/icons/bell-off.svg new file mode 100644 index 00000000..fb4c1d0f --- /dev/null +++ b/frontend/alert-2.0/public/assets/icons/bell-off.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/alert-2.0/public/assets/icons/bell.svg b/frontend/alert-2.0/public/assets/icons/bell.svg new file mode 100644 index 00000000..2db23ce8 --- /dev/null +++ b/frontend/alert-2.0/public/assets/icons/bell.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/alert-2.0/public/assets/icons/blue-trash.svg b/frontend/alert-2.0/public/assets/icons/blue-trash.svg new file mode 100644 index 00000000..c497b12e --- /dev/null +++ b/frontend/alert-2.0/public/assets/icons/blue-trash.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/frontend/alert-2.0/public/assets/icons/check.svg b/frontend/alert-2.0/public/assets/icons/check.svg new file mode 100644 index 00000000..27682f73 --- /dev/null +++ b/frontend/alert-2.0/public/assets/icons/check.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/alert-2.0/public/assets/icons/close.svg b/frontend/alert-2.0/public/assets/icons/close.svg new file mode 100644 index 00000000..2c72363d --- /dev/null +++ b/frontend/alert-2.0/public/assets/icons/close.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/alert-2.0/public/assets/icons/down-arrow.svg b/frontend/alert-2.0/public/assets/icons/down-arrow.svg new file mode 100644 index 00000000..d61661fc --- /dev/null +++ b/frontend/alert-2.0/public/assets/icons/down-arrow.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/alert-2.0/public/assets/icons/search.svg b/frontend/alert-2.0/public/assets/icons/search.svg new file mode 100644 index 00000000..19c752d8 --- /dev/null +++ b/frontend/alert-2.0/public/assets/icons/search.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/alert-2.0/public/assets/icons/trash.svg b/frontend/alert-2.0/public/assets/icons/trash.svg new file mode 100644 index 00000000..7d5d93c6 --- /dev/null +++ b/frontend/alert-2.0/public/assets/icons/trash.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/frontend/alert-2.0/public/assets/icons/up-arrow.svg b/frontend/alert-2.0/public/assets/icons/up-arrow.svg new file mode 100644 index 00000000..25cb484b --- /dev/null +++ b/frontend/alert-2.0/public/assets/icons/up-arrow.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/alert-2.0/public/assets/icons/x.svg b/frontend/alert-2.0/public/assets/icons/x.svg new file mode 100644 index 00000000..aab2a0ec --- /dev/null +++ b/frontend/alert-2.0/public/assets/icons/x.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/frontend/alert-2.0/src/components/CourseSection.tsx b/frontend/alert-2.0/src/components/CourseSection.tsx new file mode 100644 index 00000000..9181fd71 --- /dev/null +++ b/frontend/alert-2.0/src/components/CourseSection.tsx @@ -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 = ({ + section, + className, +}) => { + return ( +
+
+ +
+
{section.course_title}
+
{section.section_id}
+
+ {section.instructors.length > 0 + ? section.instructors.map(inst => inst.name).join(', ') + : 'N/A'} +
+
+
+
+ + {section.activity} + +
+
+ ) +} diff --git a/frontend/alert-2.0/src/components/CourseSectionAutocomplete.tsx b/frontend/alert-2.0/src/components/CourseSectionAutocomplete.tsx new file mode 100644 index 00000000..dbdf6366 --- /dev/null +++ b/frontend/alert-2.0/src/components/CourseSectionAutocomplete.tsx @@ -0,0 +1,68 @@ +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 }> = ({ + item: { value }, +}) => { + return ( + + ) +} + +export const CourseSectionAutocomplete: React.FC = ({ + 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, + enabled: () => debouncedQuery.length > 0, + }, + ) + return ( +
+ ({ + 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!' + } + /> +
+ ) +} diff --git a/frontend/alert-2.0/src/components/PCAHeader.tsx b/frontend/alert-2.0/src/components/PCAHeader.tsx new file mode 100644 index 00000000..6d3fe557 --- /dev/null +++ b/frontend/alert-2.0/src/components/PCAHeader.tsx @@ -0,0 +1,39 @@ +import { Badge } from '@/components/ui/badge' +import { cn } from '@/lib/utils' + +export const PCAHeader: React.FC = () => { + return ( +
+
+ +
+ Penn Course Alert +
+ 2.0 +
+
+ Get alerted when a course opens up +
+
+ ) +} diff --git a/frontend/alert-2.0/src/components/StatusIndicator.tsx b/frontend/alert-2.0/src/components/StatusIndicator.tsx new file mode 100644 index 00000000..51e080aa --- /dev/null +++ b/frontend/alert-2.0/src/components/StatusIndicator.tsx @@ -0,0 +1,35 @@ +import { match } from 'ts-pattern' + +import { Status } from '@/core/types' +import { cn } from '@/lib/utils' + +const classOfStatus = (status: Status) => + match(status) + .with(Status.OPEN, () => cn('bg-green-400')) + .with(Status.CLOSED, () => cn('bg-red-400')) + .otherwise(() => cn('bg-yellow-400')) + +export const StatusIndicator: React.FC<{ status: Status }> = ({ status }) => { + return ( +
+
+
+ ) +} diff --git a/frontend/alert-2.0/src/components/autocomplete.tsx b/frontend/alert-2.0/src/components/autocomplete.tsx new file mode 100644 index 00000000..40c482cc --- /dev/null +++ b/frontend/alert-2.0/src/components/autocomplete.tsx @@ -0,0 +1,126 @@ +import { Command as CommandPrimitive } from 'cmdk' +import React, { useState } from 'react' + +import { + Command, + CommandEmpty, + CommandGroup, + CommandItem, + CommandList, +} from './ui/command' +import { Input } from './ui/input' +import { Popover, PopoverAnchor, PopoverContent } from './ui/popover' +import { Skeleton } from './ui/skeleton' + +export interface ItemEntry { + id: string + value: T +} + +interface Props { + searchValue?: string + onSearchValueChange?: (value: string) => void + onSelect?: (obj: ItemEntry) => void + + items: ItemEntry[] + isLoading?: boolean + + emptyMessage?: string + placeholder?: string + + ItemComponent: React.FC<{ item: ItemEntry }> +} + +export function AutoComplete({ + searchValue, + onSearchValueChange, + onSelect, + + items, + isLoading, + + emptyMessage, + placeholder, + + ItemComponent, +}: Props) { + const [open, setOpen] = useState(false) + + const reset = () => { + onSearchValueChange?.('') + } + + const onInputBlur = (e: React.FocusEvent) => { + if (!e.relatedTarget?.hasAttribute('cmdk-list')) { + reset() + } + } + + const onSelectItem = (entry: ItemEntry) => { + onSelect?.(entry) + setOpen(false) + } + + return ( +
+ + + + setOpen(e.key !== 'Escape')} + onMouseDown={() => setOpen(open => !!searchValue || !open)} + onFocus={() => setOpen(true)} + onBlur={onInputBlur} + > + + + + {!open && + +
+ ) +} diff --git a/frontend/alert-2.0/src/components/ui/avatar.tsx b/frontend/alert-2.0/src/components/ui/avatar.tsx new file mode 100644 index 00000000..c6accfe9 --- /dev/null +++ b/frontend/alert-2.0/src/components/ui/avatar.tsx @@ -0,0 +1,48 @@ +import * as AvatarPrimitive from "@radix-ui/react-avatar" +import * as React from "react" + +import { cn } from "@/lib/utils" + +const Avatar = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +Avatar.displayName = AvatarPrimitive.Root.displayName + +const AvatarImage = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AvatarImage.displayName = AvatarPrimitive.Image.displayName + +const AvatarFallback = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName + +export { Avatar, AvatarFallback,AvatarImage } diff --git a/frontend/alert-2.0/src/components/ui/badge.tsx b/frontend/alert-2.0/src/components/ui/badge.tsx new file mode 100644 index 00000000..7f0d6cd1 --- /dev/null +++ b/frontend/alert-2.0/src/components/ui/badge.tsx @@ -0,0 +1,36 @@ +import { cva, type VariantProps } from "class-variance-authority" +import * as React from "react" + +import { cn } from "@/lib/utils" + +const badgeVariants = cva( + "inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", + { + variants: { + variant: { + default: + "border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80", + secondary: + "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", + destructive: + "border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80", + outline: "text-foreground", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +export interface BadgeProps + extends React.HTMLAttributes, + VariantProps {} + +function Badge({ className, variant, ...props }: BadgeProps) { + return ( +
+ ) +} + +export { Badge, badgeVariants } diff --git a/frontend/alert-2.0/src/components/ui/button.tsx b/frontend/alert-2.0/src/components/ui/button.tsx new file mode 100644 index 00000000..3c71e602 --- /dev/null +++ b/frontend/alert-2.0/src/components/ui/button.tsx @@ -0,0 +1,57 @@ +import { Slot } from "@radix-ui/react-slot" +import { cva, type VariantProps } from "class-variance-authority" +import * as React from "react" + +import { cn } from "@/lib/utils" + +const buttonVariants = cva( + "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50", + { + variants: { + variant: { + default: + "bg-primary text-primary-foreground shadow hover:bg-primary/90", + destructive: + "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90", + outline: + "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground", + secondary: + "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80", + ghost: "hover:bg-accent hover:text-accent-foreground", + link: "text-primary underline-offset-4 hover:underline", + }, + size: { + default: "h-9 px-4 py-2", + sm: "h-8 rounded-md px-3 text-xs", + lg: "h-10 rounded-md px-8", + icon: "h-9 w-9", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + } +) + +export interface ButtonProps + extends React.ButtonHTMLAttributes, + VariantProps { + asChild?: boolean +} + +const Button = React.forwardRef( + ({ className, variant, size, asChild = false, ...props }, ref) => { + const Comp = asChild ? Slot : "button" + return ( + + ) + } +) +Button.displayName = "Button" + +export { Button, buttonVariants } diff --git a/frontend/alert-2.0/src/components/ui/card.tsx b/frontend/alert-2.0/src/components/ui/card.tsx new file mode 100644 index 00000000..5e3d620d --- /dev/null +++ b/frontend/alert-2.0/src/components/ui/card.tsx @@ -0,0 +1,76 @@ +import * as React from 'react' + +import { cn } from '@/lib/utils' + +const Card = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +Card.displayName = 'Card' + +const CardHeader = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardHeader.displayName = 'CardHeader' + +const CardTitle = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +

+)) +CardTitle.displayName = 'CardTitle' + +const CardDescription = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +

+)) +CardDescription.displayName = 'CardDescription' + +const CardContent = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +

+)) +CardContent.displayName = 'CardContent' + +const CardFooter = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardFooter.displayName = 'CardFooter' + +export { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } diff --git a/frontend/alert-2.0/src/components/ui/command.tsx b/frontend/alert-2.0/src/components/ui/command.tsx new file mode 100644 index 00000000..d8798102 --- /dev/null +++ b/frontend/alert-2.0/src/components/ui/command.tsx @@ -0,0 +1,153 @@ +import { type DialogProps } from '@radix-ui/react-dialog' +import { MagnifyingGlassIcon } from '@radix-ui/react-icons' +import { Command as CommandPrimitive } from 'cmdk' +import * as React from 'react' + +import { Dialog, DialogContent } from '@/components/ui/dialog' +import { cn } from '@/lib/utils' + +const Command = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +Command.displayName = CommandPrimitive.displayName + +type CommandDialogProps = DialogProps + +const CommandDialog = ({ children, ...props }: CommandDialogProps) => { + return ( + + + + {children} + + + + ) +} + +const CommandInput = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( +
+ + +
+)) + +CommandInput.displayName = CommandPrimitive.Input.displayName + +const CommandList = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) + +CommandList.displayName = CommandPrimitive.List.displayName + +const CommandEmpty = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>((props, ref) => ( + +)) + +CommandEmpty.displayName = CommandPrimitive.Empty.displayName + +const CommandGroup = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) + +CommandGroup.displayName = CommandPrimitive.Group.displayName + +const CommandSeparator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +CommandSeparator.displayName = CommandPrimitive.Separator.displayName + +const CommandItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) + +CommandItem.displayName = CommandPrimitive.Item.displayName + +const CommandShortcut = ({ + className, + ...props +}: React.HTMLAttributes) => { + return ( + + ) +} +CommandShortcut.displayName = 'CommandShortcut' + +export { + Command, + CommandDialog, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, + CommandSeparator, + CommandShortcut, +} diff --git a/frontend/alert-2.0/src/components/ui/dialog.tsx b/frontend/alert-2.0/src/components/ui/dialog.tsx new file mode 100644 index 00000000..1b5decb7 --- /dev/null +++ b/frontend/alert-2.0/src/components/ui/dialog.tsx @@ -0,0 +1,120 @@ +import * as DialogPrimitive from "@radix-ui/react-dialog" +import { Cross2Icon } from "@radix-ui/react-icons" +import * as React from "react" + +import { cn } from "@/lib/utils" + +const Dialog = DialogPrimitive.Root + +const DialogTrigger = DialogPrimitive.Trigger + +const DialogPortal = DialogPrimitive.Portal + +const DialogClose = DialogPrimitive.Close + +const DialogOverlay = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DialogOverlay.displayName = DialogPrimitive.Overlay.displayName + +const DialogContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + {children} + + + Close + + + +)) +DialogContent.displayName = DialogPrimitive.Content.displayName + +const DialogHeader = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +DialogHeader.displayName = "DialogHeader" + +const DialogFooter = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +DialogFooter.displayName = "DialogFooter" + +const DialogTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DialogTitle.displayName = DialogPrimitive.Title.displayName + +const DialogDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DialogDescription.displayName = DialogPrimitive.Description.displayName + +export { + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogOverlay, + DialogPortal, + DialogTitle, + DialogTrigger, +} diff --git a/frontend/alert-2.0/src/components/ui/form.tsx b/frontend/alert-2.0/src/components/ui/form.tsx new file mode 100644 index 00000000..636c7032 --- /dev/null +++ b/frontend/alert-2.0/src/components/ui/form.tsx @@ -0,0 +1,176 @@ +import * as LabelPrimitive from "@radix-ui/react-label" +import { Slot } from "@radix-ui/react-slot" +import * as React from "react" +import { + Controller, + ControllerProps, + FieldPath, + FieldValues, + FormProvider, + useFormContext, +} from "react-hook-form" + +import { Label } from "@/components/ui/label" +import { cn } from "@/lib/utils" + +const Form = FormProvider + +interface FormFieldContextValue< + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath = FieldPath +> { + name: TName +} + +const FormFieldContext = React.createContext( + {} as FormFieldContextValue +) + +const FormField = < + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath = FieldPath +>({ + ...props +}: ControllerProps) => { + return ( + + + + ) +} + +const useFormField = () => { + const fieldContext = React.useContext(FormFieldContext) + const itemContext = React.useContext(FormItemContext) + const { getFieldState, formState } = useFormContext() + + const fieldState = getFieldState(fieldContext.name, formState) + + if (!fieldContext) { + throw new Error("useFormField should be used within ") + } + + const { id } = itemContext + + return { + id, + name: fieldContext.name, + formItemId: `${id}-form-item`, + formDescriptionId: `${id}-form-item-description`, + formMessageId: `${id}-form-item-message`, + ...fieldState, + } +} + +interface FormItemContextValue { + id: string +} + +const FormItemContext = React.createContext( + {} as FormItemContextValue +) + +const FormItem = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => { + const id = React.useId() + + return ( + +
+ + ) +}) +FormItem.displayName = "FormItem" + +const FormLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => { + const { error, formItemId } = useFormField() + + return ( +