mirror of
https://github.com/C9Glax/tranga-website.git
synced 2025-09-10 20:08:19 +02:00
Custom loadanimations for custom components
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
import { Button, CircularProgress } from '@mui/joy'
|
import { Button } from '@mui/joy'
|
||||||
import TProps, { TColor, TDisabled, TState } from './TProps.ts'
|
import TProps, { TColor, TDisabled, TState } from './TProps.ts'
|
||||||
import { MouseEventHandler, ReactNode, useState } from 'react'
|
import { MouseEventHandler, ReactNode, useState } from 'react'
|
||||||
|
|
||||||
@@ -21,7 +21,7 @@ export default function TButton(props: TButtonProps) {
|
|||||||
disabled={props.disabled ?? TDisabled(state)}
|
disabled={props.disabled ?? TDisabled(state)}
|
||||||
aria-disabled={props.disabled ?? TDisabled(state)}
|
aria-disabled={props.disabled ?? TDisabled(state)}
|
||||||
onClick={clicked}
|
onClick={clicked}
|
||||||
startDecorator={TDisabled(state) ? <CircularProgress /> : null}
|
className={'t-loadable'}
|
||||||
>
|
>
|
||||||
{props.children}
|
{props.children}
|
||||||
</Button>
|
</Button>
|
||||||
|
@@ -1,7 +1,8 @@
|
|||||||
import { Button, CircularProgress, Input } from '@mui/joy'
|
import { Button, Input } from '@mui/joy'
|
||||||
import { MouseEventHandler, useEffect, useState } from 'react'
|
import { MouseEventHandler, useEffect, useState } from 'react'
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import TProps, { TColor, TDisabled, TState } from './TProps.ts'
|
import TProps, { TColor, TDisabled, TState } from './TProps.ts'
|
||||||
|
import './loadingBorder.css'
|
||||||
|
|
||||||
export default function TInput(props: TInputProps) {
|
export default function TInput(props: TInputProps) {
|
||||||
const [state, setState] = useState<TState>(TState.clean)
|
const [state, setState] = useState<TState>(TState.clean)
|
||||||
@@ -63,7 +64,7 @@ export default function TInput(props: TInputProps) {
|
|||||||
value={value}
|
value={value}
|
||||||
onChange={inputValueChanged}
|
onChange={inputValueChanged}
|
||||||
onKeyDown={keyDownHandler}
|
onKeyDown={keyDownHandler}
|
||||||
startDecorator={TDisabled(state) ? <CircularProgress /> : null}
|
className={'t-loadable'}
|
||||||
endDecorator={
|
endDecorator={
|
||||||
props.submitButtonHidden ? null : (
|
props.submitButtonHidden ? null : (
|
||||||
<Button
|
<Button
|
||||||
|
56
tranga-website/src/Components/Inputs/loadingBorder.css
Normal file
56
tranga-website/src/Components/Inputs/loadingBorder.css
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
*,
|
||||||
|
*::before,
|
||||||
|
*::after {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes rotate {
|
||||||
|
100% {
|
||||||
|
transform: rotate(1turn);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.t-loadable {
|
||||||
|
position: relative;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute !important;
|
||||||
|
z-index: -2 !important;
|
||||||
|
transform-origin: 50% 50%;
|
||||||
|
left: -100vw !important;
|
||||||
|
top: -100vh !important;
|
||||||
|
width: 200vw !important;
|
||||||
|
height: 200vh !important;
|
||||||
|
background-repeat: no-repeat !important;
|
||||||
|
background-image: linear-gradient(
|
||||||
|
var(--joy-palette-primary-200),
|
||||||
|
var(--joy-palette-primary-200)
|
||||||
|
);
|
||||||
|
animation: rotate 4s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
z-index: -1;
|
||||||
|
left: 2px;
|
||||||
|
top: 2px;
|
||||||
|
width: calc(100% - 4px);
|
||||||
|
height: calc(100% - 4px);
|
||||||
|
background: black;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.t-loadable[aria-disabled='true'] {
|
||||||
|
&::before {
|
||||||
|
background-image: linear-gradient(
|
||||||
|
var(--joy-palette-danger-300),
|
||||||
|
var(--joy-palette-success-600)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
1411
tranga-website/src/api/V2.ts
Normal file
1411
tranga-website/src/api/V2.ts
Normal file
File diff suppressed because it is too large
Load Diff
405
tranga-website/src/api/data-contracts.ts
Normal file
405
tranga-website/src/api/data-contracts.ts
Normal file
@@ -0,0 +1,405 @@
|
|||||||
|
/* eslint-disable */
|
||||||
|
/* tslint:disable */
|
||||||
|
// @ts-nocheck
|
||||||
|
/*
|
||||||
|
* ---------------------------------------------------------------
|
||||||
|
* ## THIS FILE WAS GENERATED VIA SWAGGER-TYPESCRIPT-API ##
|
||||||
|
* ## ##
|
||||||
|
* ## AUTHOR: acacode ##
|
||||||
|
* ## SOURCE: https://github.com/acacode/swagger-typescript-api ##
|
||||||
|
* ---------------------------------------------------------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
export enum WorkerExecutionState {
|
||||||
|
Failed = 'Failed',
|
||||||
|
Cancelled = 'Cancelled',
|
||||||
|
Created = 'Created',
|
||||||
|
Waiting = 'Waiting',
|
||||||
|
Running = 'Running',
|
||||||
|
Completed = 'Completed',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum RequestType {
|
||||||
|
Default = 'Default',
|
||||||
|
MangaDexFeed = 'MangaDexFeed',
|
||||||
|
MangaImage = 'MangaImage',
|
||||||
|
MangaCover = 'MangaCover',
|
||||||
|
MangaDexImage = 'MangaDexImage',
|
||||||
|
MangaInfo = 'MangaInfo',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum MangaReleaseStatus {
|
||||||
|
Continuing = 'Continuing',
|
||||||
|
Completed = 'Completed',
|
||||||
|
OnHiatus = 'OnHiatus',
|
||||||
|
Cancelled = 'Cancelled',
|
||||||
|
Unreleased = 'Unreleased',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum LibraryType {
|
||||||
|
Komga = 'Komga',
|
||||||
|
Kavita = 'Kavita',
|
||||||
|
}
|
||||||
|
|
||||||
|
/** API.Schema.MangaContext.AltTitle DTO */
|
||||||
|
export interface AltTitle {
|
||||||
|
/**
|
||||||
|
* Language of the Title
|
||||||
|
* @minLength 1
|
||||||
|
*/
|
||||||
|
language: string
|
||||||
|
/**
|
||||||
|
* Title
|
||||||
|
* @minLength 1
|
||||||
|
*/
|
||||||
|
title: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The API.Schema.MangaContext.Author DTO */
|
||||||
|
export interface Author {
|
||||||
|
/**
|
||||||
|
* Name of the Author.
|
||||||
|
* @minLength 1
|
||||||
|
*/
|
||||||
|
name: string
|
||||||
|
/**
|
||||||
|
* Unique Identifier of the DTO
|
||||||
|
* @minLength 16
|
||||||
|
* @maxLength 64
|
||||||
|
*/
|
||||||
|
key: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/** API.Schema.MangaContext.Chapter DTO */
|
||||||
|
export interface Chapter {
|
||||||
|
/**
|
||||||
|
* Identifier of the Manga this Chapter belongs to
|
||||||
|
* @minLength 1
|
||||||
|
*/
|
||||||
|
mangaId: string
|
||||||
|
/**
|
||||||
|
* Volume number
|
||||||
|
* @format int32
|
||||||
|
*/
|
||||||
|
volume: number
|
||||||
|
/**
|
||||||
|
* Chapter number
|
||||||
|
* @minLength 1
|
||||||
|
*/
|
||||||
|
chapterNumber: string
|
||||||
|
/**
|
||||||
|
* Title of the Chapter
|
||||||
|
* @minLength 1
|
||||||
|
*/
|
||||||
|
title: string
|
||||||
|
/** Whether Chapter is Downloaded (on disk) */
|
||||||
|
downloaded: boolean
|
||||||
|
/** Ids of the Manga on MangaConnectors */
|
||||||
|
mangaConnectorIds: MangaConnectorId[]
|
||||||
|
/**
|
||||||
|
* Unique Identifier of the DTO
|
||||||
|
* @minLength 16
|
||||||
|
* @maxLength 64
|
||||||
|
*/
|
||||||
|
key: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FileLibrary {
|
||||||
|
/**
|
||||||
|
* @minLength 0
|
||||||
|
* @maxLength 256
|
||||||
|
*/
|
||||||
|
basePath: string
|
||||||
|
/**
|
||||||
|
* @minLength 0
|
||||||
|
* @maxLength 512
|
||||||
|
*/
|
||||||
|
libraryName: string
|
||||||
|
/**
|
||||||
|
* @minLength 16
|
||||||
|
* @maxLength 64
|
||||||
|
*/
|
||||||
|
key: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GotifyRecord {
|
||||||
|
name?: string | null
|
||||||
|
endpoint?: string | null
|
||||||
|
appToken?: string | null
|
||||||
|
/** @format int32 */
|
||||||
|
priority?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LibraryConnector {
|
||||||
|
libraryType: LibraryType
|
||||||
|
/**
|
||||||
|
* @format uri
|
||||||
|
* @minLength 0
|
||||||
|
* @maxLength 256
|
||||||
|
*/
|
||||||
|
baseUrl: string
|
||||||
|
/**
|
||||||
|
* @minLength 0
|
||||||
|
* @maxLength 256
|
||||||
|
*/
|
||||||
|
auth: string
|
||||||
|
/**
|
||||||
|
* @minLength 16
|
||||||
|
* @maxLength 64
|
||||||
|
*/
|
||||||
|
key: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/** API.Schema.MangaContext.Link DTO */
|
||||||
|
export interface Link {
|
||||||
|
/**
|
||||||
|
* Name of the Provider
|
||||||
|
* @minLength 1
|
||||||
|
*/
|
||||||
|
provider: string
|
||||||
|
/**
|
||||||
|
* Url
|
||||||
|
* @minLength 1
|
||||||
|
*/
|
||||||
|
url: string
|
||||||
|
/**
|
||||||
|
* Unique Identifier of the DTO
|
||||||
|
* @minLength 16
|
||||||
|
* @maxLength 64
|
||||||
|
*/
|
||||||
|
key: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/** API.Schema.MangaContext.Manga DTO */
|
||||||
|
export interface Manga {
|
||||||
|
/**
|
||||||
|
* Chapter cutoff for Downloads (Chapters before this will not be downloaded)
|
||||||
|
* @format float
|
||||||
|
*/
|
||||||
|
ignoreChaptersBefore: number
|
||||||
|
/**
|
||||||
|
* Release Year
|
||||||
|
* @format int32
|
||||||
|
*/
|
||||||
|
year?: number | null
|
||||||
|
/** Release Language */
|
||||||
|
originalLanguage?: string | null
|
||||||
|
/** Keys of ChapterDTOs */
|
||||||
|
chapterIds: string[]
|
||||||
|
/** Author-names */
|
||||||
|
authors: Author[]
|
||||||
|
/** Manga Tags */
|
||||||
|
tags: string[]
|
||||||
|
/** Links for more Metadata */
|
||||||
|
links: Link[]
|
||||||
|
/** Alt Titles of Manga */
|
||||||
|
altTitles: AltTitle[]
|
||||||
|
/**
|
||||||
|
* Name of the Manga
|
||||||
|
* @minLength 1
|
||||||
|
*/
|
||||||
|
name: string
|
||||||
|
/**
|
||||||
|
* Description of the Manga
|
||||||
|
* @minLength 1
|
||||||
|
*/
|
||||||
|
description: string
|
||||||
|
releaseStatus: MangaReleaseStatus
|
||||||
|
/** Ids of the Manga on MangaConnectors */
|
||||||
|
mangaConnectorIds: MangaConnectorId[]
|
||||||
|
/**
|
||||||
|
* Unique Identifier of the DTO
|
||||||
|
* @minLength 16
|
||||||
|
* @maxLength 64
|
||||||
|
*/
|
||||||
|
key: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MangaConnector {
|
||||||
|
name?: string | null
|
||||||
|
/** Whether Connector is used for Searches and Downloads */
|
||||||
|
enabled: boolean
|
||||||
|
/** Languages supported by the Connector */
|
||||||
|
supportedLanguages: string[]
|
||||||
|
/**
|
||||||
|
* Url of the Website Icon
|
||||||
|
* @minLength 1
|
||||||
|
*/
|
||||||
|
iconUrl: string
|
||||||
|
/**
|
||||||
|
* Unique Identifier of the DTO
|
||||||
|
* @minLength 16
|
||||||
|
* @maxLength 64
|
||||||
|
*/
|
||||||
|
key: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/** API.Schema.MangaContext.MangaConnectorId`1 DTO */
|
||||||
|
export interface MangaConnectorId {
|
||||||
|
/**
|
||||||
|
* Name of the Connector
|
||||||
|
* @minLength 1
|
||||||
|
*/
|
||||||
|
mangaConnectorName: string
|
||||||
|
/**
|
||||||
|
* Key of the referenced DTO
|
||||||
|
* @minLength 1
|
||||||
|
*/
|
||||||
|
foreignKey: string
|
||||||
|
/** Website Link for reference, if any */
|
||||||
|
websiteUrl?: string | null
|
||||||
|
/** Whether this Link is used for downloads */
|
||||||
|
useForDownload: boolean
|
||||||
|
/**
|
||||||
|
* Unique Identifier of the DTO
|
||||||
|
* @minLength 16
|
||||||
|
* @maxLength 64
|
||||||
|
*/
|
||||||
|
key: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MetadataEntry {
|
||||||
|
mangaId?: string | null
|
||||||
|
metadataFetcherName?: string | null
|
||||||
|
identifier?: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MetadataSearchResult {
|
||||||
|
identifier?: string | null
|
||||||
|
name?: string | null
|
||||||
|
url?: string | null
|
||||||
|
description?: string | null
|
||||||
|
coverUrl?: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Shortened Version of API.Controllers.DTOs.Manga */
|
||||||
|
export interface MinimalManga {
|
||||||
|
/**
|
||||||
|
* Name of the Manga
|
||||||
|
* @minLength 1
|
||||||
|
*/
|
||||||
|
name: string
|
||||||
|
/**
|
||||||
|
* Description of the Manga
|
||||||
|
* @minLength 1
|
||||||
|
*/
|
||||||
|
description: string
|
||||||
|
releaseStatus: MangaReleaseStatus
|
||||||
|
/** Ids of the Manga on MangaConnectors */
|
||||||
|
mangaConnectorIds: MangaConnectorId[]
|
||||||
|
/**
|
||||||
|
* Unique Identifier of the DTO
|
||||||
|
* @minLength 16
|
||||||
|
* @maxLength 64
|
||||||
|
*/
|
||||||
|
key: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NotificationConnector {
|
||||||
|
/**
|
||||||
|
* @minLength 0
|
||||||
|
* @maxLength 64
|
||||||
|
*/
|
||||||
|
name: string
|
||||||
|
/**
|
||||||
|
* @format uri
|
||||||
|
* @minLength 0
|
||||||
|
* @maxLength 2048
|
||||||
|
*/
|
||||||
|
url: string
|
||||||
|
headers: Record<string, string>
|
||||||
|
/**
|
||||||
|
* @minLength 0
|
||||||
|
* @maxLength 8
|
||||||
|
*/
|
||||||
|
httpMethod: string
|
||||||
|
/**
|
||||||
|
* @minLength 0
|
||||||
|
* @maxLength 4096
|
||||||
|
*/
|
||||||
|
body: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NtfyRecord {
|
||||||
|
name?: string | null
|
||||||
|
endpoint?: string | null
|
||||||
|
username?: string | null
|
||||||
|
password?: string | null
|
||||||
|
topic?: string | null
|
||||||
|
/** @format int32 */
|
||||||
|
priority?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProblemDetails {
|
||||||
|
type?: string | null
|
||||||
|
title?: string | null
|
||||||
|
/** @format int32 */
|
||||||
|
status?: number | null
|
||||||
|
detail?: string | null
|
||||||
|
instance?: string | null
|
||||||
|
[key: string]: any
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PushoverRecord {
|
||||||
|
name?: string | null
|
||||||
|
appToken?: string | null
|
||||||
|
user?: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TrangaSettings {
|
||||||
|
downloadLocation?: string | null
|
||||||
|
userAgent?: string | null
|
||||||
|
/** @format int32 */
|
||||||
|
imageCompression?: number
|
||||||
|
blackWhiteImages?: boolean
|
||||||
|
flareSolverrUrl?: string | null
|
||||||
|
/**
|
||||||
|
* Placeholders:
|
||||||
|
* %M Obj Name
|
||||||
|
* %V Volume
|
||||||
|
* %C Chapter
|
||||||
|
* %T Title
|
||||||
|
* %A Author (first in list)
|
||||||
|
* %I Chapter Internal ID
|
||||||
|
* %i Obj Internal ID
|
||||||
|
* %Y Year (Obj)
|
||||||
|
*
|
||||||
|
* ?_(...) replace _ with a value from above:
|
||||||
|
* Everything inside the braces will only be added if the value of %_ is not null
|
||||||
|
*/
|
||||||
|
chapterNamingScheme?: string | null
|
||||||
|
/** @format int32 */
|
||||||
|
workCycleTimeoutMs?: number
|
||||||
|
requestLimits?: {
|
||||||
|
/** @format int32 */
|
||||||
|
Default?: number
|
||||||
|
/** @format int32 */
|
||||||
|
MangaDexFeed?: number
|
||||||
|
/** @format int32 */
|
||||||
|
MangaImage?: number
|
||||||
|
/** @format int32 */
|
||||||
|
MangaCover?: number
|
||||||
|
/** @format int32 */
|
||||||
|
MangaDexImage?: number
|
||||||
|
/** @format int32 */
|
||||||
|
MangaInfo?: number
|
||||||
|
} | null
|
||||||
|
downloadLanguage?: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
/** API.Workers.BaseWorker DTO */
|
||||||
|
export interface Worker {
|
||||||
|
/** Workers this worker depends on having ran. */
|
||||||
|
dependencies: string[]
|
||||||
|
/** Workers that have not yet ran, that need to run for this Worker to run. */
|
||||||
|
missingDependencies: string[]
|
||||||
|
/** Worker can run. */
|
||||||
|
dependenciesFulfilled: boolean
|
||||||
|
state: WorkerExecutionState
|
||||||
|
/**
|
||||||
|
* Unique Identifier of the DTO
|
||||||
|
* @minLength 16
|
||||||
|
* @maxLength 64
|
||||||
|
*/
|
||||||
|
key: string
|
||||||
|
}
|
265
tranga-website/src/api/http-client.ts
Normal file
265
tranga-website/src/api/http-client.ts
Normal file
@@ -0,0 +1,265 @@
|
|||||||
|
/* eslint-disable */
|
||||||
|
/* tslint:disable */
|
||||||
|
// @ts-nocheck
|
||||||
|
/*
|
||||||
|
* ---------------------------------------------------------------
|
||||||
|
* ## THIS FILE WAS GENERATED VIA SWAGGER-TYPESCRIPT-API ##
|
||||||
|
* ## ##
|
||||||
|
* ## AUTHOR: acacode ##
|
||||||
|
* ## SOURCE: https://github.com/acacode/swagger-typescript-api ##
|
||||||
|
* ---------------------------------------------------------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
export type QueryParamsType = Record<string | number, any>
|
||||||
|
export type ResponseFormat = keyof Omit<Body, 'body' | 'bodyUsed'>
|
||||||
|
|
||||||
|
export interface FullRequestParams extends Omit<RequestInit, 'body'> {
|
||||||
|
/** set parameter to `true` for call `securityWorker` for this request */
|
||||||
|
secure?: boolean
|
||||||
|
/** request path */
|
||||||
|
path: string
|
||||||
|
/** content type of request body */
|
||||||
|
type?: ContentType
|
||||||
|
/** query params */
|
||||||
|
query?: QueryParamsType
|
||||||
|
/** format of response (i.e. response.json() -> format: "json") */
|
||||||
|
format?: ResponseFormat
|
||||||
|
/** request body */
|
||||||
|
body?: unknown
|
||||||
|
/** base url */
|
||||||
|
baseUrl?: string
|
||||||
|
/** request cancellation token */
|
||||||
|
cancelToken?: CancelToken
|
||||||
|
}
|
||||||
|
|
||||||
|
export type RequestParams = Omit<
|
||||||
|
FullRequestParams,
|
||||||
|
'body' | 'method' | 'query' | 'path'
|
||||||
|
>
|
||||||
|
|
||||||
|
export interface ApiConfig<SecurityDataType = unknown> {
|
||||||
|
baseUrl?: string
|
||||||
|
baseApiParams?: Omit<RequestParams, 'baseUrl' | 'cancelToken' | 'signal'>
|
||||||
|
securityWorker?: (
|
||||||
|
securityData: SecurityDataType | null
|
||||||
|
) => Promise<RequestParams | void> | RequestParams | void
|
||||||
|
customFetch?: typeof fetch
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface HttpResponse<D extends unknown, E extends unknown = unknown>
|
||||||
|
extends Response {
|
||||||
|
data: D
|
||||||
|
error: E
|
||||||
|
}
|
||||||
|
|
||||||
|
type CancelToken = Symbol | string | number
|
||||||
|
|
||||||
|
export enum ContentType {
|
||||||
|
Json = 'application/json',
|
||||||
|
JsonApi = 'application/vnd.api+json',
|
||||||
|
FormData = 'multipart/form-data',
|
||||||
|
UrlEncoded = 'application/x-www-form-urlencoded',
|
||||||
|
Text = 'text/plain',
|
||||||
|
}
|
||||||
|
|
||||||
|
export class HttpClient<SecurityDataType = unknown> {
|
||||||
|
public baseUrl: string = ''
|
||||||
|
private securityData: SecurityDataType | null = null
|
||||||
|
private securityWorker?: ApiConfig<SecurityDataType>['securityWorker']
|
||||||
|
private abortControllers = new Map<CancelToken, AbortController>()
|
||||||
|
private customFetch = (...fetchParams: Parameters<typeof fetch>) =>
|
||||||
|
fetch(...fetchParams)
|
||||||
|
|
||||||
|
private baseApiParams: RequestParams = {
|
||||||
|
credentials: 'same-origin',
|
||||||
|
headers: {},
|
||||||
|
redirect: 'follow',
|
||||||
|
referrerPolicy: 'no-referrer',
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(apiConfig: ApiConfig<SecurityDataType> = {}) {
|
||||||
|
Object.assign(this, apiConfig)
|
||||||
|
}
|
||||||
|
|
||||||
|
public setSecurityData = (data: SecurityDataType | null) => {
|
||||||
|
this.securityData = data
|
||||||
|
}
|
||||||
|
|
||||||
|
protected encodeQueryParam(key: string, value: any) {
|
||||||
|
const encodedKey = encodeURIComponent(key)
|
||||||
|
return `${encodedKey}=${encodeURIComponent(typeof value === 'number' ? value : `${value}`)}`
|
||||||
|
}
|
||||||
|
|
||||||
|
protected addQueryParam(query: QueryParamsType, key: string) {
|
||||||
|
return this.encodeQueryParam(key, query[key])
|
||||||
|
}
|
||||||
|
|
||||||
|
protected addArrayQueryParam(query: QueryParamsType, key: string) {
|
||||||
|
const value = query[key]
|
||||||
|
return value.map((v: any) => this.encodeQueryParam(key, v)).join('&')
|
||||||
|
}
|
||||||
|
|
||||||
|
protected toQueryString(rawQuery?: QueryParamsType): string {
|
||||||
|
const query = rawQuery || {}
|
||||||
|
const keys = Object.keys(query).filter(
|
||||||
|
(key) => 'undefined' !== typeof query[key]
|
||||||
|
)
|
||||||
|
return keys
|
||||||
|
.map((key) =>
|
||||||
|
Array.isArray(query[key])
|
||||||
|
? this.addArrayQueryParam(query, key)
|
||||||
|
: this.addQueryParam(query, key)
|
||||||
|
)
|
||||||
|
.join('&')
|
||||||
|
}
|
||||||
|
|
||||||
|
protected addQueryParams(rawQuery?: QueryParamsType): string {
|
||||||
|
const queryString = this.toQueryString(rawQuery)
|
||||||
|
return queryString ? `?${queryString}` : ''
|
||||||
|
}
|
||||||
|
|
||||||
|
private contentFormatters: Record<ContentType, (input: any) => any> = {
|
||||||
|
[ContentType.Json]: (input: any) =>
|
||||||
|
input !== null &&
|
||||||
|
(typeof input === 'object' || typeof input === 'string')
|
||||||
|
? JSON.stringify(input)
|
||||||
|
: input,
|
||||||
|
[ContentType.JsonApi]: (input: any) =>
|
||||||
|
input !== null &&
|
||||||
|
(typeof input === 'object' || typeof input === 'string')
|
||||||
|
? JSON.stringify(input)
|
||||||
|
: input,
|
||||||
|
[ContentType.Text]: (input: any) =>
|
||||||
|
input !== null && typeof input !== 'string'
|
||||||
|
? JSON.stringify(input)
|
||||||
|
: input,
|
||||||
|
[ContentType.FormData]: (input: any) =>
|
||||||
|
Object.keys(input || {}).reduce((formData, key) => {
|
||||||
|
const property = input[key]
|
||||||
|
formData.append(
|
||||||
|
key,
|
||||||
|
property instanceof Blob
|
||||||
|
? property
|
||||||
|
: typeof property === 'object' && property !== null
|
||||||
|
? JSON.stringify(property)
|
||||||
|
: `${property}`
|
||||||
|
)
|
||||||
|
return formData
|
||||||
|
}, new FormData()),
|
||||||
|
[ContentType.UrlEncoded]: (input: any) => this.toQueryString(input),
|
||||||
|
}
|
||||||
|
|
||||||
|
protected mergeRequestParams(
|
||||||
|
params1: RequestParams,
|
||||||
|
params2?: RequestParams
|
||||||
|
): RequestParams {
|
||||||
|
return {
|
||||||
|
...this.baseApiParams,
|
||||||
|
...params1,
|
||||||
|
...(params2 || {}),
|
||||||
|
headers: {
|
||||||
|
...(this.baseApiParams.headers || {}),
|
||||||
|
...(params1.headers || {}),
|
||||||
|
...((params2 && params2.headers) || {}),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected createAbortSignal = (
|
||||||
|
cancelToken: CancelToken
|
||||||
|
): AbortSignal | undefined => {
|
||||||
|
if (this.abortControllers.has(cancelToken)) {
|
||||||
|
const abortController = this.abortControllers.get(cancelToken)
|
||||||
|
if (abortController) {
|
||||||
|
return abortController.signal
|
||||||
|
}
|
||||||
|
return void 0
|
||||||
|
}
|
||||||
|
|
||||||
|
const abortController = new AbortController()
|
||||||
|
this.abortControllers.set(cancelToken, abortController)
|
||||||
|
return abortController.signal
|
||||||
|
}
|
||||||
|
|
||||||
|
public abortRequest = (cancelToken: CancelToken) => {
|
||||||
|
const abortController = this.abortControllers.get(cancelToken)
|
||||||
|
|
||||||
|
if (abortController) {
|
||||||
|
abortController.abort()
|
||||||
|
this.abortControllers.delete(cancelToken)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public request = async <T = any, E = any>({
|
||||||
|
body,
|
||||||
|
secure,
|
||||||
|
path,
|
||||||
|
type,
|
||||||
|
query,
|
||||||
|
format,
|
||||||
|
baseUrl,
|
||||||
|
cancelToken,
|
||||||
|
...params
|
||||||
|
}: FullRequestParams): Promise<HttpResponse<T, E>> => {
|
||||||
|
const secureParams =
|
||||||
|
((typeof secure === 'boolean'
|
||||||
|
? secure
|
||||||
|
: this.baseApiParams.secure) &&
|
||||||
|
this.securityWorker &&
|
||||||
|
(await this.securityWorker(this.securityData))) ||
|
||||||
|
{}
|
||||||
|
const requestParams = this.mergeRequestParams(params, secureParams)
|
||||||
|
const queryString = query && this.toQueryString(query)
|
||||||
|
const payloadFormatter =
|
||||||
|
this.contentFormatters[type || ContentType.Json]
|
||||||
|
const responseFormat = format || requestParams.format
|
||||||
|
|
||||||
|
return this.customFetch(
|
||||||
|
`${baseUrl || this.baseUrl || ''}${path}${queryString ? `?${queryString}` : ''}`,
|
||||||
|
{
|
||||||
|
...requestParams,
|
||||||
|
headers: {
|
||||||
|
...(requestParams.headers || {}),
|
||||||
|
...(type && type !== ContentType.FormData
|
||||||
|
? { 'Content-Type': type }
|
||||||
|
: {}),
|
||||||
|
},
|
||||||
|
signal:
|
||||||
|
(cancelToken
|
||||||
|
? this.createAbortSignal(cancelToken)
|
||||||
|
: requestParams.signal) || null,
|
||||||
|
body:
|
||||||
|
typeof body === 'undefined' || body === null
|
||||||
|
? null
|
||||||
|
: payloadFormatter(body),
|
||||||
|
}
|
||||||
|
).then(async (response) => {
|
||||||
|
const r = response.clone() as HttpResponse<T, E>
|
||||||
|
r.data = null as unknown as T
|
||||||
|
r.error = null as unknown as E
|
||||||
|
|
||||||
|
const data = !responseFormat
|
||||||
|
? r
|
||||||
|
: await response[responseFormat]()
|
||||||
|
.then((data) => {
|
||||||
|
if (r.ok) {
|
||||||
|
r.data = data
|
||||||
|
} else {
|
||||||
|
r.error = data
|
||||||
|
}
|
||||||
|
return r
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
r.error = e
|
||||||
|
return r
|
||||||
|
})
|
||||||
|
|
||||||
|
if (cancelToken) {
|
||||||
|
this.abortControllers.delete(cancelToken)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!response.ok) throw data
|
||||||
|
return data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user