mirror of
https://github.com/shlinkio/shlink-web-client.git
synced 2026-08-01 00:21:52 +00:00
Extract shlink-web-component outside of src folder
This commit is contained in:
144
shlink-web-component/visits/reducers/common.ts
Normal file
144
shlink-web-component/visits/reducers/common.ts
Normal file
@@ -0,0 +1,144 @@
|
||||
import { createAction, createSlice } from '@reduxjs/toolkit';
|
||||
import { flatten, prop, range, splitEvery } from 'ramda';
|
||||
import type { ShlinkState } from '../../../src/container/types';
|
||||
import type { DateInterval } from '../../../src/utils/helpers/dateIntervals';
|
||||
import { dateToMatchingInterval } from '../../../src/utils/helpers/dateIntervals';
|
||||
import { createAsyncThunk } from '../../../src/utils/helpers/redux';
|
||||
import type { ShlinkPaginator, ShlinkVisits, ShlinkVisitsParams } from '../../api-contract';
|
||||
import { parseApiError } from '../../api-contract/utils';
|
||||
import type { CreateVisit, Visit } from '../types';
|
||||
import type { LoadVisits, VisitsInfo, VisitsLoaded } from './types';
|
||||
import { createNewVisits } from './visitCreation';
|
||||
|
||||
const ITEMS_PER_PAGE = 5000;
|
||||
const PARALLEL_REQUESTS_COUNT = 4;
|
||||
const PARALLEL_STARTING_PAGE = 2;
|
||||
|
||||
const isLastPage = ({ currentPage, pagesCount }: ShlinkPaginator): boolean => currentPage >= pagesCount;
|
||||
const calcProgress = (total: number, current: number): number => (current * 100) / total;
|
||||
|
||||
type VisitsLoader = (page: number, itemsPerPage: number) => Promise<ShlinkVisits>;
|
||||
type LastVisitLoader = (excludeBots?: boolean) => Promise<Visit | undefined>;
|
||||
|
||||
interface VisitsAsyncThunkOptions<T extends LoadVisits = LoadVisits, R extends VisitsLoaded = VisitsLoaded> {
|
||||
typePrefix: string;
|
||||
createLoaders: (params: T) => [VisitsLoader, LastVisitLoader];
|
||||
getExtraFulfilledPayload: (params: T) => Partial<R>;
|
||||
shouldCancel: (getState: () => ShlinkState) => boolean;
|
||||
}
|
||||
|
||||
export const createVisitsAsyncThunk = <T extends LoadVisits = LoadVisits, R extends VisitsLoaded = VisitsLoaded>(
|
||||
{ typePrefix, createLoaders, getExtraFulfilledPayload, shouldCancel }: VisitsAsyncThunkOptions<T, R>,
|
||||
) => {
|
||||
const progressChanged = createAction<number>(`${typePrefix}/progressChanged`);
|
||||
const large = createAction<void>(`${typePrefix}/large`);
|
||||
const fallbackToInterval = createAction<DateInterval>(`${typePrefix}/fallbackToInterval`);
|
||||
|
||||
const asyncThunk = createAsyncThunk(typePrefix, async (params: T, { getState, dispatch }): Promise<Partial<R>> => {
|
||||
const [visitsLoader, lastVisitLoader] = createLoaders(params);
|
||||
|
||||
const loadVisitsInParallel = async (pages: number[]): Promise<Visit[]> =>
|
||||
Promise.all(pages.map(async (page) => visitsLoader(page, ITEMS_PER_PAGE).then(prop('data')))).then(flatten);
|
||||
|
||||
const loadPagesBlocks = async (pagesBlocks: number[][], index = 0): Promise<Visit[]> => {
|
||||
if (shouldCancel(getState)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const data = await loadVisitsInParallel(pagesBlocks[index]);
|
||||
|
||||
dispatch(progressChanged(calcProgress(pagesBlocks.length, index + PARALLEL_STARTING_PAGE)));
|
||||
|
||||
if (index < pagesBlocks.length - 1) {
|
||||
return data.concat(await loadPagesBlocks(pagesBlocks, index + 1));
|
||||
}
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
const loadVisits = async (page = 1) => {
|
||||
const { pagination, data } = await visitsLoader(page, ITEMS_PER_PAGE);
|
||||
|
||||
// If pagination was not returned, then this is an old shlink version. Just return data
|
||||
if (!pagination || isLastPage(pagination)) {
|
||||
return data;
|
||||
}
|
||||
|
||||
// If there are more pages, make requests in blocks of 4
|
||||
const pagesRange = range(PARALLEL_STARTING_PAGE, pagination.pagesCount + 1);
|
||||
const pagesBlocks = splitEvery(PARALLEL_REQUESTS_COUNT, pagesRange);
|
||||
|
||||
if (pagination.pagesCount - 1 > PARALLEL_REQUESTS_COUNT) {
|
||||
dispatch(large());
|
||||
}
|
||||
|
||||
return data.concat(await loadPagesBlocks(pagesBlocks));
|
||||
};
|
||||
|
||||
const [visits, lastVisit] = await Promise.all([loadVisits(), lastVisitLoader(params.query?.excludeBots)]);
|
||||
|
||||
if (!visits.length && lastVisit) {
|
||||
dispatch(fallbackToInterval(dateToMatchingInterval(lastVisit.date)));
|
||||
}
|
||||
|
||||
return { ...getExtraFulfilledPayload(params), visits };
|
||||
});
|
||||
|
||||
// Enhance the async thunk with extra actions
|
||||
return Object.assign(asyncThunk, { progressChanged, large, fallbackToInterval });
|
||||
};
|
||||
|
||||
export const lastVisitLoaderForLoader = (
|
||||
doIntervalFallback: boolean,
|
||||
loader: (params: ShlinkVisitsParams) => Promise<ShlinkVisits>,
|
||||
): LastVisitLoader => async (excludeBots?: boolean) => (
|
||||
!doIntervalFallback
|
||||
? Promise.resolve(undefined)
|
||||
: loader({ page: 1, itemsPerPage: 1, excludeBots }).then(({ data }) => data[0])
|
||||
);
|
||||
|
||||
interface VisitsReducerOptions<State extends VisitsInfo, AT extends ReturnType<typeof createVisitsAsyncThunk>> {
|
||||
name: string;
|
||||
asyncThunkCreator: AT;
|
||||
initialState: State;
|
||||
filterCreatedVisits: (state: State, createdVisits: CreateVisit[]) => CreateVisit[];
|
||||
}
|
||||
|
||||
export const createVisitsReducer = <State extends VisitsInfo, AT extends ReturnType<typeof createVisitsAsyncThunk>>(
|
||||
{ name, asyncThunkCreator, initialState, filterCreatedVisits }: VisitsReducerOptions<State, AT>,
|
||||
) => {
|
||||
const { pending, rejected, fulfilled, large, progressChanged, fallbackToInterval } = asyncThunkCreator;
|
||||
const { reducer, actions } = createSlice({
|
||||
name,
|
||||
initialState,
|
||||
reducers: {
|
||||
cancelGetVisits: (state) => ({ ...state, cancelLoad: true }),
|
||||
},
|
||||
extraReducers: (builder) => {
|
||||
builder.addCase(pending, () => ({ ...initialState, loading: true }));
|
||||
builder.addCase(rejected, (_, { error }) => (
|
||||
{ ...initialState, error: true, errorData: parseApiError(error) }
|
||||
));
|
||||
builder.addCase(fulfilled, (state, { payload }) => (
|
||||
{ ...state, ...payload, loading: false, loadingLarge: false, error: false }
|
||||
));
|
||||
|
||||
builder.addCase(large, (state) => ({ ...state, loadingLarge: true }));
|
||||
builder.addCase(progressChanged, (state, { payload: progress }) => ({ ...state, progress }));
|
||||
builder.addCase(fallbackToInterval, (state, { payload: fallbackInterval }) => (
|
||||
{ ...state, fallbackInterval }
|
||||
));
|
||||
|
||||
builder.addCase(createNewVisits, (state, { payload }) => {
|
||||
const { visits } = state;
|
||||
// @ts-expect-error TODO Fix type inference
|
||||
const newVisits = filterCreatedVisits(state, payload.createdVisits).map(({ visit }) => visit);
|
||||
|
||||
return !newVisits.length ? state : { ...state, visits: [...newVisits, ...visits] };
|
||||
});
|
||||
},
|
||||
});
|
||||
const { cancelGetVisits } = actions;
|
||||
|
||||
return { reducer, cancelGetVisits };
|
||||
};
|
||||
59
shlink-web-component/visits/reducers/domainVisits.ts
Normal file
59
shlink-web-component/visits/reducers/domainVisits.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import { isBetween } from '../../../src/utils/helpers/date';
|
||||
import type { ShlinkApiClient } from '../../api-contract';
|
||||
import { domainMatches } from '../../short-urls/helpers';
|
||||
import { createVisitsAsyncThunk, createVisitsReducer, lastVisitLoaderForLoader } from './common';
|
||||
import type { LoadVisits, VisitsInfo } from './types';
|
||||
|
||||
const REDUCER_PREFIX = 'shlink/domainVisits';
|
||||
|
||||
export const DEFAULT_DOMAIN = 'DEFAULT';
|
||||
|
||||
interface WithDomain {
|
||||
domain: string;
|
||||
}
|
||||
|
||||
export interface DomainVisits extends VisitsInfo, WithDomain {}
|
||||
|
||||
export interface LoadDomainVisits extends LoadVisits, WithDomain {}
|
||||
|
||||
const initialState: DomainVisits = {
|
||||
visits: [],
|
||||
domain: '',
|
||||
loading: false,
|
||||
loadingLarge: false,
|
||||
error: false,
|
||||
cancelLoad: false,
|
||||
progress: 0,
|
||||
};
|
||||
|
||||
export const getDomainVisits = (apiClient: ShlinkApiClient) => createVisitsAsyncThunk({
|
||||
typePrefix: `${REDUCER_PREFIX}/getDomainVisits`,
|
||||
createLoaders: ({ domain, query = {}, doIntervalFallback = false }: LoadDomainVisits) => {
|
||||
const { getDomainVisits: getVisits } = apiClient;
|
||||
const visitsLoader = async (page: number, itemsPerPage: number) => getVisits(
|
||||
domain,
|
||||
{ ...query, page, itemsPerPage },
|
||||
);
|
||||
const lastVisitLoader = lastVisitLoaderForLoader(doIntervalFallback, async (params) => getVisits(domain, params));
|
||||
|
||||
return [visitsLoader, lastVisitLoader];
|
||||
},
|
||||
getExtraFulfilledPayload: ({ domain, query = {} }: LoadDomainVisits) => ({ domain, query }),
|
||||
shouldCancel: (getState) => getState().domainVisits.cancelLoad,
|
||||
});
|
||||
|
||||
export const domainVisitsReducerCreator = (
|
||||
asyncThunkCreator: ReturnType<typeof getDomainVisits>,
|
||||
) => createVisitsReducer({
|
||||
name: REDUCER_PREFIX,
|
||||
initialState,
|
||||
// @ts-expect-error TODO Fix type inference
|
||||
asyncThunkCreator,
|
||||
filterCreatedVisits: ({ domain, query = {} }, createdVisits) => {
|
||||
const { startDate, endDate } = query;
|
||||
return createdVisits.filter(
|
||||
({ shortUrl, visit }) =>
|
||||
shortUrl && domainMatches(shortUrl, domain) && isBetween(visit.date, startDate, endDate),
|
||||
);
|
||||
},
|
||||
});
|
||||
41
shlink-web-component/visits/reducers/nonOrphanVisits.ts
Normal file
41
shlink-web-component/visits/reducers/nonOrphanVisits.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { isBetween } from '../../../src/utils/helpers/date';
|
||||
import type { ShlinkApiClient } from '../../api-contract';
|
||||
import { createVisitsAsyncThunk, createVisitsReducer, lastVisitLoaderForLoader } from './common';
|
||||
import type { VisitsInfo } from './types';
|
||||
|
||||
const REDUCER_PREFIX = 'shlink/orphanVisits';
|
||||
|
||||
const initialState: VisitsInfo = {
|
||||
visits: [],
|
||||
loading: false,
|
||||
loadingLarge: false,
|
||||
error: false,
|
||||
cancelLoad: false,
|
||||
progress: 0,
|
||||
};
|
||||
|
||||
export const getNonOrphanVisits = (apiClient: ShlinkApiClient) => createVisitsAsyncThunk({
|
||||
typePrefix: `${REDUCER_PREFIX}/getNonOrphanVisits`,
|
||||
createLoaders: ({ query = {}, doIntervalFallback = false }) => {
|
||||
const { getNonOrphanVisits: shlinkGetNonOrphanVisits } = apiClient;
|
||||
const visitsLoader = async (page: number, itemsPerPage: number) =>
|
||||
shlinkGetNonOrphanVisits({ ...query, page, itemsPerPage });
|
||||
const lastVisitLoader = lastVisitLoaderForLoader(doIntervalFallback, shlinkGetNonOrphanVisits);
|
||||
|
||||
return [visitsLoader, lastVisitLoader];
|
||||
},
|
||||
getExtraFulfilledPayload: ({ query = {} }) => ({ query }),
|
||||
shouldCancel: (getState) => getState().orphanVisits.cancelLoad,
|
||||
});
|
||||
|
||||
export const nonOrphanVisitsReducerCreator = (
|
||||
asyncThunkCreator: ReturnType<typeof getNonOrphanVisits>,
|
||||
) => createVisitsReducer({
|
||||
name: REDUCER_PREFIX,
|
||||
initialState,
|
||||
asyncThunkCreator,
|
||||
filterCreatedVisits: ({ query = {} }, createdVisits) => {
|
||||
const { startDate, endDate } = query;
|
||||
return createdVisits.filter(({ visit }) => isBetween(visit.date, startDate, endDate));
|
||||
},
|
||||
});
|
||||
53
shlink-web-component/visits/reducers/orphanVisits.ts
Normal file
53
shlink-web-component/visits/reducers/orphanVisits.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import { isBetween } from '../../../src/utils/helpers/date';
|
||||
import type { ShlinkApiClient } from '../../api-contract';
|
||||
import type { OrphanVisit, OrphanVisitType } from '../types';
|
||||
import { isOrphanVisit } from '../types/helpers';
|
||||
import { createVisitsAsyncThunk, createVisitsReducer, lastVisitLoaderForLoader } from './common';
|
||||
import type { LoadVisits, VisitsInfo } from './types';
|
||||
|
||||
const REDUCER_PREFIX = 'shlink/orphanVisits';
|
||||
|
||||
export interface LoadOrphanVisits extends LoadVisits {
|
||||
orphanVisitsType?: OrphanVisitType;
|
||||
}
|
||||
|
||||
const initialState: VisitsInfo = {
|
||||
visits: [],
|
||||
loading: false,
|
||||
loadingLarge: false,
|
||||
error: false,
|
||||
cancelLoad: false,
|
||||
progress: 0,
|
||||
};
|
||||
|
||||
const matchesType = (visit: OrphanVisit, orphanVisitsType?: OrphanVisitType) =>
|
||||
!orphanVisitsType || orphanVisitsType === visit.type;
|
||||
|
||||
export const getOrphanVisits = (apiClient: ShlinkApiClient) => createVisitsAsyncThunk({
|
||||
typePrefix: `${REDUCER_PREFIX}/getOrphanVisits`,
|
||||
createLoaders: ({ orphanVisitsType, query = {}, doIntervalFallback = false }: LoadOrphanVisits) => {
|
||||
const { getOrphanVisits: getVisits } = apiClient;
|
||||
const visitsLoader = async (page: number, itemsPerPage: number) => getVisits({ ...query, page, itemsPerPage })
|
||||
.then((result) => {
|
||||
const visits = result.data.filter((visit) => isOrphanVisit(visit) && matchesType(visit, orphanVisitsType));
|
||||
return { ...result, data: visits };
|
||||
});
|
||||
const lastVisitLoader = lastVisitLoaderForLoader(doIntervalFallback, getVisits);
|
||||
|
||||
return [visitsLoader, lastVisitLoader];
|
||||
},
|
||||
getExtraFulfilledPayload: ({ query = {} }: LoadOrphanVisits) => ({ query }),
|
||||
shouldCancel: (getState) => getState().orphanVisits.cancelLoad,
|
||||
});
|
||||
|
||||
export const orphanVisitsReducerCreator = (
|
||||
asyncThunkCreator: ReturnType<typeof getOrphanVisits>,
|
||||
) => createVisitsReducer({
|
||||
name: REDUCER_PREFIX,
|
||||
initialState,
|
||||
asyncThunkCreator,
|
||||
filterCreatedVisits: ({ query = {} }, createdVisits) => {
|
||||
const { startDate, endDate } = query;
|
||||
return createdVisits.filter(({ visit, shortUrl }) => !shortUrl && isBetween(visit.date, startDate, endDate));
|
||||
},
|
||||
});
|
||||
62
shlink-web-component/visits/reducers/shortUrlVisits.ts
Normal file
62
shlink-web-component/visits/reducers/shortUrlVisits.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import { isBetween } from '../../../src/utils/helpers/date';
|
||||
import type { ShlinkApiClient } from '../../api-contract';
|
||||
import type { ShortUrlIdentifier } from '../../short-urls/data';
|
||||
import { shortUrlMatches } from '../../short-urls/helpers';
|
||||
import { createVisitsAsyncThunk, createVisitsReducer, lastVisitLoaderForLoader } from './common';
|
||||
import type { LoadVisits, VisitsInfo } from './types';
|
||||
|
||||
const REDUCER_PREFIX = 'shlink/shortUrlVisits';
|
||||
|
||||
export interface ShortUrlVisits extends VisitsInfo, ShortUrlIdentifier {}
|
||||
|
||||
export interface LoadShortUrlVisits extends LoadVisits {
|
||||
shortCode: string;
|
||||
}
|
||||
|
||||
const initialState: ShortUrlVisits = {
|
||||
visits: [],
|
||||
shortCode: '',
|
||||
domain: undefined, // Deprecated. Value from query params can be used instead
|
||||
loading: false,
|
||||
loadingLarge: false,
|
||||
error: false,
|
||||
cancelLoad: false,
|
||||
progress: 0,
|
||||
};
|
||||
|
||||
export const getShortUrlVisits = (apiClient: ShlinkApiClient) => createVisitsAsyncThunk({
|
||||
typePrefix: `${REDUCER_PREFIX}/getShortUrlVisits`,
|
||||
createLoaders: ({ shortCode, query = {}, doIntervalFallback = false }: LoadShortUrlVisits) => {
|
||||
const { getShortUrlVisits: shlinkGetShortUrlVisits } = apiClient;
|
||||
const visitsLoader = async (page: number, itemsPerPage: number) => shlinkGetShortUrlVisits(
|
||||
shortCode,
|
||||
{ ...query, page, itemsPerPage },
|
||||
);
|
||||
const lastVisitLoader = lastVisitLoaderForLoader(
|
||||
doIntervalFallback,
|
||||
async (params) => shlinkGetShortUrlVisits(shortCode, { ...params, domain: query.domain }),
|
||||
);
|
||||
|
||||
return [visitsLoader, lastVisitLoader];
|
||||
},
|
||||
getExtraFulfilledPayload: ({ shortCode, query = {} }: LoadShortUrlVisits) => (
|
||||
{ shortCode, query, domain: query.domain }
|
||||
),
|
||||
shouldCancel: (getState) => getState().shortUrlVisits.cancelLoad,
|
||||
});
|
||||
|
||||
export const shortUrlVisitsReducerCreator = (
|
||||
asyncThunkCreator: ReturnType<typeof getShortUrlVisits>,
|
||||
) => createVisitsReducer({
|
||||
name: REDUCER_PREFIX,
|
||||
initialState,
|
||||
// @ts-expect-error TODO Fix type inference
|
||||
asyncThunkCreator,
|
||||
filterCreatedVisits: ({ shortCode, domain, query = {} }: ShortUrlVisits, createdVisits) => {
|
||||
const { startDate, endDate } = query;
|
||||
return createdVisits.filter(
|
||||
({ shortUrl, visit }) =>
|
||||
shortUrl && shortUrlMatches(shortUrl, shortCode, domain) && isBetween(visit.date, startDate, endDate),
|
||||
);
|
||||
},
|
||||
});
|
||||
53
shlink-web-component/visits/reducers/tagVisits.ts
Normal file
53
shlink-web-component/visits/reducers/tagVisits.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import { isBetween } from '../../../src/utils/helpers/date';
|
||||
import type { ShlinkApiClient } from '../../api-contract';
|
||||
import { createVisitsAsyncThunk, createVisitsReducer, lastVisitLoaderForLoader } from './common';
|
||||
import type { LoadVisits, VisitsInfo } from './types';
|
||||
|
||||
const REDUCER_PREFIX = 'shlink/tagVisits';
|
||||
|
||||
interface WithTag {
|
||||
tag: string;
|
||||
}
|
||||
|
||||
export interface TagVisits extends VisitsInfo, WithTag {}
|
||||
|
||||
export interface LoadTagVisits extends LoadVisits, WithTag {}
|
||||
|
||||
const initialState: TagVisits = {
|
||||
visits: [],
|
||||
tag: '',
|
||||
loading: false,
|
||||
loadingLarge: false,
|
||||
error: false,
|
||||
cancelLoad: false,
|
||||
progress: 0,
|
||||
};
|
||||
|
||||
export const getTagVisits = (apiClient: ShlinkApiClient) => createVisitsAsyncThunk({
|
||||
typePrefix: `${REDUCER_PREFIX}/getTagVisits`,
|
||||
createLoaders: ({ tag, query = {}, doIntervalFallback = false }: LoadTagVisits) => {
|
||||
const { getTagVisits: getVisits } = apiClient;
|
||||
const visitsLoader = async (page: number, itemsPerPage: number) => getVisits(
|
||||
tag,
|
||||
{ ...query, page, itemsPerPage },
|
||||
);
|
||||
const lastVisitLoader = lastVisitLoaderForLoader(doIntervalFallback, async (params) => getVisits(tag, params));
|
||||
|
||||
return [visitsLoader, lastVisitLoader];
|
||||
},
|
||||
getExtraFulfilledPayload: ({ tag, query = {} }: LoadTagVisits) => ({ tag, query }),
|
||||
shouldCancel: (getState) => getState().tagVisits.cancelLoad,
|
||||
});
|
||||
|
||||
export const tagVisitsReducerCreator = (asyncThunkCreator: ReturnType<typeof getTagVisits>) => createVisitsReducer({
|
||||
name: REDUCER_PREFIX,
|
||||
initialState,
|
||||
// @ts-expect-error TODO Fix type inference
|
||||
asyncThunkCreator,
|
||||
filterCreatedVisits: ({ tag, query = {} }: TagVisits, createdVisits) => {
|
||||
const { startDate, endDate } = query;
|
||||
return createdVisits.filter(
|
||||
({ shortUrl, visit }) => shortUrl?.tags.includes(tag) && isBetween(visit.date, startDate, endDate),
|
||||
);
|
||||
},
|
||||
});
|
||||
25
shlink-web-component/visits/reducers/types/index.ts
Normal file
25
shlink-web-component/visits/reducers/types/index.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import type { DateInterval } from '../../../../src/utils/helpers/dateIntervals';
|
||||
import type { ProblemDetailsError, ShlinkVisitsParams } from '../../../api-contract';
|
||||
import type { Visit } from '../../types';
|
||||
|
||||
export interface VisitsInfo {
|
||||
visits: Visit[];
|
||||
loading: boolean;
|
||||
loadingLarge: boolean;
|
||||
error: boolean;
|
||||
errorData?: ProblemDetailsError;
|
||||
progress: number;
|
||||
cancelLoad: boolean;
|
||||
query?: ShlinkVisitsParams;
|
||||
fallbackInterval?: DateInterval;
|
||||
}
|
||||
|
||||
export interface LoadVisits {
|
||||
query?: ShlinkVisitsParams;
|
||||
doIntervalFallback?: boolean;
|
||||
}
|
||||
|
||||
export type VisitsLoaded<T = {}> = T & {
|
||||
visits: Visit[];
|
||||
query?: ShlinkVisitsParams;
|
||||
};
|
||||
12
shlink-web-component/visits/reducers/visitCreation.ts
Normal file
12
shlink-web-component/visits/reducers/visitCreation.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import type { PayloadAction } from '@reduxjs/toolkit';
|
||||
import { createAction } from '@reduxjs/toolkit';
|
||||
import type { CreateVisit } from '../types';
|
||||
|
||||
export type CreateVisitsAction = PayloadAction<{
|
||||
createdVisits: CreateVisit[];
|
||||
}>;
|
||||
|
||||
export const createNewVisits = createAction(
|
||||
'shlink/visitCreation/createNewVisits',
|
||||
(createdVisits: CreateVisit[]) => ({ payload: { createdVisits } }),
|
||||
);
|
||||
99
shlink-web-component/visits/reducers/visitsOverview.ts
Normal file
99
shlink-web-component/visits/reducers/visitsOverview.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
import type { PayloadAction } from '@reduxjs/toolkit';
|
||||
import { createSlice } from '@reduxjs/toolkit';
|
||||
import { createAsyncThunk } from '../../../src/utils/helpers/redux';
|
||||
import type { ShlinkApiClient, ShlinkVisitsOverview } from '../../api-contract';
|
||||
import type { CreateVisit } from '../types';
|
||||
import { groupNewVisitsByType } from '../types/helpers';
|
||||
import { createNewVisits } from './visitCreation';
|
||||
|
||||
const REDUCER_PREFIX = 'shlink/visitsOverview';
|
||||
|
||||
export type PartialVisitsSummary = {
|
||||
total: number;
|
||||
nonBots?: number;
|
||||
bots?: number;
|
||||
};
|
||||
|
||||
export type ParsedVisitsOverview = {
|
||||
nonOrphanVisits: PartialVisitsSummary;
|
||||
orphanVisits: PartialVisitsSummary;
|
||||
};
|
||||
|
||||
export interface VisitsOverview extends ParsedVisitsOverview {
|
||||
loading: boolean;
|
||||
error: boolean;
|
||||
}
|
||||
|
||||
export type GetVisitsOverviewAction = PayloadAction<ShlinkVisitsOverview>;
|
||||
|
||||
const initialState: VisitsOverview = {
|
||||
nonOrphanVisits: {
|
||||
total: 0,
|
||||
},
|
||||
orphanVisits: {
|
||||
total: 0,
|
||||
},
|
||||
loading: false,
|
||||
error: false,
|
||||
};
|
||||
|
||||
const countBots = (visits: CreateVisit[]) => visits.filter(({ visit }) => visit.potentialBot).length;
|
||||
|
||||
export const loadVisitsOverview = (apiClient: ShlinkApiClient) => createAsyncThunk(
|
||||
`${REDUCER_PREFIX}/loadVisitsOverview`,
|
||||
(): Promise<ParsedVisitsOverview> => apiClient.getVisitsOverview().then(
|
||||
({ nonOrphanVisits, visitsCount, orphanVisits, orphanVisitsCount }) => ({
|
||||
nonOrphanVisits: {
|
||||
total: nonOrphanVisits?.total ?? visitsCount,
|
||||
nonBots: nonOrphanVisits?.nonBots,
|
||||
bots: nonOrphanVisits?.bots,
|
||||
},
|
||||
orphanVisits: {
|
||||
total: orphanVisits?.total ?? orphanVisitsCount,
|
||||
nonBots: orphanVisits?.nonBots,
|
||||
bots: orphanVisits?.bots,
|
||||
},
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
export const visitsOverviewReducerCreator = (
|
||||
loadVisitsOverviewThunk: ReturnType<typeof loadVisitsOverview>,
|
||||
) => createSlice({
|
||||
name: REDUCER_PREFIX,
|
||||
initialState,
|
||||
reducers: {},
|
||||
extraReducers: (builder) => {
|
||||
builder.addCase(loadVisitsOverviewThunk.pending, () => ({ ...initialState, loading: true }));
|
||||
builder.addCase(loadVisitsOverviewThunk.rejected, () => ({ ...initialState, error: true }));
|
||||
builder.addCase(loadVisitsOverviewThunk.fulfilled, (_, { payload }) => ({ ...initialState, ...payload }));
|
||||
|
||||
builder.addCase(createNewVisits, ({ nonOrphanVisits, orphanVisits, ...rest }, { payload }) => {
|
||||
const { nonOrphanVisits: newNonOrphanVisits, orphanVisits: newOrphanVisits } = groupNewVisitsByType(
|
||||
payload.createdVisits,
|
||||
);
|
||||
|
||||
const newNonOrphanTotalVisits = newNonOrphanVisits.length;
|
||||
const newNonOrphanBotVisits = countBots(newNonOrphanVisits);
|
||||
const newNonOrphanNonBotVisits = newNonOrphanTotalVisits - newNonOrphanBotVisits;
|
||||
|
||||
const newOrphanTotalVisits = newOrphanVisits.length;
|
||||
const newOrphanBotVisits = countBots(newOrphanVisits);
|
||||
const newOrphanNonBotVisits = newOrphanTotalVisits - newOrphanBotVisits;
|
||||
|
||||
return {
|
||||
...rest,
|
||||
nonOrphanVisits: {
|
||||
total: nonOrphanVisits.total + newNonOrphanTotalVisits,
|
||||
bots: nonOrphanVisits.bots && nonOrphanVisits.bots + newNonOrphanBotVisits,
|
||||
nonBots: nonOrphanVisits.nonBots && nonOrphanVisits.nonBots + newNonOrphanNonBotVisits,
|
||||
},
|
||||
orphanVisits: {
|
||||
total: orphanVisits.total + newOrphanTotalVisits,
|
||||
bots: orphanVisits.bots && orphanVisits.bots + newOrphanBotVisits,
|
||||
nonBots: orphanVisits.nonBots && orphanVisits.nonBots + newOrphanNonBotVisits,
|
||||
},
|
||||
};
|
||||
});
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user