Skip to content

Commit

Permalink
feat: add listing page pagination
Browse files Browse the repository at this point in the history
  • Loading branch information
emmanuelgautier committed Jul 20, 2024
1 parent 95f2244 commit 7159f1c
Show file tree
Hide file tree
Showing 4 changed files with 160 additions and 70 deletions.
5 changes: 5 additions & 0 deletions packages/config/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,11 @@ const galactiksConfigFileSchema = z.object({
template: z.string(),
analytics: analyticsConfigSchema.optional(),
trailingSlash: z.enum(['ignore', 'always', 'never']).optional(),
pagination: z.object({
pageSize: z.number().optional().default(10),
}).default({
pageSize: 10,
}),
pages: z
.object({
articles: pagesObjectItemSchema,
Expand Down
5 changes: 4 additions & 1 deletion packages/explorer/src/content/repositories/filters.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import type { Content } from '../../types/index.js';
import type { Content, ContentlayerWebPageElement } from '../../types/index.js';

export const availableFilters = ['type', 'isPartOf']
export type RepositoryFilters = {
type?: Content['type'];
isPartOf?: Content['isPartOf'];
inLanguage?: string;
inLanguages?: string[];
};

export type WebPageElementFilters = {
inLanguage?: string;
type?: ContentlayerWebPageElement['elementType']
};
151 changes: 111 additions & 40 deletions packages/explorer/src/content/repositories/generated.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,21 @@ import type {
} from '../../types/index.js';

import { computeDocuments } from '../hydrate/hydrate.js';
import type { RepositoryFilters, WebPageElementFilters } from './filters.js';
import { availableFilters, type RepositoryFilters, type WebPageElementFilters } from './filters.js';

export type Pagination = {
page: number;
};

export type WebPageElementsResult<T> = {
elements: T[];
pagination?: {
page: number;
pageSize: number;
total: number;
},
};
export type ContentResult = WebPageElementsResult<Content>;

let _generated: ContentlayerDataExports;
let _documents: Content[];
Expand All @@ -18,10 +32,10 @@ const dateNow = new Date();

const documentsByLanguagesSelector =
<T extends Pick<ContentlayerWebPageDocument, 'inLanguage'>>(documents: T[]) =>
(inLanguages: string[]) =>
documents.filter(
(_d) => !_d.inLanguage || inLanguages.indexOf(_d.inLanguage) !== -1
);
(inLanguages: string[]) =>
documents.filter(
(_d) => !_d.inLanguage || inLanguages.indexOf(_d.inLanguage) !== -1
);

const getGenerated = async (): Promise<ContentlayerDataExports> => {
if (!_generated) {
Expand All @@ -36,13 +50,38 @@ export const getWebsites = async (): Promise<ContentlayerWebsite[]> =>
(await getGenerated()).allWebsites || [];

export const getWebPageElements = async (
filters?: WebPageElementFilters
): Promise<ContentlayerWebPageElement[]> => {
const elements = (await getGenerated()).allWebPageElements || [];
filters?: WebPageElementFilters,
pagination?: Pagination,
): Promise<WebPageElementsResult<ContentlayerWebPageElement>> => {
let elements = (await getGenerated()).allWebPageElements || [];
if (filters?.inLanguage) {
elements = documentsByLanguagesSelector(elements)([filters.inLanguage]);
}
if (filters?.type) {
elements = elements.filter(({ elementType }) => elementType === filters.type);
}

let resultPagination = undefined;
if (pagination) {
const { pagination: {
pageSize = 10,
} } = getConfig();
const page = pagination?.page || 1;
const total = elements.length;
resultPagination = {
page,
pageSize,
total,
};

const start = (page - 1) * pageSize;
elements = elements.slice(start, start + pageSize);
}

return filters?.inLanguage
? documentsByLanguagesSelector(elements)([filters.inLanguage])
: elements;
return {
elements,
pagination: resultPagination,
};
};

const getWebPageDocuments = async (): Promise<Content[]> => {
Expand All @@ -67,11 +106,11 @@ const getWebPageDocuments = async (): Promise<Content[]> => {
)
.sort((a, b) =>
'datePublished' in b &&
b.datePublished &&
'datePublished' in a &&
a.datePublished
b.datePublished &&
'datePublished' in a &&
a.datePublished
? new Date(b.datePublished).getTime() -
new Date(a.datePublished).getTime()
new Date(a.datePublished).getTime()
: 0
);
_documents = await computeDocuments({
Expand All @@ -86,40 +125,72 @@ const getWebPageDocuments = async (): Promise<Content[]> => {

export const getWebPageElementByType = async (
type: ContentlayerWebPageElement['elementType'],
filters?: WebPageElementFilters
): Promise<ContentlayerWebPageElement | undefined> =>
(await getWebPageElements(filters)).find(
({ elementType: _t }) => _t === type
);
export const getSiteNavigationElement = (filters?: WebPageElementFilters) =>
getWebPageElementByType('SiteNavigationElement', filters);
export const getWebPageHeader = (filters?: WebPageElementFilters) =>
getWebPageElementByType('WPHeader', filters);
export const getWebPageFooter = (filters?: WebPageElementFilters) =>
getWebPageElementByType('WPFooter', filters);
filters?: WebPageElementFilters,
pagination?: Pagination,
) =>
getWebPageElements({ ...filters, type }, pagination);
export const getSiteNavigationElement = (
filters?: WebPageElementFilters,
pagination?: Pagination,
) =>
getWebPageElementByType('SiteNavigationElement', filters, pagination);
export const getWebPageHeader = (
filters?: WebPageElementFilters,
pagination?: Pagination,
) =>
getWebPageElementByType('WPHeader', filters, pagination);
export const getWebPageFooter = (
filters?: WebPageElementFilters,
pagination?: Pagination,
) =>
getWebPageElementByType('WPFooter', filters, pagination);

export const getPages = async (
filters?: RepositoryFilters
): Promise<Content[]> => {
filters?: RepositoryFilters,
pagination?: Pagination,
): Promise<ContentResult> => {
const documents = await getWebPageDocuments();
if (!filters) {
return documents;
}

let inLanguages = Array.isArray(filters.inLanguages)
let inLanguages = Array.isArray(filters?.inLanguages)
? filters.inLanguages
: [];
if (typeof filters.inLanguage === 'string') {
if (typeof filters?.inLanguage === 'string') {
inLanguages = inLanguages.concat(filters.inLanguage);
}

return documents.filter(
(_d) =>
(inLanguages.length === 0 ||
!_d.inLanguage ||
inLanguages.indexOf(_d.inLanguage) !== -1) &&
(!filters.type || _d.type === filters.type)
);
let elements = documents;
if (inLanguages.length > 0) {
elements = documentsByLanguagesSelector(documents)(inLanguages);
}

const availableFiltersArgs = typeof filters === 'object' && filters !== null
? availableFilters.filter((key) => key in filters)
: [];
if (availableFiltersArgs.length > 0) {
elements = documents.filter(_d => availableFiltersArgs.every(key => _d[key] === filters[key]));
}

let resultPagination = undefined;
if (pagination) {
const { pagination: {
pageSize = 10,
} } = getConfig();
const page = pagination?.page || 1;
const total = elements.length;
resultPagination = {
page,
pageSize,
total,
};

const start = (page - 1) * pageSize;
elements = elements.slice(start, start + pageSize);
}

return {
elements,
pagination: resultPagination,
};
};

export const getWebPageDocumentsByType = async (
Expand Down
69 changes: 40 additions & 29 deletions packages/explorer/src/content/repositories/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,28 +6,30 @@ import { documentByIdentifierSelector } from '../selectors.js';
import { createIdentifierFromString } from '../utils.js';

import type { RepositoryFilters } from './filters.js';
import { getOrganizations, getPages } from './generated.js';
import { type ContentResult, getOrganizations, getPages, type Pagination } from './generated.js';

export * from './generated.js';

export const getRootPages = async (
filters?: RepositoryFilters
): Promise<Content[]> =>
(await getPages(filters)).filter((doc) => !doc.isPartOf);
): Promise<ContentResult> =>
getPages({ ...filters, isPartOf: undefined });

export const getPagesPartOf = async (
slug: string,
filters?: RepositoryFilters
): Promise<Content[]> =>
(await getPages(filters)).filter((doc) => doc.isPartOf === slug);
filters?: RepositoryFilters,
pagination?: Pagination,
): Promise<ContentResult> =>
getPages({ ...filters, isPartOf: slug }, pagination);

export const getPagesPartOfRecursively = async (
slug: string,
filters?: RepositoryFilters
): Promise<Content[]> => {
const pages = await getPagesPartOf(slug, filters);
if (pages.length === 0) {
return pages;
filters?: RepositoryFilters,
pagination?: Pagination,
): Promise<ContentResult> => {
const pagesResult = await getPagesPartOf(slug, filters);
if (pagesResult.pagination?.total === 0) {
return pagesResult;
}

return Promise.all(
Expand All @@ -36,30 +38,36 @@ export const getPagesPartOfRecursively = async (
};

export const getSamePartOfPages = async (
content: Content
): Promise<Content[]> => {
content: Content,
filters?: RepositoryFilters,
pagination?: Pagination,
): Promise<ContentResult> => {
if (!content.isPartOf) {
return [];
}

const relatedPages = await getPagesPartOf(content.isPartOf, {
inLanguage: content.inLanguage,
type: content.type,
...filters,
});
return relatedPages.filter((doc) => doc.identifier !== content.identifier);
};

export const getRelatedPages = async (
content: Content,
exclude: Content[] = []
): Promise<Content[]> => {
exclude: Content[] = [],
filters?: RepositoryFilters,
pagination?: Pagination,
): Promise<ContentResult> => {
if (!content.keywords) {
return [];
}

const pages = await getPages({
inLanguage: content.inLanguage,
type: content.type,
...filters,
});
const excludeIdentifiers = exclude.map((doc) => doc.identifier);
return pages
Expand All @@ -71,7 +79,7 @@ export const getRelatedPages = async (
.map((doc) => {
const commonKeywords = Array.isArray(doc.keywords)
? doc.keywords.filter((keyword) => content.keywords?.includes(keyword))
.length
.length
: 0;

return { doc, commonKeywords };
Expand All @@ -83,8 +91,9 @@ export const getRelatedPages = async (

export const getPagesWithKeywordIdentifier = async (
keywordIdentifier: string,
filters?: RepositoryFilters
): Promise<Content[]> =>
filters?: RepositoryFilters,
pagination?: Pagination,
): Promise<ContentResult> =>
(await getPages(filters)).filter((doc) =>
doc.keywords?.some(
(keyword) => createIdentifierFromString(keyword) === keywordIdentifier
Expand All @@ -93,15 +102,16 @@ export const getPagesWithKeywordIdentifier = async (

export const getPageByIdentifier = async (
identifier: string,
filters?: RepositoryFilters
filters?: RepositoryFilters,
) => documentByIdentifierSelector(await getPages(filters))(identifier);

export const getPageByURL = async (url: string) =>
(await getPages()).find((page) => page.url === url);

export const getTagPageByKeyword = async (
keyword: string,
filters?: RepositoryFilters
filters?: RepositoryFilters,
pagination?: Pagination,
) =>
(await getPages(filters)).filter(
(doc) =>
Expand Down Expand Up @@ -140,17 +150,18 @@ export const getIndexPage = async (): Promise<Content | undefined> => {

type ContentWithIsPartOf = Content & Required<Pick<Content, 'isPartOf'>>;

export const getSerieWorks = async (content: ContentWithIsPartOf) =>
(
await getPagesPartOf(content.isPartOf, {
type: content.type,
inLanguage: content.inLanguage,
})
)
export const getSerieWorks = async (content: ContentWithIsPartOf) => {
const serieWorksResult = await getPagesPartOf(content.isPartOf, {
type: content.type,
inLanguage: content.inLanguage,
})

return serieWorksResult.elements
.filter((w) => 'position' in w && typeof w.position === 'number')
.sort((a, b) => (a.position as number) - (b.position as number)) as Array<
Content & Required<Pick<Content, 'position'>>
>;
Content & Required<Pick<Content, 'position'>>
>;
}

export const getPreviousWorkSeries = async (
content: Content
Expand Down

0 comments on commit 7159f1c

Please sign in to comment.