From 54b3c38bb227636c6062c6c721811be8508b54c7 Mon Sep 17 00:00:00 2001 From: Wang Haoyang Date: Wed, 25 Sep 2024 23:45:15 +0800 Subject: [PATCH 1/7] Fix axios serialization of arrays -axios serializes arrays differently from how fastapi parses arrays in a URL param -Implement a custom serializer on the frontend for the category_ids param used for the /events backend -This fixes the home page top events component: now it displays user-specific categories instead of top events from any category --- frontend/client/core/utils.ts | 4 ++++ frontend/client/services.gen.ts | 2 ++ 2 files changed, 6 insertions(+) diff --git a/frontend/client/core/utils.ts b/frontend/client/core/utils.ts index f7103fc4..7d09decf 100644 --- a/frontend/client/core/utils.ts +++ b/frontend/client/core/utils.ts @@ -331,6 +331,10 @@ export const formDataBodySerializer = { }, }; +export const categoryIdsSerializer = (params: Record) => { + return params.category_ids?.map((categoryId: number) => `category_ids=${categoryId}`).join("&"); +}; + export const jsonBodySerializer = { bodySerializer: (body: T) => JSON.stringify(body), }; diff --git a/frontend/client/services.gen.ts b/frontend/client/services.gen.ts index 6e744ce4..e5a604e0 100644 --- a/frontend/client/services.gen.ts +++ b/frontend/client/services.gen.ts @@ -6,6 +6,7 @@ import { type Options, urlSearchParamsBodySerializer, } from "./client"; +import { categoryIdsSerializer } from "./core/utils"; import type { AskGpQuestionUserQuestionsAskGpQuestionGetData, AskGpQuestionUserQuestionsAskGpQuestionGetError, @@ -286,6 +287,7 @@ export const getEventsEventsGet = ( ThrowOnError >({ ...options, + paramsSerializer: categoryIdsSerializer, url: "/events/", }); }; From ba8373cb67a886bbb2106c1c36a7ef9a3f530eed Mon Sep 17 00:00:00 2001 From: Wang Haoyang Date: Thu, 26 Sep 2024 00:02:47 +0800 Subject: [PATCH 2/7] Update frontend category names -Rename 'Science & Tech' to 'Science & Technology' in the frontend Category enum to match the backend naming -This fixes the display of the 'Science & Tech' category on the home page --- frontend/types/categories.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/frontend/types/categories.ts b/frontend/types/categories.ts index bc0e5679..e455628e 100644 --- a/frontend/types/categories.ts +++ b/frontend/types/categories.ts @@ -31,7 +31,7 @@ export enum Category { export const getCategoryFor = (categoryName: string) => { const mappings: Record = { - "science & tech": Category.SciTech, + "science & technology": Category.SciTech, "arts & humanities": Category.ArtsHumanities, politics: Category.Politics, media: Category.Media, @@ -79,3 +79,8 @@ export const categoriesToIconsMap: Record = { [Category.Education]: School, [Category.Others]: CircleHelp, }; + +export const getIconFor = (categoryName: string) => { + const category = getCategoryFor(categoryName); + return categoriesToIconsMap[category]; +}; From cd0145dc1f3d0f88d3ab6b6827f58de37e03601b Mon Sep 17 00:00:00 2001 From: Wang Haoyang Date: Thu, 26 Sep 2024 00:04:15 +0800 Subject: [PATCH 3/7] Add getEventsForCategory() query to frontend -New getEventsForCategory() query to request all events for a given list of category ids from backend --- frontend/queries/event.ts | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/frontend/queries/event.ts b/frontend/queries/event.ts index d8446897..ecd6266e 100644 --- a/frontend/queries/event.ts +++ b/frontend/queries/event.ts @@ -2,7 +2,7 @@ import { queryOptions } from "@tanstack/react-query"; import { getEventEventsIdGet, - getUserAuthSessionGet, + getEventsEventsGet, } from "@/client/services.gen"; import { QueryKeys } from "./utils/query-keys"; @@ -16,3 +16,16 @@ export const getEvent = (id: number) => query: { id }, }).then((data) => data.data), }); + +export const getEventsForCategory = (categoryId: number) => + queryOptions({ + queryKey: [QueryKeys.Categories, categoryId], + queryFn: () => + getEventsEventsGet({ + withCredentials: true, + query: { + category_ids: [ categoryId ] + }, + }).then((data) => data.data), + }); + \ No newline at end of file From 2a5364aee42dc9e1d42b4f572b33636668be9e38 Mon Sep 17 00:00:00 2001 From: Wang Haoyang Date: Thu, 26 Sep 2024 01:09:20 +0800 Subject: [PATCH 4/7] Render NewsArticle with client-side rendering NewsArticle relies on react hooks like useEffect(), so it needs to be rendered on the client side --- frontend/components/news/news-article.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/frontend/components/news/news-article.tsx b/frontend/components/news/news-article.tsx index 0210e774..7ac00e34 100644 --- a/frontend/components/news/news-article.tsx +++ b/frontend/components/news/news-article.tsx @@ -1,3 +1,5 @@ +"use client"; + import { useEffect, useState } from "react"; import Image from "next/image"; import { useRouter } from "next/navigation"; From f19267e7396607ddf1c4e975dafdd3f578e8d4aa Mon Sep 17 00:00:00 2001 From: Wang Haoyang Date: Thu, 26 Sep 2024 01:11:01 +0800 Subject: [PATCH 5/7] Add Category-specific page -Basic page that displays all events in the backend database for a given Category ID --- .../(authenticated)/categories/[id]/page.tsx | 73 +++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 frontend/app/(authenticated)/categories/[id]/page.tsx diff --git a/frontend/app/(authenticated)/categories/[id]/page.tsx b/frontend/app/(authenticated)/categories/[id]/page.tsx new file mode 100644 index 00000000..9ea0fa8c --- /dev/null +++ b/frontend/app/(authenticated)/categories/[id]/page.tsx @@ -0,0 +1,73 @@ +"use client"; + +import { CategoryDTO, MiniEventDTO } from "@/client"; +import ArticleLoading from "@/components/news/article-loading"; +import NewsArticle from "@/components/news/news-article"; +import { getCategories } from "@/queries/category"; +import { getEventsForCategory } from "@/queries/event"; +import { useQuery } from "@tanstack/react-query"; +import { useEffect, useState } from "react"; + +const Page = ({ params }: { params: { id: string } }) => { + const categoryId = parseInt(params.id); + const [ categoryName, setCategoryName ] = useState(""); + const { data: events, isSuccess: isEventsLoaded } = useQuery(getEventsForCategory(categoryId)); + const { data: categories, isSuccess: isCategoriesLoaded } = useQuery(getCategories()); + + // Very inefficient, but is there a better way to do this? New StoreProvider for CategoryDTO[]? + useEffect(() => { + if (isCategoriesLoaded && categories!.length > 0) { + categories!.forEach((category: CategoryDTO) => { + if (category.id == categoryId) { + setCategoryName(category.name); + } + }); + } + }, [categories, isCategoriesLoaded]); + + const Articles = () => { + if (!isEventsLoaded) { + return ( + <> + + + + + ) + } + + const eventData = events!.data; + if (eventData.length == 0) { + return ( +
+

No recent events. Try refreshing the page.

+
+ ) + } + + return ( + eventData.map((newsEvent: MiniEventDTO, index: number) => ( + + )) + ) + } + + return ( +
+
+ + {new Date().toDateString()} + +

+ Top events from {categoryName} +

+
+ +
+ +
+
+ ) +}; + +export default Page; From 6bd46bb9b1fac0806dd0411c4d52cd3ca03293b3 Mon Sep 17 00:00:00 2001 From: Wang Haoyang Date: Thu, 26 Sep 2024 01:12:22 +0800 Subject: [PATCH 6/7] Update sidebar to redirect to category page -Sidebar is now dynamically populated with categories fetched from the backend -Clicking a category on the sidebar brings up the Category-specific page powered by the new category-specific Page component --- .../sidebar/sidebar-item-with-icon.tsx | 10 ++++- .../sidebar/sidebar-other-topics.tsx | 40 ++++++++----------- 2 files changed, 24 insertions(+), 26 deletions(-) diff --git a/frontend/components/navigation/sidebar/sidebar-item-with-icon.tsx b/frontend/components/navigation/sidebar/sidebar-item-with-icon.tsx index ab7d614c..114c95fb 100644 --- a/frontend/components/navigation/sidebar/sidebar-item-with-icon.tsx +++ b/frontend/components/navigation/sidebar/sidebar-item-with-icon.tsx @@ -1,13 +1,19 @@ import { LucideIcon } from "lucide-react"; +import { useRouter } from "next/navigation"; interface SidebarItemWithIconProps { Icon: LucideIcon; label: string; + categoryId: number; } -const SidebarItemWithIcon = ({ Icon, label }: SidebarItemWithIconProps) => { +const SidebarItemWithIcon = ({ Icon, label, categoryId }: SidebarItemWithIconProps) => { + const router = useRouter(); + + const onClickCategory = () => router.push(`/categories/${categoryId}`); + return ( -
+
{ + const { data: categories, isSuccess: isCategoriesSuccess } = useQuery(getCategories()); + return (

Other topics

- {otherTopics.map((category) => { - const categoryLabel = categoriesToDisplayName[category]; - const categoryIcon = categoriesToIconsMap[category]; + { + categories?.map((category) => { + const categoryIcon = getIconFor(category.name); return ( - ); + ) })}
From 0c85693102a67d6da5b5dac6651d7afa9dc29b62 Mon Sep 17 00:00:00 2001 From: Wang Haoyang Date: Thu, 26 Sep 2024 01:17:06 +0800 Subject: [PATCH 7/7] Fix frontend linter errors --- .../(authenticated)/categories/[id]/page.tsx | 112 +++++++++--------- .../sidebar/sidebar-item-with-icon.tsx | 13 +- .../sidebar/sidebar-other-topics.tsx | 23 ++-- frontend/queries/event.ts | 2 +- 4 files changed, 79 insertions(+), 71 deletions(-) diff --git a/frontend/app/(authenticated)/categories/[id]/page.tsx b/frontend/app/(authenticated)/categories/[id]/page.tsx index 9ea0fa8c..7729c748 100644 --- a/frontend/app/(authenticated)/categories/[id]/page.tsx +++ b/frontend/app/(authenticated)/categories/[id]/page.tsx @@ -1,73 +1,77 @@ "use client"; +import { useEffect, useState } from "react"; +import { useQuery } from "@tanstack/react-query"; + import { CategoryDTO, MiniEventDTO } from "@/client"; import ArticleLoading from "@/components/news/article-loading"; import NewsArticle from "@/components/news/news-article"; import { getCategories } from "@/queries/category"; import { getEventsForCategory } from "@/queries/event"; -import { useQuery } from "@tanstack/react-query"; -import { useEffect, useState } from "react"; const Page = ({ params }: { params: { id: string } }) => { - const categoryId = parseInt(params.id); - const [ categoryName, setCategoryName ] = useState(""); - const { data: events, isSuccess: isEventsLoaded } = useQuery(getEventsForCategory(categoryId)); - const { data: categories, isSuccess: isCategoriesLoaded } = useQuery(getCategories()); - - // Very inefficient, but is there a better way to do this? New StoreProvider for CategoryDTO[]? - useEffect(() => { - if (isCategoriesLoaded && categories!.length > 0) { - categories!.forEach((category: CategoryDTO) => { - if (category.id == categoryId) { - setCategoryName(category.name); - } - }); - } - }, [categories, isCategoriesLoaded]); + const categoryId = parseInt(params.id); + const [categoryName, setCategoryName] = useState(""); + const { data: events, isSuccess: isEventsLoaded } = useQuery( + getEventsForCategory(categoryId), + ); + const { data: categories, isSuccess: isCategoriesLoaded } = + useQuery(getCategories()); - const Articles = () => { - if (!isEventsLoaded) { - return ( - <> - - - - - ) + // Very inefficient, but is there a better way to do this? New StoreProvider for CategoryDTO[]? + useEffect(() => { + if (isCategoriesLoaded && categories!.length > 0) { + categories!.forEach((category: CategoryDTO) => { + if (category.id == categoryId) { + setCategoryName(category.name); } + }); + } + }, [categories, isCategoriesLoaded, categoryId]); - const eventData = events!.data; - if (eventData.length == 0) { - return ( -
-

No recent events. Try refreshing the page.

-
- ) - } + const Articles = () => { + if (!isEventsLoaded) { + return ( + <> + + + + + ); + } - return ( - eventData.map((newsEvent: MiniEventDTO, index: number) => ( - - )) - ) + const eventData = events!.data; + if (eventData.length == 0) { + return ( +
+

+ No recent events. Try refreshing the page. +

+
+ ); } - return ( -
-
- - {new Date().toDateString()} - -

- Top events from {categoryName} -

-
+ return eventData.map((newsEvent: MiniEventDTO, index: number) => ( + + )); + }; -
- -
-
- ) + return ( +
+
+ + {new Date().toDateString()} + +

+ Top events from {categoryName} +

+
+ +
+ +
+
+ ); }; export default Page; diff --git a/frontend/components/navigation/sidebar/sidebar-item-with-icon.tsx b/frontend/components/navigation/sidebar/sidebar-item-with-icon.tsx index 114c95fb..043e67d8 100644 --- a/frontend/components/navigation/sidebar/sidebar-item-with-icon.tsx +++ b/frontend/components/navigation/sidebar/sidebar-item-with-icon.tsx @@ -1,5 +1,5 @@ -import { LucideIcon } from "lucide-react"; import { useRouter } from "next/navigation"; +import { LucideIcon } from "lucide-react"; interface SidebarItemWithIconProps { Icon: LucideIcon; @@ -7,13 +7,20 @@ interface SidebarItemWithIconProps { categoryId: number; } -const SidebarItemWithIcon = ({ Icon, label, categoryId }: SidebarItemWithIconProps) => { +const SidebarItemWithIcon = ({ + Icon, + label, + categoryId, +}: SidebarItemWithIconProps) => { const router = useRouter(); const onClickCategory = () => router.push(`/categories/${categoryId}`); return ( -
+
{ - const { data: categories, isSuccess: isCategoriesSuccess } = useQuery(getCategories()); + const { data: categories } = useQuery(getCategories()); return (
@@ -18,17 +16,16 @@ const SidebarOtherTopics = () => { Other topics
- { - categories?.map((category) => { + {categories?.map((category) => { const categoryIcon = getIconFor(category.name); return ( - ) + ); })}
diff --git a/frontend/queries/event.ts b/frontend/queries/event.ts index ecd6266e..03530fcb 100644 --- a/frontend/queries/event.ts +++ b/frontend/queries/event.ts @@ -24,7 +24,7 @@ export const getEventsForCategory = (categoryId: number) => getEventsEventsGet({ withCredentials: true, query: { - category_ids: [ categoryId ] + category_ids: [ categoryId ], }, }).then((data) => data.data), });