Skip to content

Commit

Permalink
Merge pull request #103 from cs3216-a3-group-4/seeleng/paginate-categ…
Browse files Browse the repository at this point in the history
…ories

feat: paginate categories page
  • Loading branch information
seelengxd authored Sep 27, 2024
2 parents afb31de + 68bb7f9 commit 359d917
Show file tree
Hide file tree
Showing 13 changed files with 474 additions and 67 deletions.
38 changes: 38 additions & 0 deletions frontend/app/(authenticated)/categories/[id]/articles-list.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
"use client";

import { MiniEventDTO } from "@/client";
import ArticleLoading from "@/components/news/article-loading";
import NewsArticle from "@/components/news/news-article";

interface Props {
eventData: MiniEventDTO[] | undefined;
isEventsLoaded: boolean;
}

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

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} />
));
};

export default Articles;
182 changes: 147 additions & 35 deletions frontend/app/(authenticated)/categories/[id]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,43 @@
"use client";

import { useEffect, useState } from "react";
import { usePathname, useRouter, useSearchParams } from "next/navigation";
import { useQuery } from "@tanstack/react-query";

import { CategoryDTO, MiniEventDTO } from "@/client";
import ScrollToTopButton from "@/components/navigation/scroll-to-top-button";
import ArticleLoading from "@/components/news/article-loading";
import NewsArticle from "@/components/news/news-article";
import {
Pagination,
PaginationContent,
PaginationItem,
PaginationLink,
PaginationNext,
PaginationPrevious,
} from "@/components/ui/pagination";
import { getCategories } from "@/queries/category";
import { getEventsForCategory } from "@/queries/event";
import { parseDate } from "@/utils/date";

function isNumeric(value: string | null) {
return value !== null && /^-?\d+$/.test(value);
}

const Page = ({ params }: { params: { id: string } }) => {
const router = useRouter();
const pathname = usePathname();
const searchParams = useSearchParams();

const categoryId = parseInt(params.id);

const pageStr = searchParams.get("page");

const page = isNumeric(pageStr) ? parseInt(pageStr!) : 1;

const [categoryName, setCategoryName] = useState<string>("");
const { data: events, isSuccess: isEventsLoaded } = useQuery(
getEventsForCategory(categoryId),
getEventsForCategory(categoryId, page),
);
const { data: categories, isSuccess: isCategoriesLoaded } =
useQuery(getCategories());
Expand All @@ -29,47 +53,135 @@ const Page = ({ params }: { params: { id: string } }) => {
}
}, [categories, isCategoriesLoaded, categoryId]);

const Articles = () => {
if (!isEventsLoaded) {
return (
<>
<ArticleLoading />
<ArticleLoading />
<ArticleLoading />
</>
);
}
const getPageUrl = (page: number) => {
// now you got a read/write object
const current = new URLSearchParams(Array.from(searchParams.entries())); // -> has to use this form

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>
);
}
// update as necessary
current.set("page", page.toString());

// cast to string
const search = current.toString();
// or const query = `${'?'.repeat(search.length && 1)}${search}`;
const query = search ? `?${search}` : "";

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

const changePage = (page: number) => {
router.push(getPageUrl(page));
};

if (!pageStr) {
changePage(1);
return;
}

const items = events?.total_count;
const pageCount = items !== undefined && Math.ceil(items / 10);
if (pageCount !== false) {
if (page <= 0) {
changePage(1);
}
if (page > pageCount) {
changePage(pageCount);
}
}

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="relative w-full h-full">
<div
className="flex bg-muted w-full h-full max-h-full py-8 overflow-y-auto"
id="home-page"
>
<div className="flex flex-col py-6 lg:py-12 w-full h-fit mx-4 md:mx-8 xl:mx-24 bg-background rounded-lg border border-border px-8">
{/* TODO: x-padding here is tied to the news article */}
<div
className="flex flex-col mb-4 gap-y-2 px-4 md:px-8 xl:px-12"
id="homePage"
>
<div className="flex">
<span className="text-4xl 2xl:text-4xl font-bold text-primary-800">
Top events from {categoryName}
</span>
</div>
<span className="text-primary text-lg">
{parseDate(new Date())}
</span>
</div>

<div className="flex flex-col w-auto mx-4 md:mx-8 xl:mx-24">
<Articles />
<div className="flex flex-col w-full">
{!isEventsLoaded ? (
<div className="flex flex-col w-full">
<ArticleLoading />
<ArticleLoading />
<ArticleLoading />
</div>
) : (
events!.data.map((newsEvent: MiniEventDTO, index: number) => (
<NewsArticle key={index} newsEvent={newsEvent} />
))
)}
</div>
{isEventsLoaded && (
<Pagination className="py-8">
<PaginationContent>
{page !== 1 && (
<PaginationItem>
<PaginationPrevious href={getPageUrl(page - 1)} />
</PaginationItem>
)}
{/* Only for last page */}
{page == pageCount && (
<PaginationItem>
<PaginationLink href={getPageUrl(page - 2)}>
{page - 2}
</PaginationLink>
</PaginationItem>
)}
{page !== 1 && (
<PaginationItem>
<PaginationLink href={getPageUrl(page - 1)}>
{page - 1}
</PaginationLink>
</PaginationItem>
)}
<PaginationItem>
<PaginationLink href="#" isActive>
{page}
</PaginationLink>
</PaginationItem>
{page !== pageCount && (
<PaginationItem>
<PaginationLink href={getPageUrl(page + 1)}>
{page + 1}
</PaginationLink>
</PaginationItem>
)}
{/* Only for first page */}
{page === 1 && (pageCount as number) >= 3 && (
<PaginationItem>
<PaginationLink href={getPageUrl(page + 2)}>
{page + 2}
</PaginationLink>
</PaginationItem>
)}
{page !== pageCount && (
<PaginationItem>
<PaginationNext href={getPageUrl(page + 1)} />
</PaginationItem>
)}
</PaginationContent>
</Pagination>
)}
</div>
</div>

<ScrollToTopButton
className="absolute right-4 bottom-4"
minHeight={200}
scrollElementId="home-page"
/>
</div>
);
};
Expand Down
35 changes: 31 additions & 4 deletions frontend/app/(authenticated)/events/[id]/event-details.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ const EventDetails = ({ event }: Props) => {
);

return (
<div className="flex flex-col px-6 text-muted-foreground font-[450] space-y-2 md:space-y-4">
<div className="flex flex-col px-6 text-muted-foreground font-[450] space-y-4 md:space-y-4">
<div className="grid grid-cols-12 gap-x-4 gap-y-3 place-items-start">
<span className="flex items-center col-span-12 md:col-span-4 xl:col-span-3">
<LayoutDashboardIcon
Expand All @@ -33,25 +33,52 @@ const EventDetails = ({ event }: Props) => {
{eventCategories.map((category) => (
<Chip
Icon={categoriesToIconsMap[category]}
className="mb-2 md:mb-0"
key={category}
label={categoriesToDisplayName[category]}
size={"lg"}
variant="primary" // TODO: this is ugly
/>
))}
</div>
</div>

<div className="grid grid-cols-12 gap-x-4 gap-y-2 place-items-start">
<div className="hidden md:grid grid-cols-12 gap-x-4 gap-y-2 place-items-start">
<span className="flex items-center col-span-12 md:col-span-4 xl:col-span-3">
<ClockIcon className="inline-flex mr-2" size={16} strokeWidth={2.3} />
Event date
</span>
<span className="col-span-1 md:col-span-8 xl:col-span-9 text-black font-normal">
<span className="col-span-10 md:col-span-8 xl:col-span-9 text-black font-normal">
{parseDate(event.date)}
</span>
</div>
<div className="flex gap-2 md:hidden">
<span className="flex items-center col-span-12 md:col-span-4 xl:col-span-3">
<ClockIcon className="inline-flex mr-2" size={16} strokeWidth={2.3} />
Event date
</span>
<span className="col-span-10 md:col-span-8 xl:col-span-9 text-black font-normal">
{parseDate(event.date)}
</span>
</div>

<div className="hidden md:grid grid-cols-12 gap-x-4 gap-y-2 place-items-start">
<span className="flex items-center col-span-12 md:col-span-4 xl:col-span-3">
<NewspaperIcon
className="inline-flex mr-2"
size={16}
strokeWidth={2.3}
/>
News source
</span>
<span className="col-span-1 md:col-span-8 xl:col-span-9 text-black font-normal">
<a className="underline" href={event.original_article.url}>
{event.original_article.source.replace("GUARDIAN", "Guardian")}
</a>
</span>
</div>

<div className="grid grid-cols-12 gap-x-4 gap-y-2 place-items-start">
<div className="flex gap-2 md:hidden">
<span className="flex items-center col-span-12 md:col-span-4 xl:col-span-3">
<NewspaperIcon
className="inline-flex mr-2"
Expand Down
64 changes: 64 additions & 0 deletions frontend/app/(authenticated)/events/[id]/page-component.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
"use client";

import { useEffect } from "react";
import Image from "next/image";
import { useQuery } from "@tanstack/react-query";

import { LoadingSpinner } from "@/components/ui/loading-spinner";
import { Separator } from "@/components/ui/separator";
import { getEvent } from "@/queries/event";

import EventAnalysis from "./event-analysis";
import EventDetails from "./event-details";
import EventSource from "./event-source";
import EventSummary from "./event-summary";

const Page = ({ params }: { params: { id: string } }) => {
const id = parseInt(params.id);
const { data, isLoading } = useQuery(getEvent(id));

console.log({ data, isLoading });

useEffect(() => {
console.log(`Client-side hydration ${id}`);
}, [id]);

if (isLoading) {
return (
<div className="flex justify-center items-center w-full">
<LoadingSpinner className="w-24 h-24" />
</div>
);
}

return (
data && (
<div className="flex flex-col mx-8 md:mx-16 xl:mx-56 py-8 w-full h-fit">
<div className="flex flex-col gap-y-10">
<div className="flex w-full max-h-52 overflow-y-clip items-center rounded-t-2xl rounded-b-sm border">
<Image
alt={data?.title}
height={154}
src={data.original_article.image_url}
style={{
width: "100%",
height: "fit-content",
}}
unoptimized
width={273}
/>
</div>
<h1 className="text-4xl font-bold px-6">{data.title}</h1>
<EventDetails event={data} />
<EventSummary summary={data.description} />
</div>
<Separator className="my-10" />
<EventAnalysis event={data} />
<Separator className="my-10" />
<EventSource originalSource={data.original_article} />
</div>
)
);
};

export default Page;
Loading

0 comments on commit 359d917

Please sign in to comment.