refactor: Overhaul Plugin Architecture & Playground UI (#1846)
* refactor: Move Plugins Out and Consolidate with Multisrc * feat: Overhaul Playground UI * feat: Add Filters to New UI * feat: Persist Selected Plugin * feat: Add Direct Parse Novel/Chapter Options and Feedback Toasts [skip ci] * feat: UX Improvements * chore: Update Plugin Path in Publish Workflow * fix: Add Backward Compatibility * chore: Update Plugin Publishing Script
This commit is contained in:
committed by
GitHub
parent
252beff0bf
commit
20c2b75b01
@@ -0,0 +1,83 @@
|
||||
import * as fs from 'fs';
|
||||
import * as cheerio from 'cheerio';
|
||||
import * as path from 'path';
|
||||
import list from './sources.json' with { type: 'json' };
|
||||
import { HotNovelPubMetadata } from './template';
|
||||
|
||||
async function getFilters(sources: HotNovelPubMetadata) {
|
||||
const filters: any = {
|
||||
sort: {
|
||||
type: 'Picker',
|
||||
label: 'Order',
|
||||
value: 'hot',
|
||||
options: [],
|
||||
},
|
||||
category: {
|
||||
type: 'Picker',
|
||||
label: 'category',
|
||||
value: '',
|
||||
options: [{ label: 'NONE', value: '' }],
|
||||
},
|
||||
};
|
||||
const body = await fetch(sources.sourceSite).then(res => res.text());
|
||||
const $: cheerio.CheerioAPI = cheerio.load(body);
|
||||
$('.new-update').remove();
|
||||
|
||||
$('section > div').each((i, el) => {
|
||||
const id = $(el).find('a[class="see-all"]').attr('href');
|
||||
if (id) {
|
||||
filters.sort.options.push({
|
||||
label: $(el).find('[class="section-title"]').text().trim(),
|
||||
value: id.split('/').pop(),
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
filters.category.label = $('.category-title').text().trim();
|
||||
const apiSite = sources.sourceSite.replace('://', '://api.');
|
||||
const jsonRaw = await fetch(apiSite + '/categories', {
|
||||
headers: {
|
||||
lang: sources.options?.lang || 'en',
|
||||
},
|
||||
});
|
||||
const json = (await jsonRaw.json()) as response;
|
||||
|
||||
if (json.data?.length) {
|
||||
json.data
|
||||
.sort((a, b) => a.name.localeCompare(b.name))
|
||||
.forEach(category =>
|
||||
filters.category.options.push({
|
||||
label: category.name,
|
||||
value: category.slug,
|
||||
}),
|
||||
);
|
||||
}
|
||||
return filters;
|
||||
}
|
||||
|
||||
type response = {
|
||||
status: number;
|
||||
message: string;
|
||||
data?: DataEntity[];
|
||||
};
|
||||
type DataEntity = {
|
||||
id: number;
|
||||
name: string;
|
||||
slug: string;
|
||||
};
|
||||
|
||||
async function start() {
|
||||
const result = [];
|
||||
for (const sources of list) {
|
||||
console.log('updating the filters in', sources.sourceName);
|
||||
const NewFilters = await getFilters(sources as any);
|
||||
sources.filters = NewFilters;
|
||||
result.push(sources);
|
||||
}
|
||||
fs.writeFileSync(
|
||||
path.join(__dirname, 'sources.json'),
|
||||
JSON.stringify(result, null, 2),
|
||||
);
|
||||
}
|
||||
|
||||
start();
|
||||
@@ -0,0 +1,63 @@
|
||||
import list from './sources.json' with { type: 'json' };
|
||||
import { readFileSync } from 'fs';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { dirname, join } from 'path';
|
||||
|
||||
const folder = dirname(fileURLToPath(import.meta.url));
|
||||
const FilterTypes = {
|
||||
TextInput: 'Text',
|
||||
Picker: 'Picker',
|
||||
CheckboxGroup: 'Checkbox',
|
||||
Switch: 'Switch',
|
||||
ExcludableCheckboxGroup: 'XCheckbox',
|
||||
};
|
||||
|
||||
const lang = {
|
||||
en: 'english',
|
||||
ru: 'russian',
|
||||
es: 'spanish',
|
||||
pt: 'portuguese',
|
||||
th: 'turkish',
|
||||
};
|
||||
|
||||
export const generateAll = function () {
|
||||
return list
|
||||
.map(p => {
|
||||
const filters = {};
|
||||
for (const k in p.filters) {
|
||||
const f = p.filters[k];
|
||||
if (f) {
|
||||
filters[k] = {
|
||||
...f,
|
||||
type: FilterTypes.Picker,
|
||||
};
|
||||
}
|
||||
}
|
||||
return { ...p, filters };
|
||||
})
|
||||
.map(metadata => {
|
||||
console.log(`[hotnovelpub]: Generating`, metadata.id);
|
||||
return generator(metadata);
|
||||
});
|
||||
};
|
||||
|
||||
const generator = function generator(metadata) {
|
||||
const HotNovelPubTemplate = readFileSync(join(folder, 'template.ts'), {
|
||||
encoding: 'utf-8',
|
||||
});
|
||||
|
||||
const pluginScript = `
|
||||
${HotNovelPubTemplate}
|
||||
const plugin = new HotNovelPubPlugin(${JSON.stringify(metadata).replace(
|
||||
/"type":"([^"]+)"/g,
|
||||
'"type":FilterTypes.$1',
|
||||
)});
|
||||
export default plugin;
|
||||
`.trim();
|
||||
|
||||
return {
|
||||
lang: lang[metadata?.options?.lang || 'en'] || 'english',
|
||||
filename: metadata.sourceName,
|
||||
pluginScript,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,387 @@
|
||||
[
|
||||
{
|
||||
"id": "hotnovelpub",
|
||||
"sourceSite": "https://hotnovelpub.com",
|
||||
"sourceName": "HotNovelPub",
|
||||
"filters": {
|
||||
"sort": {
|
||||
"type": "Picker",
|
||||
"label": "Order",
|
||||
"value": "hot",
|
||||
"options": [
|
||||
{
|
||||
"label": "Hot story",
|
||||
"value": "hot"
|
||||
},
|
||||
{
|
||||
"label": "New story",
|
||||
"value": "new"
|
||||
},
|
||||
{
|
||||
"label": "Full story",
|
||||
"value": "full"
|
||||
}
|
||||
]
|
||||
},
|
||||
"category": {
|
||||
"type": "Picker",
|
||||
"label": "Category",
|
||||
"value": "",
|
||||
"options": [
|
||||
{
|
||||
"label": "NONE",
|
||||
"value": ""
|
||||
},
|
||||
{
|
||||
"label": "Adult Romance",
|
||||
"value": "adult-romance"
|
||||
},
|
||||
{
|
||||
"label": "Fantasy",
|
||||
"value": "fantasy"
|
||||
},
|
||||
{
|
||||
"label": "LGBTQ+",
|
||||
"value": "lgbtq"
|
||||
},
|
||||
{
|
||||
"label": "Mystery",
|
||||
"value": "mystery"
|
||||
},
|
||||
{
|
||||
"label": "New Adult",
|
||||
"value": "new-adult"
|
||||
},
|
||||
{
|
||||
"label": "Paranormal",
|
||||
"value": "paranormal"
|
||||
},
|
||||
{
|
||||
"label": "Romance",
|
||||
"value": "romance"
|
||||
},
|
||||
{
|
||||
"label": "Steamy Stories",
|
||||
"value": "steamy-stories"
|
||||
},
|
||||
{
|
||||
"label": "Urban",
|
||||
"value": "urban"
|
||||
},
|
||||
{
|
||||
"label": "Werewolf/Vampire",
|
||||
"value": "werewolfvampire"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "eznovels",
|
||||
"sourceSite": "https://eznovels.com",
|
||||
"sourceName": "EzNovels",
|
||||
"filters": {
|
||||
"sort": {
|
||||
"type": "Picker",
|
||||
"label": "Order",
|
||||
"value": "hot",
|
||||
"options": [
|
||||
{
|
||||
"label": "Горячая история",
|
||||
"value": "hot"
|
||||
},
|
||||
{
|
||||
"label": "Новая история",
|
||||
"value": "new"
|
||||
},
|
||||
{
|
||||
"label": "Полный рассказ",
|
||||
"value": "full"
|
||||
}
|
||||
]
|
||||
},
|
||||
"category": {
|
||||
"type": "Picker",
|
||||
"label": "Категория",
|
||||
"value": "",
|
||||
"options": [
|
||||
{
|
||||
"label": "NONE",
|
||||
"value": ""
|
||||
},
|
||||
{
|
||||
"label": "Ведьмы",
|
||||
"value": "vedmy"
|
||||
},
|
||||
{
|
||||
"label": "Городское фэнтези",
|
||||
"value": "gorodskoe-fentezi"
|
||||
},
|
||||
{
|
||||
"label": "Городской роман",
|
||||
"value": "gorodskoj-roman"
|
||||
},
|
||||
{
|
||||
"label": "Любовное фэнтези",
|
||||
"value": "lyubovnoe-fentezi"
|
||||
},
|
||||
{
|
||||
"label": "Оборотни",
|
||||
"value": "oborotni"
|
||||
},
|
||||
{
|
||||
"label": "Сверхъестественное",
|
||||
"value": "sverhuestestvennoe"
|
||||
},
|
||||
{
|
||||
"label": "Современный любовный роман",
|
||||
"value": "sovremennyj-lyubovnyj-roman"
|
||||
},
|
||||
{
|
||||
"label": "Триллеры",
|
||||
"value": "trillery"
|
||||
},
|
||||
{
|
||||
"label": "Фэнтези",
|
||||
"value": "fentezi"
|
||||
},
|
||||
{
|
||||
"label": "Эротика",
|
||||
"value": "erotika"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"lang": "ru"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "lightnoveldaily",
|
||||
"sourceSite": "https://lightnoveldaily.com",
|
||||
"sourceName": "LightNovelDaily",
|
||||
"filters": {
|
||||
"sort": {
|
||||
"type": "Picker",
|
||||
"label": "Order",
|
||||
"value": "hot",
|
||||
"options": [
|
||||
{
|
||||
"label": "Historia caliente",
|
||||
"value": "hot"
|
||||
},
|
||||
{
|
||||
"label": "Nuevas cuentos",
|
||||
"value": "new"
|
||||
},
|
||||
{
|
||||
"label": "Historia completa",
|
||||
"value": "full"
|
||||
}
|
||||
]
|
||||
},
|
||||
"category": {
|
||||
"type": "Picker",
|
||||
"label": "Categoría",
|
||||
"value": "",
|
||||
"options": [
|
||||
{
|
||||
"label": "NONE",
|
||||
"value": ""
|
||||
},
|
||||
{
|
||||
"label": "Acción/Aventura",
|
||||
"value": "accinaventura"
|
||||
},
|
||||
{
|
||||
"label": "Fantasía",
|
||||
"value": "fantasa"
|
||||
},
|
||||
{
|
||||
"label": "Ficción adolescente",
|
||||
"value": "ficcin-adolescente"
|
||||
},
|
||||
{
|
||||
"label": "Hombre lobo/Vampiro",
|
||||
"value": "hombre-lobovampiro"
|
||||
},
|
||||
{
|
||||
"label": "LGBTQ+",
|
||||
"value": "lgbtq"
|
||||
},
|
||||
{
|
||||
"label": "Mafia",
|
||||
"value": "mafia"
|
||||
},
|
||||
{
|
||||
"label": "Nuevo Adulto",
|
||||
"value": "nuevo-adulto"
|
||||
},
|
||||
{
|
||||
"label": "Pornografía",
|
||||
"value": "pornografa"
|
||||
},
|
||||
{
|
||||
"label": "Romántico",
|
||||
"value": "romntico"
|
||||
},
|
||||
{
|
||||
"label": "Urbano",
|
||||
"value": "urbano"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"lang": "es"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "lanovels",
|
||||
"sourceSite": "https://lanovels.com",
|
||||
"sourceName": "LaNovels",
|
||||
"filters": {
|
||||
"sort": {
|
||||
"type": "Picker",
|
||||
"label": "Order",
|
||||
"value": "hot",
|
||||
"options": [
|
||||
{
|
||||
"label": "História quente",
|
||||
"value": "hot"
|
||||
},
|
||||
{
|
||||
"label": "Nova estória",
|
||||
"value": "new"
|
||||
},
|
||||
{
|
||||
"label": "História completa",
|
||||
"value": "full"
|
||||
}
|
||||
]
|
||||
},
|
||||
"category": {
|
||||
"type": "Picker",
|
||||
"label": "Categoria",
|
||||
"value": "",
|
||||
"options": [
|
||||
{
|
||||
"label": "NONE",
|
||||
"value": ""
|
||||
},
|
||||
{
|
||||
"label": "Aventura",
|
||||
"value": "aventura"
|
||||
},
|
||||
{
|
||||
"label": "Erótico",
|
||||
"value": "erotico"
|
||||
},
|
||||
{
|
||||
"label": "Fantasia",
|
||||
"value": "fantasia"
|
||||
},
|
||||
{
|
||||
"label": "Ficção adolescente",
|
||||
"value": "ficcao-adolescente"
|
||||
},
|
||||
{
|
||||
"label": "Ficção científica",
|
||||
"value": "ficcao-cientifica"
|
||||
},
|
||||
{
|
||||
"label": "Lobisomens/Vampiros",
|
||||
"value": "lobisomens-vampiros"
|
||||
},
|
||||
{
|
||||
"label": "Mistério",
|
||||
"value": "misterio"
|
||||
},
|
||||
{
|
||||
"label": "Paranormal",
|
||||
"value": "paranormal"
|
||||
},
|
||||
{
|
||||
"label": "Romance",
|
||||
"value": "romance"
|
||||
},
|
||||
{
|
||||
"label": "Suspense/Terror",
|
||||
"value": "suspense-terror"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"lang": "pt"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "thnovels",
|
||||
"sourceSite": "https://thnovels.com",
|
||||
"sourceName": "ThNovels",
|
||||
"filters": {
|
||||
"sort": {
|
||||
"type": "Picker",
|
||||
"label": "Order",
|
||||
"value": "hot",
|
||||
"options": [
|
||||
{
|
||||
"label": "เรื่องร้อน",
|
||||
"value": "hot"
|
||||
},
|
||||
{
|
||||
"label": "เรื่องใหม่",
|
||||
"value": "new"
|
||||
},
|
||||
{
|
||||
"label": "เรื่องเต็ม",
|
||||
"value": "full"
|
||||
}
|
||||
]
|
||||
},
|
||||
"category": {
|
||||
"type": "Picker",
|
||||
"label": "หมวดหมู่",
|
||||
"value": "",
|
||||
"options": [
|
||||
{
|
||||
"label": "NONE",
|
||||
"value": ""
|
||||
},
|
||||
{
|
||||
"label": "ใช้ชีวิต",
|
||||
"value": "ใช้ชีวิต"
|
||||
},
|
||||
{
|
||||
"label": "นิยายY",
|
||||
"value": "นิยายy"
|
||||
},
|
||||
{
|
||||
"label": "นิยายสำหรับผู้ใหญ่",
|
||||
"value": "นิยายสำหรับผู้ใหญ่"
|
||||
},
|
||||
{
|
||||
"label": "ประวัติศาสตร์",
|
||||
"value": "ประวัติศาสตร์"
|
||||
},
|
||||
{
|
||||
"label": "แฟนตาซี",
|
||||
"value": "แฟนตาซี"
|
||||
},
|
||||
{
|
||||
"label": "มนุษย์หมาป่าแวมไพร์",
|
||||
"value": "มนุษย์หมาป่าแวมไพร์"
|
||||
},
|
||||
{
|
||||
"label": "โรแมนซ์",
|
||||
"value": "โรแมนซ์"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"lang": "th"
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,275 @@
|
||||
import { fetchApi } from '@libs/fetch';
|
||||
import { Filters, FilterTypes } from '@libs/filterInputs';
|
||||
import { Plugin } from '@/types/plugin';
|
||||
import { NovelStatus } from '@libs/novelStatus';
|
||||
|
||||
export type HotNovelPubMetadata = {
|
||||
id: string;
|
||||
sourceSite: string;
|
||||
sourceName: string;
|
||||
filters?: Filters;
|
||||
options?: HotNovelPubOptions;
|
||||
};
|
||||
|
||||
type HotNovelPubOptions = {
|
||||
lang?: string;
|
||||
};
|
||||
|
||||
class HotNovelPubPlugin implements Plugin.PluginBase {
|
||||
id: string;
|
||||
name: string;
|
||||
icon: string;
|
||||
site: string;
|
||||
apiSite: string;
|
||||
version: string;
|
||||
filters?: Filters;
|
||||
lang: string;
|
||||
|
||||
constructor(metadata: HotNovelPubMetadata) {
|
||||
this.id = metadata.id;
|
||||
this.name = metadata.sourceName;
|
||||
this.icon = `multisrc/hotnovelpub/${metadata.id.toLowerCase()}/icon.png`;
|
||||
this.site = metadata.sourceSite;
|
||||
this.apiSite = metadata.sourceSite.replace('://', '://api.');
|
||||
this.version = '1.0.1';
|
||||
this.filters = metadata.filters;
|
||||
this.lang = metadata.options?.lang || 'en';
|
||||
}
|
||||
|
||||
async popularNovels(
|
||||
pageNo: number,
|
||||
{ filters, showLatestNovels }: Plugin.PopularNovelsOptions,
|
||||
): Promise<Plugin.NovelItem[]> {
|
||||
let url = this.apiSite + '/books/';
|
||||
url += showLatestNovels ? 'new' : filters?.sort?.value || 'hot';
|
||||
if (filters?.category?.value) {
|
||||
url = this.apiSite + '/category/' + filters.category.value;
|
||||
}
|
||||
|
||||
url += '/?page=' + (pageNo - 1) + '&limit=20';
|
||||
|
||||
const result: responseNovels = await fetchApi(url, {
|
||||
headers: {
|
||||
lang: this.lang,
|
||||
},
|
||||
}).then(res => res.json());
|
||||
const novels: Plugin.NovelItem[] = [];
|
||||
|
||||
if (result.status && result.data.books.data?.length) {
|
||||
result.data.books.data.forEach(novel =>
|
||||
novels.push({
|
||||
name: novel.name,
|
||||
cover: this.site + novel.image,
|
||||
path: novel.slug,
|
||||
}),
|
||||
);
|
||||
}
|
||||
return novels;
|
||||
}
|
||||
|
||||
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
|
||||
const json: responseNovel = await fetchApi(
|
||||
this.apiSite + '/book/' + novelPath,
|
||||
{
|
||||
headers: {
|
||||
lang: this.lang,
|
||||
},
|
||||
},
|
||||
).then(res => res.json());
|
||||
|
||||
const novel: Plugin.SourceNovel = {
|
||||
name: json.data.book.name,
|
||||
path: novelPath,
|
||||
cover: this.site + json.data.book.image,
|
||||
summary: json.data.book.authorize.description,
|
||||
author: json.data.book.authorize.name,
|
||||
status:
|
||||
json.data.book.status === 'updating'
|
||||
? NovelStatus.Ongoing
|
||||
: NovelStatus.Completed,
|
||||
};
|
||||
|
||||
if (json.data.tags.tags_name?.length) {
|
||||
novel.genres = json.data.tags.tags_name.join(',');
|
||||
}
|
||||
|
||||
if (json.data.chapters?.length) {
|
||||
const chapters: Plugin.ChapterItem[] = [];
|
||||
json.data.chapters.forEach((chapter, chapterIndex) =>
|
||||
chapters.push({
|
||||
name: chapter.title,
|
||||
path: chapter.slug,
|
||||
releaseTime: undefined,
|
||||
chapterNumber: (chapter.index || chapterIndex) + 1,
|
||||
}),
|
||||
);
|
||||
|
||||
novel.chapters = chapters;
|
||||
}
|
||||
return novel;
|
||||
}
|
||||
|
||||
async parseChapter(chapterPath: string): Promise<string> {
|
||||
const body = await fetchApi(this.resolveUrl(chapterPath)).then(res =>
|
||||
res.text(),
|
||||
);
|
||||
|
||||
let chapterText =
|
||||
body.match(/<div id="content-item" ([\s\S]*?)<\/div>/)?.[0] || '';
|
||||
|
||||
if (chapterText) {
|
||||
const result = await fetchApi(
|
||||
this.site + '/server/getContent?slug=' + chapterPath,
|
||||
);
|
||||
const json = (await result.json()) as ChapterType;
|
||||
|
||||
if (json.data) {
|
||||
chapterText += json.data
|
||||
.map(item => '<p>' + item + '</p>')
|
||||
.join('')
|
||||
.replace(/\n/g, '</p><p>')
|
||||
.replace(/\s/g, ' ');
|
||||
}
|
||||
}
|
||||
return chapterText.replace(/\.copy right hot novel pub/g, '');
|
||||
}
|
||||
|
||||
async searchNovels(searchTerm: string): Promise<Plugin.NovelItem[]> {
|
||||
const result: responseSearch = await fetchApi(this.apiSite + '/search', {
|
||||
headers: {
|
||||
'Content-Type': 'application/json;charset=utf-8',
|
||||
Referer: this.site,
|
||||
Origin: this.site,
|
||||
lang: this.lang,
|
||||
},
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ key_search: searchTerm }),
|
||||
}).then(res => res.json());
|
||||
const novels: Plugin.NovelItem[] = [];
|
||||
|
||||
if (result.status && result.data.books?.length) {
|
||||
result.data.books.forEach(novel =>
|
||||
novels.push({
|
||||
name: novel.name,
|
||||
path: novel.slug,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
return novels;
|
||||
}
|
||||
resolveUrl = (path: string) => this.site + '/' + path;
|
||||
}
|
||||
|
||||
type responseNovels = {
|
||||
status: number;
|
||||
message: string;
|
||||
data: Data;
|
||||
};
|
||||
type Data = {
|
||||
category?: Category;
|
||||
books: Books;
|
||||
};
|
||||
type Category = {
|
||||
id: number;
|
||||
name: string;
|
||||
description: string;
|
||||
slug: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
countryId: number;
|
||||
};
|
||||
type Books = {
|
||||
total: number;
|
||||
pages_count: number;
|
||||
data?: DataEntity[];
|
||||
};
|
||||
type DataEntity = {
|
||||
id: number;
|
||||
name: string;
|
||||
view: number;
|
||||
status: string;
|
||||
image: string;
|
||||
slug: string;
|
||||
categories?: CategoriesEntity[] | null;
|
||||
source?: null;
|
||||
};
|
||||
type CategoriesEntity = {
|
||||
id: number;
|
||||
name: string;
|
||||
slug?: string;
|
||||
};
|
||||
|
||||
type responseNovel = {
|
||||
status: number;
|
||||
message: string;
|
||||
data: Data1;
|
||||
};
|
||||
type Data1 = {
|
||||
book: Book1;
|
||||
tags: Tags;
|
||||
chapters?: ChaptersEntity[];
|
||||
};
|
||||
type Book1 = {
|
||||
id: number;
|
||||
name: string;
|
||||
description: string;
|
||||
view: number;
|
||||
rate: number;
|
||||
status: string;
|
||||
image: string;
|
||||
createdAt: string;
|
||||
chapterCount: number;
|
||||
crawlStt: string;
|
||||
rateCount: number;
|
||||
slug: string;
|
||||
categories?: CategoriesEntity[] | null;
|
||||
authorize: Authorize;
|
||||
updated_at: string;
|
||||
source?: null;
|
||||
idSource?: null;
|
||||
prize?: null;
|
||||
};
|
||||
type Authorize = {
|
||||
id: number;
|
||||
name: string;
|
||||
description: string;
|
||||
avatar: string;
|
||||
slug: string;
|
||||
};
|
||||
type Tags = {
|
||||
tags_name?: string[] | null;
|
||||
tags?: CategoriesEntity[] | null;
|
||||
};
|
||||
type ChaptersEntity = {
|
||||
id: number;
|
||||
title: string;
|
||||
slug: string;
|
||||
index: number;
|
||||
};
|
||||
|
||||
type ChapterType = {
|
||||
type: string;
|
||||
data?: string[];
|
||||
};
|
||||
|
||||
type responseSearch = {
|
||||
status: number;
|
||||
message: string;
|
||||
data: Data2;
|
||||
};
|
||||
type Data2 = {
|
||||
books?: BooksEntity[] | null;
|
||||
authorizes?: AuthorizesEntityOrCategoriesEntity[] | null;
|
||||
categories?: AuthorizesEntityOrCategoriesEntity[] | null;
|
||||
};
|
||||
type BooksEntity = {
|
||||
id: number;
|
||||
name: string;
|
||||
slug: string;
|
||||
};
|
||||
type AuthorizesEntityOrCategoriesEntity = {
|
||||
id: number;
|
||||
name: string;
|
||||
slug: string;
|
||||
};
|
||||
Reference in New Issue
Block a user