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({
-
-
+
+ {user && user.role == UserRole.Admin ? (
+ <>
+ {/* admin view */}
+ -
+
+ Dashboard
+
+
+ -
+
+
+ Students
+
+
+ -
+
+
+ Admins
+
+
+ -
+
+
+ Open Content
+
+
+ -
+
+
+ Resources
+
+
+ -
+
+
+ Platforms
+
+
+ -
+
+
+ Course Catalog
+
+
+ -
+
+
+ Programs
+
+
+ >
+ ) : (
+ <>
+ {/* student view */}
+ -
+
+ Dashboard
+
+
+ -
+
+ My Courses
+
+
+ -
+
+ My Progress
+
+
+ -
+
+
+ Open Content
+
+
+ -
+
+
+ Course Catalog
+
+
+ -
+
+
+ Programs
+
+
+ >
+ )}
+
);
}
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}
+ />
+ );
+ })
+ )}
+
+
+
+
+ );
+}
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;