Skip to content

Commit 3851973

Browse files
authored
chore: add articles sections by category (#1304)
1 parent 5ec37df commit 3851973

29 files changed

+455
-560
lines changed

src/app/[lng]/(infos)/learn/page.tsx

+5-1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { getArticles } from '@/app/lib/getArticles';
33
import { getFeaturedArticle } from '@/app/lib/getFeaturedArticle';
44
import LearnPage from '@/app/ui/learn/LearnPage';
55
import type { Metadata } from 'next';
6+
import { getTags } from 'src/app/lib/getTags';
67

78
export async function generateMetadata(): Promise<Metadata> {
89
return {
@@ -18,9 +19,12 @@ export async function generateMetadata(): Promise<Metadata> {
1819
export default async function Page() {
1920
// TODO: make this component client side by removing async, a hook should do the job, will permit us to pre-render the pages
2021
const featuredArticle = await getFeaturedArticle();
21-
const carouselArticles = await getArticles(featuredArticle.data.id);
22+
const carouselArticles = await getArticles(featuredArticle.data.id, 5);
23+
const tags = await getTags();
24+
2225
return (
2326
<LearnPage
27+
tags={tags}
2428
carouselArticles={carouselArticles}
2529
featuredArticle={featuredArticle.data}
2630
url={carouselArticles.url}

src/app/[lng]/layout.tsx

+7-7
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,15 @@
11
import initTranslations from '@/app/i18n';
2-
import TranslationsProvider from '@/providers/TranslationProvider';
3-
import i18nConfig from 'i18nconfig';
4-
import type { ReactNode } from 'react';
5-
import React from 'react';
6-
import { defaultNS, fallbackLng, namespaces } from 'src/i18n';
7-
import { fonts } from '@/fonts/fonts';
8-
import Script from 'next/script';
92
import { PixelBg } from '@/components/illustrations/PixelBg';
3+
import { fonts } from '@/fonts/fonts';
104
import { ReactQueryProvider } from '@/providers/ReactQueryProvider';
5+
import TranslationsProvider from '@/providers/TranslationProvider';
116
import { WalletProvider } from '@/providers/WalletProvider';
127
import { AppRouterCacheProvider } from '@mui/material-nextjs/v14-appRouter';
8+
import i18nConfig from 'i18nconfig';
9+
import Script from 'next/script';
1310
import type { Viewport } from 'next/types';
11+
import type { ReactNode } from 'react';
12+
import { defaultNS, fallbackLng, namespaces } from 'src/i18n';
1413
import { metadata as JumperMetadata } from '../lib/metadata';
1514
export const metadata = JumperMetadata;
1615

@@ -33,6 +32,7 @@ export default async function RootLayout({
3332
lang={lng || fallbackLng}
3433
suppressHydrationWarning
3534
className={fonts.map((f) => f.variable).join(' ')}
35+
style={{ scrollBehavior: 'smooth' }}
3636
>
3737
<head>
3838
<link rel="icon" href="/favicon.ico" sizes="any" />

src/app/lib/getArticles.ts

+6-3
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,13 @@ export interface GetArticlesResponse extends StrapiResponse<BlogArticleData> {
77

88
export async function getArticles(
99
excludeId?: number,
10+
pageSize?: number,
1011
): Promise<GetArticlesResponse> {
11-
const urlParams = new ArticleStrapiApi()
12-
.sort('desc')
13-
.addPaginationParams({ page: 1, pageSize: 20, withCount: false });
12+
const urlParams = new ArticleStrapiApi().sort('desc').addPaginationParams({
13+
page: 1,
14+
pageSize: pageSize || 20,
15+
withCount: false,
16+
});
1417
const apiBaseUrl = urlParams.getApiBaseUrl();
1518
const apiUrl = urlParams.getApiUrl();
1619
const accessToken = urlParams.getApiAccessToken();

src/app/lib/getTags.ts

+88
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import type { StrapiResponse, TagAttributes } from '@/types/strapi';
2+
import { TagStrapiApi } from '@/utils/strapi/StrapiApi';
3+
4+
export interface GetTagsResponse extends StrapiResponse<TagAttributes> {
5+
url: string;
6+
}
7+
8+
const predefinedOrder = ['Announcement', 'Partner', 'Bridge'];
9+
10+
// Helper function to sort tags based on predefined order
11+
const sortTagsByPredefinedOrder = (tags: TagAttributes[]) => {
12+
return tags.sort((a, b) => {
13+
const titleA = a.attributes.Title;
14+
const titleB = b.attributes.Title;
15+
16+
const indexA = predefinedOrder.indexOf(titleA);
17+
const indexB = predefinedOrder.indexOf(titleB);
18+
19+
if (indexA === -1 && indexB === -1) {
20+
return 0;
21+
}
22+
if (indexA === -1) {
23+
return 1;
24+
}
25+
if (indexB === -1) {
26+
return -1;
27+
}
28+
return indexA - indexB;
29+
});
30+
};
31+
32+
// Helper function to sort blog articles by `publishedAt` date
33+
const sortBlogArticlesByPublishedDate = (tags: TagAttributes[]) => {
34+
return tags.map((tag) => {
35+
tag.attributes.blog_articles.data = tag.attributes.blog_articles.data.sort(
36+
(a, b) => {
37+
const dateA = a.attributes.publishedAt
38+
? Date.parse(a.attributes.publishedAt)
39+
: -Infinity; // Default to oldest if undefined
40+
const dateB = b.attributes.publishedAt
41+
? Date.parse(b.attributes.publishedAt)
42+
: -Infinity; // Default to oldest if undefined
43+
44+
return dateB - dateA;
45+
},
46+
);
47+
return tag;
48+
});
49+
};
50+
51+
export async function getTags(): Promise<GetTagsResponse> {
52+
const urlParams = new TagStrapiApi().addPaginationParams({
53+
page: 1,
54+
pageSize: 20,
55+
withCount: false,
56+
});
57+
const apiBaseUrl = urlParams.getApiBaseUrl();
58+
const apiUrl = urlParams.getApiUrl();
59+
const accessToken = urlParams.getApiAccessToken();
60+
61+
const res = await fetch(decodeURIComponent(apiUrl), {
62+
cache: 'force-cache',
63+
headers: {
64+
Authorization: `Bearer ${accessToken}`,
65+
},
66+
});
67+
68+
if (!res.ok) {
69+
throw new Error('Failed to fetch data');
70+
}
71+
72+
const data = await res.json().then((output) => {
73+
let filteredData = output.data;
74+
75+
// Sort blog_articles by published date first
76+
filteredData = sortBlogArticlesByPublishedDate(filteredData);
77+
78+
// Then sort tags by predefined order
79+
filteredData = sortTagsByPredefinedOrder(filteredData);
80+
81+
return {
82+
meta: output.meta,
83+
data: filteredData,
84+
};
85+
});
86+
87+
return { ...data, url: apiBaseUrl };
88+
}

src/app/ui/learn/LearnPage.tsx

+5-1
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,20 @@ import { BlogCarousel } from '@/components/Blog/BlogCarousel/BlogCarousel';
33
import { FeaturedArticle } from '@/components/Blog/FeaturedArticle/FeaturedArticle';
44
import { JoinDiscordBanner } from '@/components/JoinDiscordBanner/JoinDiscordBanner';
55
import type { BlogArticleData, StrapiResponse } from '@/types/strapi';
6+
import type { GetTagsResponse } from 'src/app/lib/getTags';
7+
import { BlogArticlesCollections } from 'src/components/Blog/BlogArticlesCollections/BlogArticlesCollections';
68

79
interface LearnPageProps {
810
carouselArticles: GetArticlesResponse;
911
featuredArticle: StrapiResponse<BlogArticleData>;
1012
url: string;
13+
tags: GetTagsResponse;
1114
}
1215

1316
const LearnPage = ({
1417
carouselArticles,
1518
featuredArticle,
19+
tags,
1620
url,
1721
}: LearnPageProps) => {
1822
return (
@@ -28,7 +32,7 @@ const LearnPage = ({
2832
)}
2933
<BlogCarousel url={url} data={carouselArticles?.data} />
3034
<JoinDiscordBanner />
31-
{/* <BlogArticlesBoard /> */}
35+
<BlogArticlesCollections tags={tags} data={carouselArticles?.data} />
3236
</div>
3337
);
3438
};

src/components/Blog/BlogArticleCard/BlogArticleCard.style.ts

+10-3
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { Tag } from '@/components/Tag.style';
2+
import type { BoxProps } from '@mui/material';
23
import {
34
Box,
45
Card,
@@ -9,8 +10,8 @@ import {
910
type Breakpoint,
1011
} from '@mui/material';
1112
import { styled } from '@mui/material/styles';
12-
import { urbanist } from 'src/fonts/fonts';
1313
import Image from 'next/image';
14+
import { urbanist } from 'src/fonts/fonts';
1415

1516
export const BlogArticleCardContainer = styled(Card)(({ theme }) => ({
1617
flexShrink: 0,
@@ -111,14 +112,20 @@ export const BlogArticleCardTitleSkeleton = styled(Skeleton)(({ theme }) => ({
111112
minHeight: '64px',
112113
}));
113114

114-
export const BlogArticleCardMetaContainer = styled(Box)(({ theme }) => ({
115+
interface BlogArticleCardMetaContainerProps extends BoxProps {
116+
hasTags: boolean;
117+
}
118+
119+
export const BlogArticleCardMetaContainer = styled(Box, {
120+
shouldForwardProp: (prop) => prop !== 'hasTags',
121+
})<BlogArticleCardMetaContainerProps>(({ theme, hasTags }) => ({
115122
display: 'flex',
116123
alignItems: 'center',
117124
fontSize: '14px',
118125
color: theme.palette.text.primary,
119126
'*': { textWrap: 'nowrap' },
120127
[theme.breakpoints.up('sm' as Breakpoint)]: {
121-
marginLeft: theme.spacing(2),
128+
...(hasTags && { marginLeft: theme.spacing(1) }),
122129
},
123130
}));
124131

src/components/Blog/BlogArticleCard/BlogArticleCard.tsx

+23-31
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,12 @@ import { TrackingAction, TrackingEventParameter } from '@/const/trackingKeys';
22
import { JUMPER_LEARN_PATH } from '@/const/urls';
33
import { useUserTracking } from '@/hooks/userTracking/useUserTracking';
44
import { useMenuStore } from '@/stores/menu/MenuStore';
5-
import type { StrapiImageData, TagData } from '@/types/strapi';
5+
import type { BlogArticleData } from '@/types/strapi';
66
import { formatDate } from '@/utils/formatDate';
77
import { readingTime } from '@/utils/readingTime';
88
import type { CSSObject } from '@mui/material';
99
import { Skeleton } from '@mui/material';
1010
import Link from 'next/link';
11-
import type { RootNode } from 'node_modules/@strapi/blocks-react-renderer/dist/BlocksRenderer';
1211
import { useTranslation } from 'react-i18next';
1312
import {
1413
BlogArticleCardContainer,
@@ -23,63 +22,51 @@ import {
2322
} from '.';
2423

2524
interface BlogArticleCardProps {
25+
article: BlogArticleData;
2626
baseUrl: string;
27-
id: number;
28-
image: StrapiImageData;
29-
content: RootNode[];
30-
publishedAt: string | null | undefined;
31-
createdAt: string;
32-
title: string;
33-
tags: TagData | undefined;
3427
trackingCategory: string;
35-
slug: string;
3628
styles?: CSSObject;
3729
}
3830

3931
export const BlogArticleCard = ({
32+
article,
4033
baseUrl,
4134
trackingCategory,
42-
image,
43-
tags,
44-
content,
45-
publishedAt,
46-
createdAt,
47-
id,
48-
title,
4935
styles,
50-
slug,
5136
}: BlogArticleCardProps) => {
5237
const { trackEvent } = useUserTracking();
53-
const minRead = readingTime(content);
38+
const minRead = readingTime(article.attributes.Content);
5439
const { t } = useTranslation();
5540
const { closeAllMenus } = useMenuStore((state) => state);
56-
5741
const handleClick = () => {
5842
trackEvent({
5943
category: trackingCategory,
6044
action: TrackingAction.ClickArticleCard,
6145
label: 'click-blog-article-card',
6246
data: {
63-
[TrackingEventParameter.ArticleTitle]: title,
64-
[TrackingEventParameter.ArticleCardId]: id,
47+
[TrackingEventParameter.ArticleTitle]: article.attributes.Title,
48+
[TrackingEventParameter.ArticleCardId]: article.id,
6549
},
6650
});
6751
closeAllMenus();
6852
};
6953
return (
7054
<Link
71-
href={`${JUMPER_LEARN_PATH}/${slug}`}
72-
style={{ textDecoration: 'none' }}
55+
href={`${JUMPER_LEARN_PATH}/${article.attributes.Slug}`}
56+
style={{ textDecoration: 'none', width: '100%' }}
7357
>
7458
<BlogArticleCardContainer
7559
variant="outlined"
7660
onClick={handleClick}
7761
sx={styles}
7862
>
79-
{image.data ? (
63+
{article.attributes.Image.data ? (
8064
<BlogArticleCardImage
81-
src={`${baseUrl}${image.data?.attributes?.formats.small.url}`}
82-
alt={image.data?.attributes?.alternativeText ?? title}
65+
src={`${baseUrl}${article.attributes.Image.data?.attributes?.formats.small.url}`}
66+
alt={
67+
article.attributes.Image.data?.attributes?.alternativeText ??
68+
article.attributes.Title
69+
}
8370
// read the following to udnerstand why width and height are set to 0, https://github.com/vercel/next.js/discussions/18474#discussioncomment-5501724
8471
width={0}
8572
height={0}
@@ -100,17 +87,22 @@ export const BlogArticleCard = ({
10087

10188
<BlogArticleCardContent>
10289
<BlogArticleCardTitle variant="bodyLarge">
103-
{title}
90+
{article.attributes.Title}
10491
</BlogArticleCardTitle>
10592
<BlogArticleCardDetails>
106-
{tags?.data.slice(0, 1).map((tag, index) => (
93+
{article.attributes.tags?.data.slice(0, 1).map((tag, index) => (
10794
<BlogArticleCardTag key={index} variant="bodyXSmall" as="h3">
10895
{tag.attributes.Title}
10996
</BlogArticleCardTag>
11097
))}
111-
<BlogArticleCardMetaContainer>
98+
<BlogArticleCardMetaContainer
99+
hasTags={article.attributes.tags?.data.length > 0}
100+
>
112101
<BlogArticleMetaDate variant="bodyXSmall" as="span">
113-
{formatDate(publishedAt || createdAt)}
102+
{formatDate(
103+
article.attributes.publishedAt ||
104+
article.attributes.createdAt,
105+
)}
114106
</BlogArticleMetaDate>
115107
<BlogArticleMetaReadingTime variant="bodyXSmall" as="span">
116108
{t('blog.minRead', { minRead: minRead })}

src/components/Blog/BlogArticleCard/BlogArticleCardSkeleton.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ export const BlogArticleCardSkeleton = () => {
2121
<BlogArticleCardTitleSkeleton variant="text" />
2222
<BlogArticleCardDetails>
2323
<BlogArticleCardTagSkeleton variant="text" />
24-
<BlogArticleCardMetaContainer>
24+
<BlogArticleCardMetaContainer hasTags={true}>
2525
<BlogArticleCardMetaSkeleton variant="text" />
2626
</BlogArticleCardMetaContainer>
2727
</BlogArticleCardDetails>

0 commit comments

Comments
 (0)