first commit
Some checks failed
Build / run (push) Has been cancelled

This commit is contained in:
maher
2025-10-29 11:42:25 +01:00
commit 703f50a09d
4595 changed files with 385164 additions and 0 deletions

View File

@@ -0,0 +1,43 @@
import {BackendResponse} from '@common/http/backend-response/backend-response';
import {useMutation} from '@tanstack/react-query';
import {apiClient, queryClient} from '@common/http/query-client';
import {toast} from '@common/ui/toast/toast';
import {message} from '@common/i18n/message';
import {useParams} from 'react-router-dom';
import {TitleTag} from '@app/admin/titles/requests/use-detach-title-tag';
import {UseFormReturn} from 'react-hook-form';
import {onFormQueryError} from '@common/errors/on-form-query-error';
interface Response extends BackendResponse {}
export interface AttachTitleTagPayload {
tag_name: string;
}
export function useAttachTitleTag(
form: UseFormReturn<AttachTitleTagPayload>,
tagType: TitleTag['model_type'],
) {
const {titleId} = useParams();
return useMutation({
mutationFn: (payload: AttachTitleTagPayload) =>
attachTag(titleId!, tagType, payload),
onSuccess: async () => {
await queryClient.invalidateQueries({
queryKey: ['titles', `${titleId}`],
});
toast(message('Tag attached'));
},
onError: r => onFormQueryError(r, form),
});
}
function attachTag(
titleId: number | string,
tagType: TitleTag['model_type'],
payload: AttachTitleTagPayload,
): Promise<Response> {
return apiClient
.post(`titles/${titleId}/tags/${tagType}`, payload)
.then(r => r.data);
}

View File

@@ -0,0 +1,33 @@
import {BackendResponse} from '@common/http/backend-response/backend-response';
import {useMutation} from '@tanstack/react-query';
import {apiClient, queryClient} from '@common/http/query-client';
import {showHttpErrorToast} from '@common/utils/http/show-http-error-toast';
import {titleSeasonsQueryKey} from '@app/titles/requests/use-title-seasons';
import {toast} from '@common/ui/toast/toast';
import {message} from '@common/i18n/message';
import {Season} from '@app/titles/models/season';
interface Response extends BackendResponse {
season: Season;
}
export function useCreateSeason(titleId: number) {
return useMutation({
mutationFn: () => createSeason(titleId),
onSuccess: async response => {
await queryClient.invalidateQueries({
queryKey: titleSeasonsQueryKey(response.season.title_id),
});
toast(
message('Season :number created', {
values: {number: response.season.number},
}),
);
},
onError: r => showHttpErrorToast(r),
});
}
function createSeason(titleId: number): Promise<Response> {
return apiClient.post(`titles/${titleId}/seasons`).then(r => r.data);
}

View File

@@ -0,0 +1,53 @@
import {BackendResponse} from '@common/http/backend-response/backend-response';
import {useMutation} from '@tanstack/react-query';
import {apiClient, queryClient} from '@common/http/query-client';
import {UseFormReturn} from 'react-hook-form';
import {onFormQueryError} from '@common/errors/on-form-query-error';
import {useParams} from 'react-router-dom';
import {toast} from '@common/ui/toast/toast';
import {message} from '@common/i18n/message';
import {titleCreditsQueryKey} from '@app/admin/titles/requests/use-title-credits';
interface Response extends BackendResponse {
//
}
export interface CreateTitleCreditPayload {
person_id: number;
character: string;
department: string;
job: string;
season?: number | string;
episode?: number | string;
}
export function useCreateTitleCredit(
form: UseFormReturn<CreateTitleCreditPayload>,
) {
const {titleId, season, episode} = useParams();
return useMutation({
mutationFn: (payload: CreateTitleCreditPayload) =>
createCredit(titleId!, season, episode, payload),
onSuccess: async () => {
await queryClient.invalidateQueries({
queryKey: titleCreditsQueryKey(titleId!),
});
toast(message('Credit added'));
},
onError: r => onFormQueryError(r, form),
});
}
function createCredit(
titleId: number | string,
season: number | string | undefined,
episode: number | string | undefined,
payload: CreateTitleCreditPayload,
): Promise<Response> {
payload = {
...payload,
season,
episode,
};
return apiClient.post(`titles/${titleId}/credits`, payload).then(r => r.data);
}

View File

@@ -0,0 +1,45 @@
import {BackendResponse} from '@common/http/backend-response/backend-response';
import {useMutation} from '@tanstack/react-query';
import {apiClient, queryClient} from '@common/http/query-client';
import {showHttpErrorToast} from '@common/utils/http/show-http-error-toast';
import {UseFormReturn} from 'react-hook-form';
import {onFormQueryError} from '@common/errors/on-form-query-error';
import {Title} from '@app/titles/models/title';
import {CreateVideoPayload} from '@app/admin/videos/requests/use-create-video';
interface Response extends BackendResponse {
title: Title;
}
export interface CreateTitlePayload {
name: string;
original_title: string;
is_series: boolean;
poster: string;
backdrop: string;
release_date: string;
tagline: string;
description: string;
runtime: number;
certification: string;
budget: number;
revenue: number;
language: string;
popularity: number;
images: {url: string}[];
videos: CreateVideoPayload[];
}
export function useCreateTitle(form: UseFormReturn<CreateTitlePayload>) {
return useMutation({
mutationFn: (payload: CreateTitlePayload) => createTitle(payload),
onSuccess: async () => {
await queryClient.invalidateQueries({queryKey: ['titles']});
},
onError: r => (form ? onFormQueryError(r, form) : showHttpErrorToast(r)),
});
}
function createTitle(payload: CreateTitlePayload): Promise<Response> {
return apiClient.post(`titles`, payload).then(r => r.data);
}

View File

@@ -0,0 +1,25 @@
import {BackendResponse} from '@common/http/backend-response/backend-response';
import {useMutation} from '@tanstack/react-query';
import {apiClient, queryClient} from '@common/http/query-client';
import {showHttpErrorToast} from '@common/utils/http/show-http-error-toast';
import {toast} from '@common/ui/toast/toast';
import {message} from '@common/i18n/message';
import {useParams} from 'react-router-dom';
interface Response extends BackendResponse {}
export function useDeleteImage(imageId: number | string) {
const {titleId} = useParams();
return useMutation({
mutationFn: () => deleteImage(imageId),
onSuccess: async () => {
await queryClient.invalidateQueries({queryKey: ['titles', `${titleId}`]});
toast(message('Image deleted'));
},
onError: r => showHttpErrorToast(r),
});
}
function deleteImage(imageId: number | string): Promise<Response> {
return apiClient.delete(`images/${imageId}`).then(r => r.data);
}

View File

@@ -0,0 +1,27 @@
import {BackendResponse} from '@common/http/backend-response/backend-response';
import {useMutation} from '@tanstack/react-query';
import {apiClient, queryClient} from '@common/http/query-client';
import {showHttpErrorToast} from '@common/utils/http/show-http-error-toast';
import {toast} from '@common/ui/toast/toast';
import {message} from '@common/i18n/message';
import {titleSeasonsQueryKey} from '@app/titles/requests/use-title-seasons';
import {Title} from '@app/titles/models/title';
interface Response extends BackendResponse {}
export function useDeleteSeason(title: Title, seasonId: number | string) {
return useMutation({
mutationFn: () => deleteSeason(seasonId),
onSuccess: async () => {
await queryClient.invalidateQueries({
queryKey: titleSeasonsQueryKey(title.id),
});
toast(message('Season deleted'));
},
onError: r => showHttpErrorToast(r),
});
}
function deleteSeason(seasonId: number | string): Promise<Response> {
return apiClient.delete(`seasons/${seasonId}`).then(r => r.data);
}

View File

@@ -0,0 +1,37 @@
import {BackendResponse} from '@common/http/backend-response/backend-response';
import {useMutation} from '@tanstack/react-query';
import {apiClient, queryClient} from '@common/http/query-client';
import {showHttpErrorToast} from '@common/utils/http/show-http-error-toast';
import {toast} from '@common/ui/toast/toast';
import {message} from '@common/i18n/message';
import {useParams} from 'react-router-dom';
import {titleCreditsQueryKey} from '@app/admin/titles/requests/use-title-credits';
interface Response extends BackendResponse {}
export function useDeleteTitleCredit(creditId: number) {
const {titleId, season, episode} = useParams();
return useMutation({
mutationFn: () => deleteCredit(titleId!, season, episode, creditId),
onSuccess: async () => {
await queryClient.invalidateQueries({
queryKey: titleCreditsQueryKey(titleId!),
});
toast(message('Credit deleted'));
},
onError: r => showHttpErrorToast(r),
});
}
function deleteCredit(
titleId: number | string,
season: string | undefined,
episode: string | undefined,
creditId: number | string,
): Promise<Response> {
return apiClient
.delete(`titles/${titleId}/credits/${creditId}`, {
params: {season, episode},
})
.then(r => r.data);
}

View File

@@ -0,0 +1,32 @@
import {BackendResponse} from '@common/http/backend-response/backend-response';
import {useMutation} from '@tanstack/react-query';
import {apiClient, queryClient} from '@common/http/query-client';
import {showHttpErrorToast} from '@common/utils/http/show-http-error-toast';
import {toast} from '@common/ui/toast/toast';
import {message} from '@common/i18n/message';
import {useParams} from 'react-router-dom';
import {Keyword} from '@app/titles/models/keyword';
import {Genre} from '@app/titles/models/genre';
import {ProductionCountry} from '@app/titles/models/production-country';
interface Response extends BackendResponse {}
export type TitleTag = Keyword | Genre | ProductionCountry;
export function useDetachTitleTag(tag: TitleTag) {
const {titleId} = useParams();
return useMutation({
mutationFn: () => detachTag(titleId!, tag),
onSuccess: async () => {
await queryClient.invalidateQueries({queryKey: ['titles', `${titleId}`]});
toast(message('Tag detached'));
},
onError: r => showHttpErrorToast(r),
});
}
function detachTag(titleId: number | string, tag: TitleTag): Promise<Response> {
return apiClient
.delete(`titles/${titleId}/tags/${tag.model_type}/${tag.id}`)
.then(r => r.data);
}

View File

@@ -0,0 +1,203 @@
import {useTrans} from '@common/i18n/use-trans';
import {ChipValue} from '@common/ui/forms/input-field/chip-field/chip-field';
import {useCallback, useRef, useState} from 'react';
import {apiClient, queryClient} from '@common/http/query-client';
import {showHttpErrorToast} from '@common/utils/http/show-http-error-toast';
import {BackendResponse} from '@common/http/backend-response/backend-response';
import {Title} from '@app/titles/models/title';
import {DatatableDataQueryKey} from '@common/datatable/requests/paginated-resources';
import {toast} from '@common/ui/toast/toast';
import {message} from '@common/i18n/message';
interface Response extends BackendResponse {
titles: Title[];
total_pages: number;
}
export interface ImportMultipleFromTmdbFormValue {
type: 'movie' | 'series';
country?: string;
language?: string;
min_rating?: string;
max_rating?: string;
genres?: ChipValue[];
keywords?: ChipValue[];
release_date?: {
start?: string;
end?: string;
};
pages_to_import?: number;
start_from_page?: number;
current_page?: number;
}
interface Payload
extends Omit<
ImportMultipleFromTmdbFormValue,
'genres' | 'keywords' | 'release_date'
> {
genres?: string;
keywords?: string;
start_date?: string;
end_date?: string;
}
export interface ImportMultipleProgressData {
totalItems: number;
currentItem: number;
progress: number;
titleList: string[];
}
interface MutateOptions {
onSuccess?: () => void;
onProgress?: (data: ImportMultipleProgressData) => void;
}
export function useImportMultipleFromTmdb() {
const {trans} = useTrans();
const titlesList = useRef<string[]>([]);
const [isLoading, setIsLoading] = useState(false);
const controller = useRef(new AbortController());
const cancel = useCallback(() => {
controller.current.abort('canceled');
}, []);
const handler = useCallback(
async (v: ImportMultipleFromTmdbFormValue, options: MutateOptions) => {
let stopped = false;
let error = false;
let pagesToImport = v.pages_to_import ? +v.pages_to_import : 1;
const startFromPage = v.start_from_page ? +v.start_from_page : 1;
if (pagesToImport + startFromPage > 500) {
pagesToImport = 500 - startFromPage;
}
const stopImporting = () => {
setIsLoading(false);
titlesList.current = [];
controller.current = new AbortController();
stopped = true;
};
let currentPage = startFromPage;
setIsLoading(true);
controller.current.signal.addEventListener('abort', () =>
stopImporting(),
);
let index = 0;
while (index <= pagesToImport && !stopped) {
// open progress bar instantly, instead of waiting for first response to come back
if (index === 0) {
options.onProgress?.({
totalItems: pagesToImport * 20,
currentItem: 0,
progress: 0,
titleList: [],
});
}
index++;
currentPage++;
try {
const response = await apiClient
.post<Response>(
'tmdb/import',
formValueToPayload({...v, current_page: currentPage}),
{
signal: controller.current.signal,
},
)
.then(r => r.data);
if (response.total_pages < pagesToImport) {
pagesToImport = response.total_pages;
}
// limit array to 1000 items
if (titlesList.current.length > 1000) {
titlesList.current = titlesList.current.slice(0, 1000);
}
titlesList.current.unshift(...response.titles.map(t => t.name));
const totalItems = pagesToImport * 20;
const currentItem = (index - 1) * 20;
options.onProgress?.({
totalItems: totalItems,
currentItem: currentItem,
progress: Math.round((currentItem / totalItems) * 100),
titleList: titlesList.current,
});
} catch (e) {
stopImporting();
error = true;
if ((e as any).message !== 'canceled') {
console.error(e);
showHttpErrorToast(e);
}
}
}
if (!error) {
await queryClient.invalidateQueries({
queryKey: DatatableDataQueryKey('titles'),
});
toast(trans(message('Titles imported')));
setIsLoading(false);
options.onSuccess?.();
}
},
[trans],
);
return {
mutate: handler,
cancel,
isLoading,
};
}
function formValueToPayload(values: ImportMultipleFromTmdbFormValue): Payload {
const payload: Payload = {
type: values.type,
pages_to_import: values.pages_to_import,
start_from_page: values.start_from_page,
current_page: values.current_page,
};
if (values.country) {
payload.country = values.country;
}
if (values.language) {
payload.language = values.language;
}
if (values.min_rating) {
payload.min_rating = values.min_rating;
}
if (values.max_rating) {
payload.max_rating = values.max_rating;
}
if (values.genres) {
payload.genres = values.genres.map(genre => genre.id).join(',');
}
if (values.keywords) {
payload.keywords = values.keywords.map(keyword => keyword.id).join(',');
}
if (values.release_date) {
payload.start_date = values.release_date.start;
payload.end_date = values.release_date.start;
}
return payload;
}

View File

@@ -0,0 +1,37 @@
import {useMutation} from '@tanstack/react-query';
import {apiClient, queryClient} from '@common/http/query-client';
import {useTrans} from '@common/i18n/use-trans';
import {BackendResponse} from '@common/http/backend-response/backend-response';
import {toast} from '@common/ui/toast/toast';
import {message} from '@common/i18n/message';
import {DatatableDataQueryKey} from '@common/datatable/requests/paginated-resources';
import {showHttpErrorToast} from '@common/utils/http/show-http-error-toast';
import {Title} from '@app/titles/models/title';
import {Person} from '@app/titles/models/person';
interface Response extends BackendResponse {
mediaItem: Title | Person;
}
export interface ImportMediaItemPayload {
tmdb_id: string;
media_type: 'movie' | 'series' | 'person';
}
export function useImportSingleFromTmdb() {
const {trans} = useTrans();
return useMutation({
mutationFn: (props: ImportMediaItemPayload) => importMediaItem(props),
onSuccess: async () => {
await queryClient.invalidateQueries({
queryKey: DatatableDataQueryKey('titles'),
});
toast(trans(message('Item imported')));
},
onError: err => showHttpErrorToast(err),
});
}
function importMediaItem(payload: ImportMediaItemPayload): Promise<Response> {
return apiClient.post('media/import', payload).then(r => r.data);
}

View File

@@ -0,0 +1,34 @@
import {BackendResponse} from '@common/http/backend-response/backend-response';
import {useMutation} from '@tanstack/react-query';
import {apiClient, queryClient} from '@common/http/query-client';
import {useParams} from 'react-router-dom';
import {toast} from '@common/ui/toast/toast';
import {message} from '@common/i18n/message';
import {titleCreditsQueryKey} from '@app/admin/titles/requests/use-title-credits';
import {showHttpErrorToast} from '@common/utils/http/show-http-error-toast';
interface Response extends BackendResponse {
//
}
interface Payload {
ids: number[];
}
export function useSortTitleCredits() {
const {titleId} = useParams();
return useMutation({
mutationFn: (payload: Payload) => sortCredits(payload),
onSuccess: async () => {
await queryClient.invalidateQueries({
queryKey: titleCreditsQueryKey(titleId!),
});
toast(message('Credit added'));
},
onError: r => showHttpErrorToast(r),
});
}
function sortCredits(payload: Payload): Promise<Response> {
return apiClient.post(`titles/credits/reorder`, payload).then(r => r.data);
}

View File

@@ -0,0 +1,41 @@
import {useInfiniteData} from '@common/ui/infinite-scroll/use-infinite-data';
import {TitleCredit} from '@app/titles/models/title';
import {useParams} from 'react-router-dom';
export const titleCreditsQueryKey = (
titleId: number | string,
season?: number | string,
episode?: number | string,
params?: any
) => {
const key = ['titles', `${titleId}`, 'credits'];
if (season) {
key.push('season', `${season}`);
}
if (episode) {
key.push('episode', `${episode}`);
}
if (params) {
key.push(params);
}
return key;
};
interface Params {
department?: string;
crewOnly?: string;
}
export function useTitleCredits(params: Params = {}) {
const {titleId, season, episode} = useParams();
return useInfiniteData<TitleCredit>({
endpoint: `titles/${titleId}/credits`,
queryKey: titleCreditsQueryKey(titleId!, season, episode, params),
queryParams: {
...params,
perPage: 30,
season: season || '',
episode: episode || '',
},
});
}

View File

@@ -0,0 +1,52 @@
import {BackendResponse} from '@common/http/backend-response/backend-response';
import {useMutation} from '@tanstack/react-query';
import {apiClient, queryClient} from '@common/http/query-client';
import {UseFormReturn} from 'react-hook-form';
import {onFormQueryError} from '@common/errors/on-form-query-error';
import {useParams} from 'react-router-dom';
import {toast} from '@common/ui/toast/toast';
import {message} from '@common/i18n/message';
import {titleCreditsQueryKey} from '@app/admin/titles/requests/use-title-credits';
import {CreateTitleCreditPayload} from '@app/admin/titles/requests/use-create-title-credit';
interface Response extends BackendResponse {
//
}
export interface UpdateTitleCreditPayload
extends Omit<CreateTitleCreditPayload, 'person_id'> {}
export function useUpdateTitleCredit(
form: UseFormReturn<UpdateTitleCreditPayload>,
creditId: number,
) {
const {titleId, season, episode} = useParams();
return useMutation({
mutationFn: (payload: UpdateTitleCreditPayload) =>
updateTitle(titleId!, season, episode, creditId, payload),
onSuccess: async () => {
await queryClient.invalidateQueries({
queryKey: titleCreditsQueryKey(titleId!),
});
toast(message('Credit updated'));
},
onError: r => onFormQueryError(r, form),
});
}
function updateTitle(
titleId: string,
season: string | undefined,
episode: string | undefined,
creditId: number,
payload: UpdateTitleCreditPayload,
): Promise<Response> {
payload = {
...payload,
season,
episode,
};
return apiClient
.put(`titles/${titleId}/credits/${creditId}`, payload)
.then(r => r.data);
}

View File

@@ -0,0 +1,30 @@
import {BackendResponse} from '@common/http/backend-response/backend-response';
import {useMutation} from '@tanstack/react-query';
import {apiClient, queryClient} from '@common/http/query-client';
import {UseFormReturn} from 'react-hook-form';
import {onFormQueryError} from '@common/errors/on-form-query-error';
import {useParams} from 'react-router-dom';
import {Title} from '@app/titles/models/title';
import {CreateTitlePayload} from '@app/admin/titles/requests/use-create-title';
interface Response extends BackendResponse {
title: Title;
}
export function useUpdateTitle(form: UseFormReturn<CreateTitlePayload>) {
const {titleId} = useParams();
return useMutation({
mutationFn: (payload: CreateTitlePayload) => updateTitle(titleId!, payload),
onSuccess: async () => {
await queryClient.invalidateQueries({queryKey: ['titles']});
},
onError: r => onFormQueryError(r, form),
});
}
function updateTitle(
titleId: string,
payload: CreateTitlePayload,
): Promise<Response> {
return apiClient.put(`titles/${titleId}`, payload).then(r => r.data);
}

View File

@@ -0,0 +1,36 @@
import {BackendResponse} from '@common/http/backend-response/backend-response';
import {useMutation} from '@tanstack/react-query';
import {apiClient, queryClient} from '@common/http/query-client';
import {showHttpErrorToast} from '@common/utils/http/show-http-error-toast';
import {TitleImage} from '@app/titles/models/title-image';
import {toast} from '@common/ui/toast/toast';
import {message} from '@common/i18n/message';
import {useParams} from 'react-router-dom';
interface Response extends BackendResponse {
image: TitleImage;
}
interface Payload {
titleId: number | string;
file: File;
}
export function useUploadImage() {
const {titleId} = useParams();
return useMutation({
mutationFn: (payload: Payload) => uploadImage(payload),
onSuccess: async () => {
await queryClient.invalidateQueries({queryKey: ['titles', `${titleId}`]});
toast(message('Image uploaded'));
},
onError: r => showHttpErrorToast(r),
});
}
function uploadImage(payload: Payload): Promise<Response> {
const formData = new FormData();
formData.append('titleId', payload.titleId.toString());
formData.append('file', payload.file);
return apiClient.post(`images`, formData).then(r => r.data);
}