Skip to content

Commit

Permalink
Merge pull request #86 from cs3216-a3-group-4/haoyang/per-category-page
Browse files Browse the repository at this point in the history
Implement per-category page
  • Loading branch information
haoyangw authored Sep 25, 2024
2 parents 47bacdd + 8625968 commit 01b0cb5
Show file tree
Hide file tree
Showing 8 changed files with 133 additions and 28 deletions.
77 changes: 77 additions & 0 deletions frontend/app/(authenticated)/categories/[id]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +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";

const Page = ({ params }: { params: { id: string } }) => {
const categoryId = parseInt(params.id);
const [categoryName, setCategoryName] = useState<string>("");
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, categoryId]);

const Articles = () => {
if (!isEventsLoaded) {
return (
<>
<ArticleLoading />
<ArticleLoading />
<ArticleLoading />
</>
);
}

const eventData = events!.data;
if (eventData.length == 0) {
return (
<div className="flex w-full justify-center">
<p className="text-sm text-offblack">
No recent events. Try refreshing the page.
</p>
</div>
);
}

return eventData.map((newsEvent: MiniEventDTO, index: number) => (
<NewsArticle key={index} newsEvent={newsEvent} />
));
};

return (
<div className="flex flex-col w-full py-8">
<div className="flex flex-col mb-8 gap-y-2 mx-8 md:mx-16 xl:mx-32">
<span className="text-sm text-muted-foreground">
{new Date().toDateString()}
</span>
<h1 className="text-3xl 2xl:text-4xl font-bold">
Top events from {categoryName}
</h1>
</div>

<div className="flex flex-col w-auto mx-4 md:mx-8 xl:mx-24">
<Articles />
</div>
</div>
);
};

export default Page;
4 changes: 4 additions & 0 deletions frontend/client/core/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -331,6 +331,10 @@ export const formDataBodySerializer = {
},
};

export const categoryIdsSerializer = (params: Record<string, any>) => {
return params.category_ids?.map((categoryId: number) => `category_ids=${categoryId}`).join("&");
};

export const jsonBodySerializer = {
bodySerializer: <T>(body: T) => JSON.stringify(body),
};
Expand Down
2 changes: 2 additions & 0 deletions frontend/client/services.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
type Options,
urlSearchParamsBodySerializer,
} from "./client";
import { categoryIdsSerializer } from "./core/utils";
import type {
AskGpQuestionUserQuestionsAskGpQuestionGetData,
AskGpQuestionUserQuestionsAskGpQuestionGetError,
Expand Down Expand Up @@ -286,6 +287,7 @@ export const getEventsEventsGet = <ThrowOnError extends boolean = false>(
ThrowOnError
>({
...options,
paramsSerializer: categoryIdsSerializer,
url: "/events/",
});
};
Expand Down
17 changes: 15 additions & 2 deletions frontend/components/navigation/sidebar/sidebar-item-with-icon.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,26 @@
import { useRouter } from "next/navigation";
import { LucideIcon } from "lucide-react";

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 (
<div className="flex rounded px-2 py-1.5 hover:bg-muted-foreground/5 text-sm font-medium items-center">
<div
className="flex rounded px-2 py-1.5 hover:bg-muted-foreground/5 text-sm font-medium items-center"
onClick={onClickCategory}
>
<Icon
className="mr-3 text-muted-foreground"
size={16}
Expand Down
37 changes: 13 additions & 24 deletions frontend/components/navigation/sidebar/sidebar-other-topics.tsx
Original file line number Diff line number Diff line change
@@ -1,40 +1,29 @@
import {
categoriesToDisplayName,
categoriesToIconsMap,
Category,
} from "@/types/categories";
"use client";

import SidebarItemWithIcon from "./sidebar-item-with-icon";
import { useQuery } from "@tanstack/react-query";

import { getCategories } from "@/queries/category";
import { getIconFor } from "@/types/categories";

// TODO: dynamically fetch
const otherTopics = [
Category.SciTech,
Category.ArtsHumanities,
Category.Politics,
Category.Media,
Category.Environment,
Category.Economics,
Category.Sports,
Category.GenderEquality,
Category.Religion,
Category.SocietyCulture,
];
import SidebarItemWithIcon from "./sidebar-item-with-icon";

const SidebarOtherTopics = () => {
const { data: categories } = useQuery(getCategories());

return (
<div className="flex flex-col space-y-2.5">
<h1 className="text-sm font-medium text-muted-foreground/80 px-2">
Other topics
</h1>
<div className="flex flex-col">
{otherTopics.map((category) => {
const categoryLabel = categoriesToDisplayName[category];
const categoryIcon = categoriesToIconsMap[category];
{categories?.map((category) => {
const categoryIcon = getIconFor(category.name);
return (
<SidebarItemWithIcon
Icon={categoryIcon}
key={category}
label={categoryLabel}
categoryId={category.id}
key={category.id}
label={category.name}
/>
);
})}
Expand Down
2 changes: 2 additions & 0 deletions frontend/components/news/news-article.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
"use client";

import { useEffect, useState } from "react";
import Image from "next/image";
import { useRouter } from "next/navigation";
Expand Down
15 changes: 14 additions & 1 deletion frontend/queries/event.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { queryOptions } from "@tanstack/react-query";

import {
getEventEventsIdGet,
getUserAuthSessionGet,
getEventsEventsGet,
} from "@/client/services.gen";

import { QueryKeys } from "./utils/query-keys";
Expand All @@ -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),
});

7 changes: 6 additions & 1 deletion frontend/types/categories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export enum Category {

export const getCategoryFor = (categoryName: string) => {
const mappings: Record<string, Category> = {
"science & tech": Category.SciTech,
"science & technology": Category.SciTech,
"arts & humanities": Category.ArtsHumanities,
politics: Category.Politics,
media: Category.Media,
Expand Down Expand Up @@ -79,3 +79,8 @@ export const categoriesToIconsMap: Record<Category, LucideIcon> = {
[Category.Education]: School,
[Category.Others]: CircleHelp,
};

export const getIconFor = (categoryName: string) => {
const category = getCategoryFor(categoryName);
return categoriesToIconsMap[category];
};

0 comments on commit 01b0cb5

Please sign in to comment.