Skip to content

Commit d1a1ec5

Browse files
committed
feat: implement category management with new components and context
1 parent 4b9249b commit d1a1ec5

File tree

14 files changed

+311
-10
lines changed

14 files changed

+311
-10
lines changed

src/app/layout.tsx

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { toUITheme } from '../utils/theme';
1313
import { Modals } from '../components/Modals/Modals';
1414
import { getSSRJwtFromCookies, getSSRLoggedInAccount } from '../utils/auth/ssrAuth';
1515
import AuthSessionChecker from '../components/Auth/AuthSessionChecker';
16+
import { apiRequestService } from '../factories/apiRequestService';
1617

1718
export const metadata = {
1819
title: 'Podverse',
@@ -28,8 +29,11 @@ export default async function RootLayout({ children }: { children: React.ReactNo
2829
const ssrLoggedInAccount = await getSSRLoggedInAccount();
2930
const ssrShouldLogout = !!(jwt && !ssrLoggedInAccount);
3031

31-
const messages = (await import(`../../i18n/originals/${locale}.json`)).default;
32+
const categoriesResponse = await apiRequestService.reqCategoryGetAll();
33+
const categories = categoriesResponse.data;
3234

35+
const messages = (await import(`../../i18n/originals/${locale}.json`)).default;
36+
3337
return (
3438
<html lang={locale} data-theme={theme}>
3539
<head>
@@ -43,7 +47,8 @@ export default async function RootLayout({ children }: { children: React.ReactNo
4347
locale={locale}
4448
ssrLoggedInAccount={ssrLoggedInAccount}
4549
theme={theme}
46-
messages={messages}>
50+
messages={messages}
51+
categories={categories}>
4752
<WindowWrapper>
4853
<SideBar />
4954
<PageWrapper>

src/clients/CategoriesClient.tsx

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
"use client";
2+
3+
import { useTranslations } from "next-intl";
4+
import { useRouter } from "next/navigation";
5+
import React from "react";
6+
import CategoriesList from "../components/Category/CategoriesList";
7+
import HeaderTextOnly from "../components/Header/HeaderTextOnly";
8+
import MainWrapper from "../components/MainWrapper/MainWrapper";
9+
import { DTOCategory } from "podverse-helpers";
10+
11+
interface CategoriesClientProps {
12+
titleKey: string;
13+
linkPath: string;
14+
}
15+
16+
export default function CategoriesClient(props: CategoriesClientProps) {
17+
const { titleKey, linkPath } = props;
18+
const tMedia = useTranslations("media");
19+
const router = useRouter();
20+
21+
const onClick = (category: DTOCategory) => {
22+
router.push(`${linkPath}?category=${category.mapping_key}`);
23+
}
24+
25+
return (
26+
<>
27+
<HeaderTextOnly title={tMedia(titleKey)} />
28+
<MainWrapper>
29+
<CategoriesList onCategoryClick={onClick} />
30+
</MainWrapper>
31+
</>
32+
);
33+
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import { useTranslations } from "next-intl";
2+
import { DTOCategory } from "podverse-helpers";
3+
import React from "react";
4+
import Link from "../Link/Link";
5+
import SubHeader from "../SubHeader/SubHeader";
6+
import { useCategories } from "../../contexts/Categories";
7+
import styles from "../../styles/components/Category/CategoriesList.module.scss";
8+
9+
type CategoriesListProps = {
10+
onCategoryClick: (category: DTOCategory, event: React.MouseEvent<HTMLButtonElement>) => void;
11+
};
12+
13+
function CategoryListItems({
14+
categories,
15+
tCategories,
16+
onCategoryClick,
17+
}: {
18+
categories: DTOCategory[];
19+
tCategories: any;
20+
onCategoryClick: (category: DTOCategory, event: React.MouseEvent<HTMLButtonElement>) => void;
21+
}) {
22+
if (!categories || categories.length === 0) return null;
23+
return (
24+
<ul className={styles.categoryList}>
25+
{categories.map((category) => (
26+
<li key={category.id}>
27+
<Link onClick={(e) => onCategoryClick(category, e)}>
28+
{tCategories(category.mapping_key)}
29+
</Link>
30+
{category.children && category.children.length > 0 && (
31+
<CategoryListItems
32+
categories={category.children}
33+
tCategories={tCategories}
34+
onCategoryClick={onCategoryClick}
35+
/>
36+
)}
37+
</li>
38+
))}
39+
</ul>
40+
);
41+
}
42+
43+
const CategoriesList = ({ onCategoryClick }: CategoriesListProps) => {
44+
const { categories } = useCategories();
45+
const tCategories = useTranslations("categories");
46+
47+
return (
48+
<section aria-labelledby="categories-heading">
49+
<SubHeader title={tCategories("categories")} />
50+
<nav>
51+
<CategoryListItems
52+
categories={categories}
53+
tCategories={tCategories}
54+
onCategoryClick={onCategoryClick}
55+
/>
56+
</nav>
57+
</section>
58+
);
59+
};
60+
61+
export default CategoriesList;

src/components/Channel/ChannelsList.tsx

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"use client";
22

33
import { useTranslations } from "next-intl";
4-
import { DTOChannel } from "podverse-helpers";
4+
import { CategoryMappingKeys, DTOChannel, QueryParamsChannelsType } from "podverse-helpers";
55
import React, { useRef } from "react";
66
import ChannelListItem from "./ChannelsListItem";
77
import CallToActionMessage from "../CallToActionMessage/CallToActionMessage";
@@ -15,9 +15,11 @@ type Props = {
1515
channels: DTOChannel[];
1616
totalPages: number;
1717
showSubscribeMessage: boolean;
18+
type?: QueryParamsChannelsType;
19+
category?: CategoryMappingKeys | null;
1820
};
1921

20-
const ChannelList: React.FC<Props> = ({ page = 1, setPage, channels, totalPages, showSubscribeMessage }) => {
22+
const ChannelList: React.FC<Props> = ({ page = 1, setPage, channels, totalPages, showSubscribeMessage, type, category }) => {
2123
const topRef = useRef<HTMLDivElement>(null);
2224
const tInstructions = useTranslations("instructions");
2325
const tAuthentication = useTranslations("authentication");
@@ -26,19 +28,22 @@ const ChannelList: React.FC<Props> = ({ page = 1, setPage, channels, totalPages,
2628
useSkipInitialEffect(() => {
2729
topRef?.current?.scrollIntoView();
2830
}, [channels]);
31+
32+
const showCallToAction = showSubscribeMessage;
33+
const showPagination = !showSubscribeMessage;
2934

3035
return (
3136
<>
3237
<div ref={topRef} />
33-
{showSubscribeMessage && (
38+
{showCallToAction && (
3439
<CallToActionMessage
3540
message={tInstructions("login_for_subscriptions")}
3641
buttonLabel={tAuthentication("login")}
3742
onButtonClick={() => openModal("LoginModal")}
3843
/>
3944
)}
4045
{
41-
!showSubscribeMessage && (
46+
showPagination && (
4247
<Pagination
4348
currentPage={page}
4449
maxButtons={5}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
"use client";
2+
3+
import React from "react";
4+
import styles from "../../styles/components/Header/Header.module.scss";
5+
6+
type HeaderTextOnlyProps = {
7+
title: string;
8+
};
9+
10+
export const HeaderTextOnly: React.FC<HeaderTextOnlyProps> = ({ title }) => (
11+
<header className={styles.header}>
12+
<div className={styles.headerContent}>
13+
<h1 className={styles.title}>{title}</h1>
14+
</div>
15+
</header>
16+
);
17+
18+
export default HeaderTextOnly;

src/components/Link/Link.tsx

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import React from "react";
2+
import NextLink from "next/link";
3+
import classNames from "classnames";
4+
import styles from "../../styles/components/Link/Link.module.scss";
5+
6+
type CustomLinkProps = {
7+
href?: string;
8+
onClick?: (e: React.MouseEvent<HTMLButtonElement>) => void;
9+
children: React.ReactNode;
10+
className?: string;
11+
type?: "button" | "submit" | "reset";
12+
tabIndex?: number;
13+
"aria-label"?: string;
14+
disabled?: boolean;
15+
style?: React.CSSProperties;
16+
};
17+
18+
const Link: React.FC<CustomLinkProps> = ({
19+
href,
20+
onClick,
21+
children,
22+
className,
23+
type = "button",
24+
tabIndex,
25+
"aria-label": ariaLabel,
26+
disabled = false,
27+
style,
28+
...rest
29+
}) => {
30+
if (href) {
31+
return (
32+
<NextLink
33+
href={href}
34+
className={classNames(styles.link, className)}
35+
tabIndex={tabIndex}
36+
aria-label={ariaLabel}
37+
style={style}
38+
{...rest}
39+
>
40+
{children}
41+
</NextLink>
42+
);
43+
}
44+
return (
45+
<button
46+
type={type}
47+
onClick={onClick}
48+
className={classNames(styles.link, className)}
49+
tabIndex={tabIndex}
50+
aria-label={ariaLabel}
51+
disabled={disabled}
52+
style={style}
53+
{...rest}
54+
>
55+
{children}
56+
</button>
57+
);
58+
};
59+
60+
export default Link;
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
"use client";
2+
3+
import React from "react";
4+
import styles from "../../styles/components/SubHeader/SubHeader.module.scss";
5+
6+
type SubHeaderProps = {
7+
title: string;
8+
filterDropdowns?: React.ReactNode[];
9+
};
10+
11+
export const SubHeader: React.FC<SubHeaderProps> = ({ title, filterDropdowns }) => (
12+
<header className={styles.subheader}>
13+
<div className={styles.subheaderContent}>
14+
<h2 className={styles.title}>{title}</h2>
15+
<div className={styles.filterDropdowns}>
16+
{filterDropdowns}
17+
</div>
18+
</div>
19+
</header>
20+
);
21+
22+
export default SubHeader;

src/contexts/Categories.tsx

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { DTOCategory } from "podverse-helpers";
2+
import React, { createContext, useState, ReactNode } from "react";
3+
import { useContext } from "react";
4+
5+
type CategoriesContextType = {
6+
categories: DTOCategory[];
7+
};
8+
9+
export const CategoriesContext = createContext<CategoriesContextType>({
10+
categories: [],
11+
});
12+
13+
type CategoriesProviderProps = {
14+
children: ReactNode;
15+
ssrCategories?: DTOCategory[];
16+
};
17+
18+
export const CategoriesProvider = ({
19+
children,
20+
ssrCategories = [],
21+
}: CategoriesProviderProps) => {
22+
const [categories] = useState<DTOCategory[]>(ssrCategories);
23+
24+
return (
25+
<CategoriesContext.Provider value={{ categories }}>
26+
{children}
27+
</CategoriesContext.Provider>
28+
);
29+
};
30+
31+
export function useCategories() {
32+
const ctx = useContext(CategoriesContext);
33+
if (!ctx) throw new Error("useCategories must be used within a CategoriesProvider");
34+
return ctx;
35+
}

src/providers/Providers.tsx

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,36 @@
11
"use client";
22

33
import { NextIntlClientProvider } from 'next-intl';
4-
import { DTOAccount } from 'podverse-helpers';
4+
import { DTOAccount, DTOCategory } from 'podverse-helpers';
55
import { AccountProvider } from '../contexts/Account';
66
import { ThemeProvider } from '../contexts/Theme';
77
import { ModalsProvider } from '../contexts/Modals';
88
import { UITheme } from '../utils/theme';
9+
import { CategoriesProvider } from '../contexts/Categories';
910

1011
export default function Providers({
1112
children,
1213
theme,
1314
locale,
1415
ssrLoggedInAccount,
15-
messages
16+
messages,
17+
categories
1618
}: {
1719
children: React.ReactNode;
1820
theme: UITheme;
1921
locale: string;
2022
ssrLoggedInAccount: DTOAccount | null;
2123
messages: Record<string, any>;
24+
categories: DTOCategory[];
2225
}) {
2326
return (
2427
<NextIntlClientProvider locale={locale} messages={messages} timeZone="America/Chicago">
2528
<ThemeProvider initialTheme={theme}>
2629
<AccountProvider ssrLoggedInAccount={ssrLoggedInAccount}>
2730
<ModalsProvider>
28-
{children}
31+
<CategoriesProvider ssrCategories={categories}>
32+
{children}
33+
</CategoriesProvider>
2934
</ModalsProvider>
3035
</AccountProvider>
3136
</ThemeProvider>
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
.categoryList {
2+
list-style: none;
3+
padding: 0;
4+
margin: 0;
5+
}
6+
7+
.categoryList ul {
8+
margin: 1rem 0 0 1rem;
9+
}
10+
11+
.categoryList li {
12+
margin-bottom: 1rem;
13+
}

0 commit comments

Comments
 (0)