Skip to content

Commit

Permalink
♻️(frontend) make document and video components generic
Browse files Browse the repository at this point in the history
The Document and Video components are almost identical in structure.
This refactor moves them into the core frontend package as generic
components, allowing for better reusability and maintainability.
  • Loading branch information
quitterie-lcs committed Nov 6, 2024
1 parent da32cf5 commit 8cda9bc
Show file tree
Hide file tree
Showing 45 changed files with 351 additions and 496 deletions.
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import React, { ReactNode } from "react";
import { DateRangePicker } from "@openfun/cunningham-react";
import dayjs from "dayjs";
import { useFilters } from "../../hooks";
import { useDateFilters } from "../../hooks";
import { formatDates, getDefaultDates } from "../../utils";

export type FiltersProps = {
export type DateFiltersProps = {
children: ReactNode;
};

Expand All @@ -14,8 +14,10 @@ export type FiltersProps = {
* @component
* @returns {JSX.Element} - The rendered Filters component with date range picker.
*/
export const Filters: React.FC<FiltersProps> = ({ children }: FiltersProps) => {
const { date, setDate } = useFilters();
export const DateFilters: React.FC<DateFiltersProps> = ({
children,
}: DateFiltersProps) => {
const { date, setDate } = useDateFilters();

const handleDateChange = (value: [string, string] | null): void => {
if (value) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import React from "react";
import { Select } from "@openfun/cunningham-react";
import { DateFilters } from "../DateFilters";
import { useResourceFilters } from "../../hooks";
import { Resource } from "../../types";

export type ResourceOption = {
value: string;
label: string;
};

// FIXME - refactor this part with the prototype of Experience Index.
export const RESOURCES: Array<Resource> = [
{ id: "uuid://0aecfa93-cef3-45ae-b7f5-a603e9e45f50", title: "Resource 1" },
{ id: "uuid://1c0c127a-f121-4bd1-8db6-918605c2645d", title: "Resource 2" },
{ id: "uuid://541dab6b-50ae-4444-b230-494f0621f132", title: "Resource 3" },
{ id: "uuid://69d32ad5-3af5-4160-a995-87e09da6865c", title: "Resource 4" },
{ id: "uuid://7d4f3c70-1e79-4243-9b7d-166076ce8bfb", title: "Resource 5" },
{ id: "uuid://8d386f48-3baa-4acf-8a46-0f2be4ae243e", title: "Resource 6" },
{ id: "uuid://b172ec09-97ec-4651-bc57-6eabebf47ed0", title: "Resource 7" },
{ id: "uuid://d613b564-5d18-4238-a69c-0fc8cee5d0e7", title: "Resource 8" },
{ id: "uuid://dd38149d-956a-483d-8975-c1506de1e1a9", title: "Resource 9" },
{ id: "uuid://e151ee65-7a72-478c-ac57-8a02f19e748b", title: "Resource 10" },
];

export type ResourceFiltersProps = {
label: string;
};
/**
* A React functional component for filtering documents and specifying a date range.
*
* This component provides user interface elements to select documents from a list,
* specify a date range, and trigger a data refresh.
* @param {label} string - The label name for the resources.
*
*/
export const ResourceFilters: React.FC<ResourceFiltersProps> = ({ label }) => {
const { setResources } = useResourceFilters();

const getResourceOptions = (): ResourceOption[] => {
return RESOURCES.map((item) => ({
value: item.id,
label: item.title,
}));
};

const handleResourceIdsChange = (
value: string | string[] | number | undefined,
): void => {
if (typeof value === "number") {
return;
}

setResources(RESOURCES.filter((resource) => value?.includes(resource.id)));
};

return (
<DateFilters>
<Select
label={label}
defaultValue={RESOURCES[0].id}
options={getResourceOptions()}
multi={true}
monoline={true}
onChange={(selectedValues) =>
handleResourceIdsChange(selectedValues.target.value)
}
/>
</DateFilters>
);
};
3 changes: 2 additions & 1 deletion src/frontend/packages/core/src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ export * from "./AppLayout";
export * from "./BreadCrumb";
export * from "./Card";
export * from "./Comparison";
export * from "./Filters";
export * from "./DateFilters";
export * from "./Footer";
export * from "./Header";
export * from "./Layout";
Expand All @@ -11,4 +11,5 @@ export * from "./Logo";
export * from "./LTI";
export * from "./Metrics";
export * from "./Plots";
export * from "./ResourceFilters";
export * from "./Tooltip";
3 changes: 2 additions & 1 deletion src/frontend/packages/core/src/contexts/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from "./ltiContext";
export * from "./filtersContext";
export * from "./jwtContext";
export * from "./dateFiltersContext";
export * from "./resourceFiltersContext";
29 changes: 29 additions & 0 deletions src/frontend/packages/core/src/contexts/resourceFiltersContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import React, {
createContext,
Dispatch,
SetStateAction,
useMemo,
useState,
} from "react";
import { Resource } from "../types";

export interface ResourceFiltersContextType {
resources: Array<Resource>;
setResources: Dispatch<SetStateAction<Array<Resource>>>;
}

export const ResourceFiltersContext =
createContext<ResourceFiltersContextType | null>(null);

export const ResourceFiltersProvider: React.FC<{ children: any }> = ({
children,
}) => {
const [resources, setResources] = useState<Array<Resource>>([]);
const value = useMemo(() => ({ resources, setResources }), [resources]);

return (
<ResourceFiltersContext.Provider value={value}>
{children}
</ResourceFiltersContext.Provider>
);
};
5 changes: 3 additions & 2 deletions src/frontend/packages/core/src/hooks/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export * from "./useFilters";
export * from "./useDateFilters";
export * from "./useJwtContext";
export * from "./useLTIContext";
export * from "./useRefreshToken";
export * from "./useResourceFilters";
export * from "./useTokenInterceptor";
export * from "./useJwtContext";
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useContext } from "react";
import { FiltersContext, FiltersContextType } from "../contexts";

export const useFilters = (): FiltersContextType => {
export const useDateFilters = (): FiltersContextType => {
const value = useContext(FiltersContext);
if (!value) {
throw new Error(`Missing wrapping Provider for Store FiltersContextType`);
Expand Down
15 changes: 15 additions & 0 deletions src/frontend/packages/core/src/hooks/useResourceFilters.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { useContext } from "react";
import {
ResourceFiltersContext,
ResourceFiltersContextType,
} from "../contexts";

export const useResourceFilters = (): ResourceFiltersContextType => {
const value = useContext(ResourceFiltersContext);
if (!value) {
throw new Error(
`Missing wrapping Provider for Store ResourceFiltersContextType`,
);
}
return value;
};
2 changes: 1 addition & 1 deletion src/frontend/packages/core/src/index.scss
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
@forward "components/BoundaryScreenError";
@forward "components/Card";
@forward "components/Comparison";
@forward "components/Filters";
@forward "components/DateFilters";
@forward "components/Header";
@forward "components/LoadingBarWrapper";
@forward "components/LTI/SelectContent";
Expand Down
28 changes: 28 additions & 0 deletions src/frontend/packages/core/src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,31 @@ export interface DecodedJwtLTI {
token_type: string;
user: DecodedJwtUserLTI;
}

export interface ResourceMetricsResponseItem {
date: string;
count: number;
}

export interface ResourceMetricsResponse {
id: string;
total: number;
counts: Array<ResourceMetricsResponseItem>;
}

export interface ResourceMetricsQueryParams {
since: string;
until: string;
unique?: boolean;
complete?: boolean;
}

export interface Resource {
id: string;
title: string;
}

export interface UseResourceMetricsReturn {
resourceMetrics: ResourceMetricsResponse[];
isFetching: boolean;
}
20 changes: 20 additions & 0 deletions src/frontend/packages/core/src/utils/getPreviousPeriod.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import dayjs from "dayjs";
import { formatDates } from "./formatDates";

/**
* Calculate the previous time period based on the provided since and until dates.
*
* This function calculates the start and end dates of the previous time period relative to the given 'since' and 'until' dates.
* It computes the duration of the selected period and calculates the previous period's start and end dates accordingly.
*
* @param {string} since - The start date of the current period.
* @param {string} until - The end date of the current period.
* @returns {string[]} An array of two ISO date strings representing the start and end dates of the previous time period.
*/
export const getPreviousPeriod = (since: string, until: string) => {
const periodDuration = dayjs(until).diff(since, "day") + 1;
return formatDates([
dayjs(since).subtract(periodDuration, "day"),
dayjs(since).subtract(1, "day"),
]);
};
2 changes: 2 additions & 0 deletions src/frontend/packages/core/src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,6 @@ export const WARREN_COLOR = "#312783";
export * from "./decodeJwtLTI";
export * from "./formatDates";
export * from "./getDefaultDates";
export * from "./getPreviousPeriod";
export * from "./parseDataContext";
export * from "./sumMetrics";
13 changes: 13 additions & 0 deletions src/frontend/packages/core/src/utils/sumMetrics.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { ResourceMetricsResponse } from "../types";

/**
* Calculate the total sum of downloads from an array of ResourceMetricsResponse objects.
* @param {ResourceMetricsResponse[]} metrics - An array of ResourceMetricsResponse objects.
* @returns {number} The total sum of downloads.
*/
export const sumMetrics = (metrics: ResourceMetricsResponse[]): number =>
metrics?.length
? metrics
.map((v) => v.total)
.reduce((previous: number, current: number) => previous + current)
: 0;
43 changes: 20 additions & 23 deletions src/frontend/packages/document/src/api/getDocumentDownloads.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { useQueries } from "@tanstack/react-query";
import { AxiosInstance } from "axios";
import { useTokenInterceptor, apiAxios } from "@openfun/warren-core";
import {
DocumentDownloadsQueryParams,
DocumentDownloadsResponse,
Document,
} from "../types";
useTokenInterceptor,
apiAxios,
UseResourceMetricsReturn,
ResourceMetricsQueryParams,
ResourceMetricsResponse,
Resource,
} from "@openfun/warren-core";

export const DEFAULT_BASE_QUERY_KEY = "documentDownloads";

Expand All @@ -14,14 +16,14 @@ export const DEFAULT_BASE_QUERY_KEY = "documentDownloads";
*
* @param {AxiosInstance} client - Axios instance for making the API request.
* @param {string} documentId - The ID of the document to fetch downloads for.
* @param {DocumentDownloadsQueryParams} queryParams - Optional filters for the request.
* @returns {Promise<DocumentDownloadsResponse>} A promise that resolves to the document downloads data.
* @param {ResourceMetricsQueryParams} queryParams - Optional filters for the request.
* @returns {Promise<ResourceMetricsResponse>} A promise that resolves to the document downloads data.
*/
const getDocumentDownloads = async (
client: AxiosInstance,
documentId: string,
queryParams: DocumentDownloadsQueryParams,
): Promise<DocumentDownloadsResponse> => {
queryParams: ResourceMetricsQueryParams,
): Promise<ResourceMetricsResponse> => {
const response = await client.get(`document/${documentId}/downloads`, {
params: Object.fromEntries(
Object.entries(queryParams).filter(([, value]) => !!value),
Expand All @@ -33,26 +35,21 @@ const getDocumentDownloads = async (
};
};

export type UseDocumentDownloadsReturn = {
documentDownloads: DocumentDownloadsResponse[];
isFetching: boolean;
};

/**
* A custom hook for fetching document downloads data for multiple documents in parallel.
*
* @param {Array<Document>} documents - An array of documents to fetch downloads for.
* @param {DocumentDownloadsQueryParams} queryParams - Optional filters for the requests.
* @param {Array<Resource>} documents - An array of documents to fetch downloads for.
* @param {ResourceMetricsQueryParams} queryParams - Optional filters for the requests.
* @param {boolean} wait - Optional flag to control the order of execution.
* @param {string} baseQueryKey - Optional base query key.
* @returns {UseDocumentDownloadsReturn} An object containing the fetched data and loading status.
* @returns {UseResourceMetricsReturn} An object containing the fetched data and loading status.
*/
export const useDocumentDownloads = (
documents: Array<Document>,
queryParams: DocumentDownloadsQueryParams,
documents: Array<Resource>,
queryParams: ResourceMetricsQueryParams,
wait: boolean = false,
baseQueryKey: string = DEFAULT_BASE_QUERY_KEY,
): UseDocumentDownloadsReturn => {
): UseResourceMetricsReturn => {
const { since, until, unique } = queryParams;

// Get the API client, set with the authorization headers and refresh mechanism
Expand All @@ -72,12 +69,12 @@ export const useDocumentDownloads = (
const isFetching = queryResults.some((r) => r.isFetching);

// Extract the data from the successful query results
const documentDownloads = queryResults
const resourceMetrics = queryResults
.filter((r) => r.isSuccess)
.map((queryResult) => queryResult.data) as DocumentDownloadsResponse[];
.map((queryResult) => queryResult.data) as ResourceMetricsResponse[];

return {
documentDownloads,
resourceMetrics,
isFetching,
};
};
Loading

0 comments on commit 8cda9bc

Please sign in to comment.