From de2ff1bba6d19e8073e1f0dc6152f250e0ac9068 Mon Sep 17 00:00:00 2001 From: Clyde <163448150+CK-7vn@users.noreply.github.com> Date: Thu, 31 Oct 2024 12:17:20 -0400 Subject: [PATCH] feat: add Programs page to UI and seed program tags (#471) --- backend/seeder/main.go | 11 ++ backend/src/handlers/programs_handler.go | 2 +- frontend/src/Components/Navbar.tsx | 233 +++++++++-------------- frontend/src/Components/ProgramCard.tsx | 81 ++++++++ frontend/src/Pages/Programs.tsx | 101 ++++++++++ frontend/src/app.tsx | 35 +++- frontend/src/common.ts | 15 ++ 7 files changed, 333 insertions(+), 145 deletions(-) create mode 100644 frontend/src/Components/ProgramCard.tsx create mode 100644 frontend/src/Pages/Programs.tsx diff --git a/backend/seeder/main.go b/backend/seeder/main.go index 4e58bb28b..95ccd9ecc 100644 --- a/backend/seeder/main.go +++ b/backend/seeder/main.go @@ -286,6 +286,7 @@ func seedTestData(db *gorm.DB) { func createFacilityPrograms(db *gorm.DB) ([]models.ProgramSection, error) { facilities := []models.Facility{} randNames := []string{"Anger Management", "Substance Abuse Treatment", "AA/NA", "Thinking for a Change", "A New Freedom", "Dog Training", "A New Path", "GED/Hi-SET", "Parenting", "Employment", "Life Skills", "Health and Wellness", "Financial Literacy", "Computer Skills", "Parenting", "Employment", "Life Skills"} + randTags := []string{"Rehabilitation", "Life-Skills", "12-step", "Required", "Recovery", "DV-Requirement", "10-Weeks", "Addiction"} if err := db.Find(&facilities).Error; err != nil { return nil, err } @@ -313,6 +314,16 @@ func createFacilityPrograms(db *gorm.DB) ([]models.ProgramSection, error) { if err := db.Create(&prog[i]).Error; err != nil { log.Fatalf("Failed to create program: %v", err) } + numTags := rand.Intn(3) + 1 + for j := 0; j < numTags; j++ { + tag := models.ProgramTag{ + ProgramID: prog[i].ID, + Value: randTags[rand.Intn(len(randTags))], + } + if err := db.Create(&tag).Error; err != nil { + log.Fatalf("Failed to create tag: %v", err) + } + } section := models.ProgramSection{ FacilityID: facilities[idx].ID, ProgramID: prog[i].ID, diff --git a/backend/src/handlers/programs_handler.go b/backend/src/handlers/programs_handler.go index df74d863e..a5672beef 100644 --- a/backend/src/handlers/programs_handler.go +++ b/backend/src/handlers/programs_handler.go @@ -9,7 +9,7 @@ import ( ) func (srv *Server) registerProgramsRoutes() { - srv.Mux.Handle("GET /api/programs", srv.applyAdminMiddleware(srv.handleIndexPrograms)) + srv.Mux.Handle("GET /api/programs", srv.applyMiddleware(srv.handleIndexPrograms)) srv.Mux.Handle("GET /api/programs/{id}", srv.applyMiddleware(srv.handleShowProgram)) srv.Mux.Handle("POST /api/programs", srv.applyAdminMiddleware(srv.handleCreateProgram)) srv.Mux.Handle("DELETE /api/programs/{id}", srv.applyAdminMiddleware(srv.handleDeleteProgram)) diff --git a/frontend/src/Components/Navbar.tsx b/frontend/src/Components/Navbar.tsx index 5f6ba9709..e1f573504 100644 --- a/frontend/src/Components/Navbar.tsx +++ b/frontend/src/Components/Navbar.tsx @@ -11,17 +11,11 @@ import { RectangleStackIcon, TrophyIcon, UsersIcon, - ArrowRightEndOnRectangleIcon, - SunIcon, - MoonIcon, - UserCircleIcon, - BuildingOffice2Icon + DocumentTextIcon } from '@heroicons/react/24/solid'; -import { useAuth, handleLogout } from '@/useAuth'; +import { useAuth } from '@/useAuth'; import ULIComponent from './ULIComponent'; import { Link } from 'react-router-dom'; -import ThemeToggle from './ThemeToggle'; - export default function Navbar({ isPinned, onTogglePin @@ -60,138 +54,97 @@ export default function Navbar({ - -
- -
+ ); } diff --git a/frontend/src/Components/ProgramCard.tsx b/frontend/src/Components/ProgramCard.tsx new file mode 100644 index 000000000..c8580e003 --- /dev/null +++ b/frontend/src/Components/ProgramCard.tsx @@ -0,0 +1,81 @@ +import { BookmarkIcon } from '@heroicons/react/24/solid'; +import { BookmarkIcon as BookmarkIconOutline } from '@heroicons/react/24/outline'; +import LightGreenPill from './pill-labels/LightGreenPill'; +import { MouseEvent } from 'react'; +import { Program, ViewType } from '@/common'; +import API from '@/api/api'; + +export default function ProgramCard({ + program, + callMutate, + view +}: { + program: Program; + callMutate: () => void; + view?: ViewType; +}) { + function updateFavorite(e: MouseEvent) { + e.preventDefault(); + API.put(`programs/${program.id}/save`, {}) + .then(() => { + callMutate(); + }) + .catch((error) => { + console.log(error); + }); + } + + const bookmark: JSX.Element = program.is_favorited ? ( + + ) : ( + + ); + + const tagPills = program.tags.map((tag) => ( + {tag.value.toString()} + )); + + if (view === ViewType.List) { + return ( + e.preventDefault()} + > +
+
+
updateFavorite(e)}>{bookmark}
+

{program.name}

+

|

+
{tagPills}
+
+

+ {program.description} +

+
+
+ ); + } else { + return ( +
+
updateFavorite(e)} + > + {bookmark} +
+
+

{program.name}

+

+ {program.description} +

+
+ {tagPills} +
+
+
+ ); + } +} diff --git a/frontend/src/Pages/Programs.tsx b/frontend/src/Pages/Programs.tsx new file mode 100644 index 000000000..c49f884bf --- /dev/null +++ b/frontend/src/Pages/Programs.tsx @@ -0,0 +1,101 @@ +import { useAuth } from '@/useAuth'; +import { useState, useRef } from 'react'; +import ProgramCard from '@/Components/ProgramCard'; +import SearchBar from '@/Components/inputs/SearchBar'; +import { Program, ServerResponse, ViewType, UserRole } from '@/common'; +import useSWR from 'swr'; +import DropdownControl from '@/Components/inputs/DropdownControl'; +import { AxiosError } from 'axios'; +import { PlusCircleIcon } from '@heroicons/react/24/outline'; +import ToggleView from '@/Components/ToggleView'; + +export default function Programs() { + const { user } = useAuth(); + const addProgramModal = useRef(null); + + if (!user) { + return null; + } + + const [activeView, setActiveView] = useState(ViewType.Grid); + const [searchTerm, setSearchTerm] = useState(''); + const [order, setOrder] = useState('asc'); + const { data, error, mutate } = useSWR, AxiosError>( + `/api/programs?search=${searchTerm}&order=${order}` + ); + const programData = data?.data as Program[]; + + function handleSearch(newSearch: string) { + setSearchTerm(newSearch); + } + + return ( +
+
+
+ + +
+
+ + {user.role === UserRole.Admin && ( + + )} +
+
+
+ {error ? ( +

Error loading programs.

+ ) : programData?.length === 0 ? ( +

No programs to display.

+ ) : ( + programData?.map((program: Program) => { + return ( + void mutate()} + view={activeView} + key={program.id} + /> + ); + }) + )} +
+ + +
+

Add New Program

+

+ This feature will be implemented soon. +

+
+ +
+
+
+
+ ); +} diff --git a/frontend/src/app.tsx b/frontend/src/app.tsx index 8c0909805..bbe5e6a9e 100644 --- a/frontend/src/app.tsx +++ b/frontend/src/app.tsx @@ -20,6 +20,7 @@ import StudentManagement from '@/Pages/StudentManagement.tsx'; import OpenContentManagement from './Pages/OpenContentManagement'; import OpenContent from './Pages/OpenContent'; import LibraryViewer from './Pages/LibraryViewer'; +import Programs from './Pages/Programs.tsx'; import { checkDefaultFacility, checkExistingFlow, @@ -147,6 +148,15 @@ const router = createBrowserRouter([ title: 'Library Viewer', path: ['viewer', 'libraries', ':library_name'] } + }, + { + path: 'programs', + element: , + errorElement: , + handle: { + title: 'Programs', + path: ['programs'] + } } ] }, @@ -232,10 +242,6 @@ const router = createBrowserRouter([ path: ['open-content-management'] } }, - { - path: '*', - element: - }, { path: 'facilities-management', element: , @@ -244,6 +250,27 @@ const router = createBrowserRouter([ path: ['facilities-management'] }, errorElement: + }, + { + path: 'course-catalog-admin', + element: , + handle: { + title: 'Course Catalog', + path: ['course-catalog'] + } + }, + { + path: 'programs', + element: , + errorElement: , + handle: { + title: 'Programs', + path: ['programs'] + } + }, + { + path: '*', + element: } ] } diff --git a/frontend/src/common.ts b/frontend/src/common.ts index e9dc96eba..3b4d1b61e 100644 --- a/frontend/src/common.ts +++ b/frontend/src/common.ts @@ -631,6 +631,21 @@ export interface Library { open_content_provider: OpenContentProvider; } +export interface Program { + id: number; + created_at: Date; + updated_at: Date; + name: string; + description: string; + tags: ProgramTag[]; + is_favorited: boolean; +} + +export interface ProgramTag { + id: string; + value: number; +} + export interface OpenContentProvider { id: number; name: string;