Skip to content

Commit

Permalink
feat: add favorite groupings to Trending Content page per ticket 550
Browse files Browse the repository at this point in the history
  • Loading branch information
carddev81 committed Dec 26, 2024
1 parent e3342d5 commit 3b07eed
Show file tree
Hide file tree
Showing 4 changed files with 219 additions and 12 deletions.
11 changes: 10 additions & 1 deletion backend/src/handlers/open_content_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -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},
}
}

Expand All @@ -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)
}
110 changes: 110 additions & 0 deletions backend/src/handlers/open_content_handler_test.go
Original file line number Diff line number Diff line change
@@ -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")
}
}
})
}
}
95 changes: 95 additions & 0 deletions frontend/src/Components/OpenContentItemAccordion.tsx
Original file line number Diff line number Diff line change
@@ -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<string, OpenContentItem[]>;

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<string | null>(null);
const toggleAccordion = (key: string) => {
setActiveKey(activeKey === key ? null : key);
};
const displayIconByTitle = (title: string) => {
let icon = null;
switch (title) {
case 'Videos':
icon = <VideoCameraIcon className="w-4" />;
break;
case 'Libraries':
icon = <BookOpenIcon className="w-4" />;
break;
case 'Helpful Links':
icon = <InformationCircleIcon className="w-4" />;
break;
default:
break;
}
return icon;
};
return (
<div className="w-full max-w-md mx-auto">
{Object.entries(contentMap).map(([title, contentItems]) => (
<div key={title} className="border-b">
<button
className="flex items-center w-full px-4 py-3 text-left"
onClick={() => toggleAccordion(title)}
>
<ChevronRightIcon
className={`w-4 mr-3 transform transition-transform ${
activeKey === title ? 'rotate-90' : ''
}`}
/>
{displayIconByTitle(title)}
<span className="flex-1 text-lg font-medium ml-2">
{title}
</span>
</button>
<div
className={`overflow-hidden transition-[max-height] duration-700 ${
activeKey === title ? 'max-h-[800px]' : 'max-h-0'
}`}
>
{contentItems.map((item) => (
<OpenContentCard
key={item.content_id}
content={item}
/>
))}
</div>
</div>
))}
</div>
);
}
15 changes: 4 additions & 11 deletions frontend/src/Pages/StudentLayer1.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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();
Expand All @@ -31,7 +31,7 @@ export default function StudentLayer1() {
const { data: favorites, mutate: mutateFavLibs } = useSWR<
ServerResponseMany<OpenContentItem>,
AxiosError
>('api/open-content/favorites');
>('api/open-content/favorite-groupings');
const { data: helpfulLinks, mutate: mutateHelpfulFavs } = useSWR<
ServerResponseOne<HelpfulLinkAndSort>,
AxiosError
Expand Down Expand Up @@ -109,15 +109,8 @@ export default function StudentLayer1() {
<div className="min-w-[300px] border-l border-grey-1 flex flex-col gap-6 px-6 py-4">
<h2>Favorites</h2>
<div className="space-y-3 w-full">
{favorites ? (
favorites.data.map((favorite: OpenContentItem) => {
return (
<OpenContentCard
key={favorite.content_id}
content={favorite}
/>
);
})
{favorites?.data && favorites.data.length > 0 ? (
<OpenContentItemAccordion items={favorites.data} />
) : (
<div>No Favorites</div>
)}
Expand Down

0 comments on commit 3b07eed

Please sign in to comment.