From 3b07eed4aa8f06e0bb0d214b74c4bee3235028f3 Mon Sep 17 00:00:00 2001 From: carddev81 Date: Thu, 26 Dec 2024 13:18:32 -0600 Subject: [PATCH] feat: add favorite groupings to Trending Content page per ticket 550 --- backend/src/handlers/open_content_handler.go | 11 +- .../src/handlers/open_content_handler_test.go | 110 ++++++++++++++++++ .../Components/OpenContentItemAccordion.tsx | 95 +++++++++++++++ frontend/src/Pages/StudentLayer1.tsx | 15 +-- 4 files changed, 219 insertions(+), 12 deletions(-) create mode 100644 backend/src/handlers/open_content_handler_test.go create mode 100644 frontend/src/Components/OpenContentItemAccordion.tsx diff --git a/backend/src/handlers/open_content_handler.go b/backend/src/handlers/open_content_handler.go index b8744b94..afa66858 100644 --- a/backend/src/handlers/open_content_handler.go +++ b/backend/src/handlers/open_content_handler.go @@ -9,8 +9,9 @@ import ( func (srv *Server) registerOpenContentRoutes() []routeDef { axx := models.Feature(models.OpenContentAccess) return []routeDef{ - {"GET /api/open-content/favorites", srv.handleGetUserFavoriteOpenContent, false, axx}, {"GET /api/open-content", srv.handleIndexOpenContent, false, axx}, + {"GET /api/open-content/favorites", srv.handleGetUserFavoriteOpenContent, false, axx}, + {"GET /api/open-content/favorite-groupings", srv.handleGetUserFavoriteOpenContentGroupings, false, axx}, } } @@ -36,3 +37,11 @@ func (srv *Server) handleGetUserFavoriteOpenContent(w http.ResponseWriter, r *ht meta := models.NewPaginationInfo(page, perPage, total) return writePaginatedResponse(w, http.StatusOK, favorites, meta) } + +func (srv *Server) handleGetUserFavoriteOpenContentGroupings(w http.ResponseWriter, r *http.Request, log sLog) error { + favorites, err := srv.Db.GetUserFavoriteGroupings(srv.getUserID(r)) + if err != nil { + return newDatabaseServiceError(err) + } + return writeJsonResponse(w, http.StatusOK, favorites) +} diff --git a/backend/src/handlers/open_content_handler_test.go b/backend/src/handlers/open_content_handler_test.go new file mode 100644 index 00000000..aca1677f --- /dev/null +++ b/backend/src/handlers/open_content_handler_test.go @@ -0,0 +1,110 @@ +package handlers + +import ( + "UnlockEdv2/src/models" + "encoding/json" + "net/http" + "slices" + "testing" +) + +func TestHandleIndexOpenContent(t *testing.T) { + httpTests := []httpTest{ + {"TestIndexOpenContentAsAdmin", "admin", nil, http.StatusOK, ""}, + {"TestIndexOpenContentAsUser", "student", nil, http.StatusOK, ""}, + } + for _, test := range httpTests { + t.Run(test.testName, func(t *testing.T) { + req, err := http.NewRequest(http.MethodGet, "/api/open-content", nil) + if err != nil { + t.Fatalf("unable to create new request, error is %v", err) + } + handler := getHandlerByRole(server.handleIndexOpenContent, test.role) + rr := executeRequest(t, req, handler, test) + var all bool + if test.role == "admin" { + all = true + } + openContentProviders, err := server.Db.GetOpenContent(all) + if err != nil { + t.Fatalf("unable to get open content from db, error is %v", err) + } + data := models.Resource[[]models.OpenContentProvider]{} + received := rr.Body.String() + if err = json.Unmarshal([]byte(received), &data); err != nil { + t.Errorf("failed to unmarshal resource, error is %v", err) + } + for _, provider := range openContentProviders { + if !slices.ContainsFunc(data.Data, func(ocProvider models.OpenContentProvider) bool { + return ocProvider.ID == provider.ID + }) { + t.Error("providers not found, out of sync") + } + } + }) + } +} + +func TestHandleGetUserFavoriteOpenContentGroupings(t *testing.T) { + httpTests := []httpTest{ + {"TestGetUserFavoriteOpenContentGroupingsAsUser", "student", map[string]any{"user_id": uint(4)}, http.StatusOK, ""}, + } + for _, test := range httpTests { + t.Run(test.testName, func(t *testing.T) { + req, err := http.NewRequest(http.MethodGet, "/api/open-content/favorite-groupings", nil) + if err != nil { + t.Fatalf("unable to create new request, error is %v", err) + } + handler := getHandlerByRole(server.handleGetUserFavoriteOpenContentGroupings, test.role) + rr := executeRequest(t, req, handler, test) + contentItems, err := server.Db.GetUserFavoriteGroupings(test.mapKeyValues["user_id"].(uint)) + if err != nil { + t.Fatalf("unable to get open content items from db, error is %v", err) + } + data := models.Resource[[]models.OpenContentItem]{} + received := rr.Body.String() + if err = json.Unmarshal([]byte(received), &data); err != nil { + t.Errorf("failed to unmarshal resource, error is %v", err) + } + for _, contentItem := range contentItems { + if !slices.ContainsFunc(data.Data, func(item models.OpenContentItem) bool { + return item.ContentId == contentItem.ContentId + }) { + t.Error("open content favorites not found, out of sync") + } + } + }) + } +} + +func TestHandleGetUserFavoriteOpenContent(t *testing.T) { + httpTests := []httpTest{ + {"TestGetUserFavoriteOpenContentUser", "student", map[string]any{"user_id": uint(4), "page": 1, "per_page": 10}, http.StatusOK, ""}, + } + for _, test := range httpTests { + t.Run(test.testName, func(t *testing.T) { + req, err := http.NewRequest(http.MethodGet, "/api/open-content/favorites", nil) + if err != nil { + t.Fatalf("unable to create new request, error is %v", err) + } + handler := getHandlerByRole(server.handleGetUserFavoriteOpenContent, test.role) + rr := executeRequest(t, req, handler, test) + _, favorites, err := server.Db.GetUserFavorites(test.mapKeyValues["user_id"].(uint), test.mapKeyValues["page"].(int), test.mapKeyValues["per_page"].(int)) + if err != nil { + t.Fatalf("unable to get user favorites from db, error is %v", err) + } + data := models.PaginatedResource[models.OpenContentItem]{} + received := rr.Body.String() + if err = json.Unmarshal([]byte(received), &data); err != nil { + t.Errorf("failed to unmarshal resource, error is %v", err) + } + for _, favorite := range favorites { + if !slices.ContainsFunc(data.Data, func(item models.OpenContentItem) bool { + return favorite.ContentId == item.ContentId + }) { + t.Error("favorites not found, out of sync") + } + } + }) + } +} diff --git a/frontend/src/Components/OpenContentItemAccordion.tsx b/frontend/src/Components/OpenContentItemAccordion.tsx new file mode 100644 index 00000000..f2db329d --- /dev/null +++ b/frontend/src/Components/OpenContentItemAccordion.tsx @@ -0,0 +1,95 @@ +import { OpenContentItem } from '@/common'; +import { + ChevronRightIcon, + VideoCameraIcon, + BookOpenIcon, + InformationCircleIcon +} from '@heroicons/react/24/solid'; +import React, { useState } from 'react'; +import OpenContentCard from './cards/OpenContentCard'; + +type OpenContentItemMap = Record; + +export default function OpenContentItemAccordion({ + items +}: { + items: OpenContentItem[]; +}) { + const contentMap: OpenContentItemMap = {}; + items.forEach((item) => { + let sectionKey; + switch (item.content_type) { + case 'video': + sectionKey = 'Videos'; + break; + case 'library': + sectionKey = 'Libraries'; + break; + case 'helpful_link': + sectionKey = 'Helpful Links'; + break; + default: + sectionKey = 'All Others'; + break; + } + if (!contentMap[sectionKey]) { + contentMap[sectionKey] = []; + } + contentMap[sectionKey].push(item); + }); + const [activeKey, setActiveKey] = useState(null); + const toggleAccordion = (key: string) => { + setActiveKey(activeKey === key ? null : key); + }; + const displayIconByTitle = (title: string) => { + let icon = null; + switch (title) { + case 'Videos': + icon = ; + break; + case 'Libraries': + icon = ; + break; + case 'Helpful Links': + icon = ; + break; + default: + break; + } + return icon; + }; + return ( +
+ {Object.entries(contentMap).map(([title, contentItems]) => ( +
+ +
+ {contentItems.map((item) => ( + + ))} +
+
+ ))} +
+ ); +} diff --git a/frontend/src/Pages/StudentLayer1.tsx b/frontend/src/Pages/StudentLayer1.tsx index eea2e07d..5dd71917 100644 --- a/frontend/src/Pages/StudentLayer1.tsx +++ b/frontend/src/Pages/StudentLayer1.tsx @@ -7,7 +7,6 @@ import { HelpfulLinkAndSort, ServerResponseMany } from '@/common'; -import OpenContentCard from '@/Components/cards/OpenContentCard'; import HelpfulLinkCard from '@/Components/cards/HelpfulLinkCard'; import { useAuth } from '@/useAuth'; import { useLoaderData, useNavigate } from 'react-router-dom'; @@ -16,6 +15,7 @@ import { FeaturedContent } from '@/Components/dashboard'; import TopContentList from '@/Components/dashboard/TopContentList'; import useSWR from 'swr'; import { AxiosError } from 'axios'; +import OpenContentItemAccordion from '@/Components/OpenContentItemAccordion'; export default function StudentLayer1() { const { user } = useAuth(); @@ -31,7 +31,7 @@ export default function StudentLayer1() { const { data: favorites, mutate: mutateFavLibs } = useSWR< ServerResponseMany, AxiosError - >('api/open-content/favorites'); + >('api/open-content/favorite-groupings'); const { data: helpfulLinks, mutate: mutateHelpfulFavs } = useSWR< ServerResponseOne, AxiosError @@ -109,15 +109,8 @@ export default function StudentLayer1() {

Favorites

- {favorites ? ( - favorites.data.map((favorite: OpenContentItem) => { - return ( - - ); - }) + {favorites?.data && favorites.data.length > 0 ? ( + ) : (
No Favorites
)}