squash
This commit is contained in:
parent
755f0ca387
commit
b143186d85
|
@ -2,6 +2,7 @@ test/
|
||||||
storage/
|
storage/
|
||||||
dist/
|
dist/
|
||||||
client/dev/
|
client/dev/
|
||||||
|
client/dist/
|
||||||
client/node_modules/
|
client/node_modules/
|
||||||
__pycache__
|
__pycache__
|
||||||
venv
|
venv
|
||||||
|
|
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -15,7 +15,7 @@ dev_*
|
||||||
client/dev
|
client/dev
|
||||||
|
|
||||||
# Dev file server
|
# Dev file server
|
||||||
storage/
|
/storage/
|
||||||
|
|
||||||
# Javascript packages
|
# Javascript packages
|
||||||
node_modules
|
node_modules
|
||||||
|
|
|
@ -1,29 +0,0 @@
|
||||||
# webpack output
|
|
||||||
**/dev
|
|
||||||
**/dist
|
|
||||||
|
|
||||||
**/.classpath
|
|
||||||
**/.dockerignore
|
|
||||||
**/.env
|
|
||||||
**/.git
|
|
||||||
**/.gitignore
|
|
||||||
**/.project
|
|
||||||
**/.settings
|
|
||||||
**/.toolstarget
|
|
||||||
**/.vs
|
|
||||||
**/.vscode
|
|
||||||
**/*.code-workspace
|
|
||||||
**/*.*proj.user
|
|
||||||
**/*.dbmdl
|
|
||||||
**/*.jfm
|
|
||||||
**/azds.yaml
|
|
||||||
**/charts
|
|
||||||
**/docker-compose*
|
|
||||||
**/compose*
|
|
||||||
**/Dockerfile*
|
|
||||||
**/node_modules
|
|
||||||
**/npm-debug.log
|
|
||||||
**/obj
|
|
||||||
**/secrets.dev.yaml
|
|
||||||
**/values.dev.yaml
|
|
||||||
README.md
|
|
3
client/.vscode/extensions.json
vendored
3
client/.vscode/extensions.json
vendored
|
@ -1,3 +0,0 @@
|
||||||
{
|
|
||||||
"recommendations": []
|
|
||||||
}
|
|
12
client/.vscode/settings.json
vendored
12
client/.vscode/settings.json
vendored
|
@ -1,14 +1,9 @@
|
||||||
{
|
{
|
||||||
|
"typescript.tsdk": "./node_modules/typescript/lib",
|
||||||
|
"typescript.enablePromptUseWorkspaceTsdk": true,
|
||||||
"files.exclude": {
|
"files.exclude": {
|
||||||
"node_modules": true
|
"node_modules": true
|
||||||
},
|
},
|
||||||
// this option does work and is required for emmet in jinja to work
|
|
||||||
"files.associations": {
|
|
||||||
"*.html": "jinja-html"
|
|
||||||
},
|
|
||||||
"emmet.includeLanguages": {
|
|
||||||
"jinja-html": "html"
|
|
||||||
},
|
|
||||||
"search.exclude": {
|
"search.exclude": {
|
||||||
"**/node_modules": true,
|
"**/node_modules": true,
|
||||||
"**/bower_components": true,
|
"**/bower_components": true,
|
||||||
|
@ -19,9 +14,6 @@
|
||||||
"javascript.preferences.importModuleSpecifierEnding": "js",
|
"javascript.preferences.importModuleSpecifierEnding": "js",
|
||||||
"javascript.preferences.quoteStyle": "double",
|
"javascript.preferences.quoteStyle": "double",
|
||||||
"javascript.format.semicolons": "insert",
|
"javascript.format.semicolons": "insert",
|
||||||
"[jinja-html]": {
|
|
||||||
"editor.tabSize": 2
|
|
||||||
},
|
|
||||||
"[javascript]": {
|
"[javascript]": {
|
||||||
"editor.tabSize": 2
|
"editor.tabSize": 2
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,16 +0,0 @@
|
||||||
# syntax=docker/dockerfile:1
|
|
||||||
|
|
||||||
FROM node:16.14
|
|
||||||
|
|
||||||
ENV NODE_ENV=production
|
|
||||||
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
COPY ["package.json", "package-lock.json", "/app/"]
|
|
||||||
|
|
||||||
RUN npm install -g npm
|
|
||||||
RUN npm ci --also=dev
|
|
||||||
|
|
||||||
COPY . /app
|
|
||||||
|
|
||||||
CMD [ "npm", "run", "build" ]
|
|
|
@ -1,15 +0,0 @@
|
||||||
# syntax=docker/dockerfile:1
|
|
||||||
|
|
||||||
FROM node:12.22
|
|
||||||
|
|
||||||
ENV NODE_ENV=development
|
|
||||||
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
COPY ["package.json", "package-lock.json*", "./"]
|
|
||||||
|
|
||||||
RUN npm install
|
|
||||||
|
|
||||||
COPY . .
|
|
||||||
|
|
||||||
CMD ["npm", "run", "dev"]
|
|
|
@ -1,92 +0,0 @@
|
||||||
const path = require("path");
|
|
||||||
const fse = require("fs-extra");
|
|
||||||
const HTMLWebpackPlugin = require("html-webpack-plugin");
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @typedef BuildOptions
|
|
||||||
* @property {string} fileExtension
|
|
||||||
* @property {string} outputPrefix
|
|
||||||
* @property {HTMLWebpackPlugin.Options} pluginOptions Webpack plugin options.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/** */
|
|
||||||
class TemplateFile {
|
|
||||||
/**
|
|
||||||
* @param {fse.Dirent} dirent
|
|
||||||
* @param {string} path Absolute path to the file.
|
|
||||||
*/
|
|
||||||
constructor(dirent, path) {
|
|
||||||
this.dirent = dirent;
|
|
||||||
this.path = path;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Builds an array of HTML webpack plugins from the provided folder.
|
|
||||||
* @param {string} basePath Absolute path to the template folder.
|
|
||||||
* @param {BuildOptions} options Build optons.
|
|
||||||
*/
|
|
||||||
function buildHTMLWebpackPluginsRecursive(basePath, options) {
|
|
||||||
/**
|
|
||||||
* @type {HTMLWebpackPlugin[]}
|
|
||||||
*/
|
|
||||||
const plugins = [];
|
|
||||||
const files = walkFolder(basePath);
|
|
||||||
|
|
||||||
files.forEach((file) => {
|
|
||||||
const isTemplateFile = file.dirent.isFile() && file.path.endsWith(`${options.fileExtension}`);
|
|
||||||
|
|
||||||
if (isTemplateFile) {
|
|
||||||
const outputBase = path.relative(basePath, file.path);
|
|
||||||
const outputPath = path.join(path.basename(basePath), outputBase);
|
|
||||||
|
|
||||||
const webpackPlugin = new HTMLWebpackPlugin({
|
|
||||||
...options.pluginOptions,
|
|
||||||
template: file.path,
|
|
||||||
filename: outputPath,
|
|
||||||
});
|
|
||||||
|
|
||||||
plugins.push(webpackPlugin);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return plugins;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {string} folderPath Absolute path to the folder.
|
|
||||||
* @param {TemplateFile[]} files
|
|
||||||
*/
|
|
||||||
function walkFolder(folderPath, files = [], currentCount = 0) {
|
|
||||||
const nestedLimit = 1000;
|
|
||||||
const folderContents = fse.readdirSync(folderPath, { withFileTypes: true });
|
|
||||||
|
|
||||||
folderContents.forEach((entry) => {
|
|
||||||
const file = entry.isFile() && entry;
|
|
||||||
const folder = entry.isDirectory() && entry;
|
|
||||||
|
|
||||||
if (file) {
|
|
||||||
const filePath = path.join(folderPath, file.name);
|
|
||||||
files.push(new TemplateFile(file, filePath));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (folder) {
|
|
||||||
currentCount++;
|
|
||||||
|
|
||||||
if (currentCount > nestedLimit) {
|
|
||||||
throw new Error(`The folder at "${folderPath}" contains more than ${nestedLimit} folders.`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const newFolderPath = path.join(folderPath, folder.name);
|
|
||||||
|
|
||||||
return walkFolder(newFolderPath, files, currentCount);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return files;
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
buildHTMLWebpackPluginsRecursive,
|
|
||||||
};
|
|
|
@ -1,5 +0,0 @@
|
||||||
{
|
|
||||||
"html": {
|
|
||||||
"snippets": {}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,21 +1,167 @@
|
||||||
|
// @ts-check
|
||||||
const path = require("path");
|
const path = require("path");
|
||||||
|
const fs = require("fs");
|
||||||
|
|
||||||
require("dotenv").config({
|
/**
|
||||||
path: path.resolve(__dirname, "..", ".."),
|
* @typedef IConfiguration
|
||||||
});
|
* @property {string} site
|
||||||
|
* @property {string} [sentry_dsn_js]
|
||||||
|
* @property {boolean} development_mode
|
||||||
|
* @property {boolean} automatic_migrations
|
||||||
|
* @property {IServerConfig} webserver
|
||||||
|
* @property {IArchiveServerConfig} [archive_server]
|
||||||
|
*/
|
||||||
|
|
||||||
const kemonoSite = process.env.KEMONO_SITE || "http://localhost:5000";
|
/**
|
||||||
const nodeEnv = process.env.NODE_ENV || "production";
|
* @typedef IServerConfig
|
||||||
|
* @property {IUIConfig} ui
|
||||||
|
* @property {number} port
|
||||||
|
* @property {string} [base_url]
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef IUIConfig
|
||||||
|
* @property {IHomeConfig} home
|
||||||
|
* @property {{paysite_list: string[], artists_or_creators: string}} config
|
||||||
|
* @property {IMatomoConfig} [matomo]
|
||||||
|
* @property {ISidebarConfig} [sidebar]
|
||||||
|
* @property {unknown[]} sidebar_items
|
||||||
|
* @property {unknown[]} [footer_items]
|
||||||
|
* @property {IBannerConfig} [banner]
|
||||||
|
* @property {IAdsConfig} [ads]
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef IMatomoConfig
|
||||||
|
* @property {boolean} enabled
|
||||||
|
* @property {string} plain_code b64-encoded string
|
||||||
|
* @property {string} tracking_domain
|
||||||
|
* @property {string} tracking_code
|
||||||
|
* @property {string} site_id
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef ISidebarConfig
|
||||||
|
* @property {boolean} [disable_dms]
|
||||||
|
* @property {boolean} [disable_faq]
|
||||||
|
* @property {boolean} [disable_filehaus]
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef IBannerConfig
|
||||||
|
* @property {string} [global] b64-encoded string
|
||||||
|
* @property {string} [welcome] b64-encoded string
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef IHomeConfig
|
||||||
|
* @property {string} [site_name]
|
||||||
|
* @property {string} [mascot_path]
|
||||||
|
* @property {string} [logo_path]
|
||||||
|
* @property {string} [welcome_credits] b64-encoded string
|
||||||
|
* @property {string} [home_background_image]
|
||||||
|
* @property {{ title: string, date: string, content: string }[]} [announcements]
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef IAdsConfig
|
||||||
|
* @property {string} [header] b64-encoded string
|
||||||
|
* @property {string} [middle] b64-encoded string
|
||||||
|
* @property {string} [footer] b64-encoded string
|
||||||
|
* @property {string} [slider] b64-encoded string
|
||||||
|
* @property {string} [video] b64-encoded JSON string
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef IArchiveServerConfig
|
||||||
|
* @property {boolean} [enabled]
|
||||||
|
*/
|
||||||
|
|
||||||
|
const configuration = getConfiguration();
|
||||||
|
const apiServerBaseURL = configuration.webserver.base_url;
|
||||||
|
const sentryDSN = configuration.sentry_dsn_js;
|
||||||
|
const apiServerPort = !apiServerBaseURL
|
||||||
|
? undefined
|
||||||
|
: configuration.webserver.port;
|
||||||
|
const siteName = configuration.webserver.ui.home.site_name || "Kemono";
|
||||||
|
const homeBackgroundImage =
|
||||||
|
configuration.webserver.ui.home.home_background_image;
|
||||||
|
const homeMascotPath = configuration.webserver.ui.home.mascot_path;
|
||||||
|
const homeLogoPath = configuration.webserver.ui.home.logo_path;
|
||||||
|
const homeWelcomeCredits = configuration.webserver.ui.home.welcome_credits;
|
||||||
|
const homeAnnouncements = configuration.webserver.ui.home.announcements;
|
||||||
|
// TODO: in development it should point to webpack server
|
||||||
|
const kemonoSite = configuration.site || "http://localhost:5000";
|
||||||
|
const paysiteList = configuration.webserver.ui.config.paysite_list;
|
||||||
|
const artistsOrCreators =
|
||||||
|
configuration.webserver.ui.config.artists_or_creators ?? "Artists";
|
||||||
|
const disableDMs = configuration.webserver.ui.sidebar?.disable_dms ?? true;
|
||||||
|
const disableFAQ = configuration.webserver.ui.sidebar?.disable_faq ?? true;
|
||||||
|
const disableFilehaus =
|
||||||
|
configuration.webserver.ui.sidebar?.disable_filehaus ?? true;
|
||||||
|
const sidebarItems = configuration.webserver.ui.sidebar_items;
|
||||||
|
const footerItems = configuration.webserver.ui.footer_items;
|
||||||
|
const bannerGlobal = configuration.webserver.ui.banner?.global;
|
||||||
|
const bannerWelcome = configuration.webserver.ui.banner?.welcome;
|
||||||
|
const headerAd = configuration.webserver.ui.ads?.header;
|
||||||
|
const middleAd = configuration.webserver.ui.ads?.middle;
|
||||||
|
const footerAd = configuration.webserver.ui.ads?.footer;
|
||||||
|
const sliderAd = configuration.webserver.ui.ads?.slider;
|
||||||
|
const videoAd = configuration.webserver.ui.ads?.video;
|
||||||
|
const isArchiveServerEnabled = configuration.archive_server?.enabled ?? false;
|
||||||
|
const analyticsEnabled = configuration.webserver.ui.matomo?.enabled ?? false;
|
||||||
|
const analyticsCode = configuration.webserver.ui.matomo?.plain_code;
|
||||||
const iconsPrepend = process.env.ICONS_PREPEND || "";
|
const iconsPrepend = process.env.ICONS_PREPEND || "";
|
||||||
const bannersPrepend = process.env.BANNERS_PREPEND || "";
|
const bannersPrepend = process.env.BANNERS_PREPEND || "";
|
||||||
const thumbnailsPrepend = process.env.THUMBNAILS_PREPEND || "";
|
const thumbnailsPrepend = process.env.THUMBNAILS_PREPEND || "";
|
||||||
const creatorsLocation = process.env.CREATORS_LOCATION || "";
|
const creatorsLocation = process.env.CREATORS_LOCATION || "";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @TODO config validation
|
||||||
|
* @returns {IConfiguration}
|
||||||
|
*/
|
||||||
|
function getConfiguration() {
|
||||||
|
const configPath = path.resolve(__dirname, "..", "..", "config.json");
|
||||||
|
// TODO: async reading
|
||||||
|
const fileContent = fs.readFileSync(configPath, { encoding: "utf8" });
|
||||||
|
/**
|
||||||
|
* @type {IConfiguration}
|
||||||
|
*/
|
||||||
|
const config = JSON.parse(fileContent);
|
||||||
|
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
kemonoSite,
|
kemonoSite,
|
||||||
nodeEnv,
|
sentryDSN,
|
||||||
|
siteName,
|
||||||
iconsPrepend,
|
iconsPrepend,
|
||||||
bannersPrepend,
|
bannersPrepend,
|
||||||
thumbnailsPrepend,
|
thumbnailsPrepend,
|
||||||
creatorsLocation,
|
creatorsLocation,
|
||||||
|
artistsOrCreators,
|
||||||
|
disableDMs,
|
||||||
|
disableFAQ,
|
||||||
|
disableFilehaus,
|
||||||
|
sidebarItems,
|
||||||
|
footerItems,
|
||||||
|
bannerGlobal,
|
||||||
|
bannerWelcome,
|
||||||
|
homeBackgroundImage,
|
||||||
|
homeMascotPath,
|
||||||
|
homeLogoPath,
|
||||||
|
paysiteList,
|
||||||
|
homeWelcomeCredits,
|
||||||
|
homeAnnouncements,
|
||||||
|
headerAd,
|
||||||
|
middleAd,
|
||||||
|
footerAd,
|
||||||
|
sliderAd,
|
||||||
|
videoAd,
|
||||||
|
isArchiveServerEnabled,
|
||||||
|
apiServerBaseURL,
|
||||||
|
apiServerPort,
|
||||||
|
analyticsEnabled,
|
||||||
|
analyticsCode,
|
||||||
};
|
};
|
||||||
|
|
10
client/extra.d.ts
vendored
Normal file
10
client/extra.d.ts
vendored
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
// required for typescript not to choke on css modules
|
||||||
|
declare module '*.scss' {
|
||||||
|
const content: Record<string, string>;
|
||||||
|
export default content;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module '*.yaml' {
|
||||||
|
const data: any
|
||||||
|
export default data
|
||||||
|
}
|
|
@ -1,9 +0,0 @@
|
||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
"baseUrl": ".",
|
|
||||||
"module": "commonJS",
|
|
||||||
"target": "es2015",
|
|
||||||
"moduleResolution": "node"
|
|
||||||
},
|
|
||||||
"exclude": ["node_modules", "dist", "dev", "src"]
|
|
||||||
}
|
|
2968
client/package-lock.json
generated
2968
client/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
|
@ -1,37 +1,64 @@
|
||||||
{
|
{
|
||||||
"name": "kemono-2-client",
|
"name": "kemono-2-client",
|
||||||
"version": "0.2.1",
|
"version": "1.0.0",
|
||||||
"description": "frontend for kemono 2",
|
"description": "frontend for kemono 2",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
|
||||||
"dev": "webpack serve --config webpack.dev.js",
|
|
||||||
"build": "webpack --config webpack.prod.js"
|
|
||||||
},
|
|
||||||
"keywords": [],
|
|
||||||
"author": "BassOfBass",
|
"author": "BassOfBass",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "webpack serve --config webpack.dev.js",
|
||||||
|
"validate": "node scripts/validate.mjs",
|
||||||
|
"build": "webpack --config webpack.prod.js"
|
||||||
|
},
|
||||||
|
"imports": {
|
||||||
|
"#storage/*": "./src/browser/storage/*/index.ts",
|
||||||
|
"#hooks": "./src/browser/hooks/index.ts",
|
||||||
|
"#components/*": "./src/components/*/index.ts",
|
||||||
|
"#env/*": "./src/env/*.ts",
|
||||||
|
"#lib/*": "./src/lib/*/index.ts",
|
||||||
|
"#pages/*": "./src/pages/*.tsx",
|
||||||
|
"#entities/*": "./src/entities/*/index.ts",
|
||||||
|
"#css": "./src/css/*.scss",
|
||||||
|
"#assets/*": "./src/assets/*",
|
||||||
|
"#api/*": "./src/api/*/index.ts"
|
||||||
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.22.10",
|
"@babel/runtime": "^7.22.10",
|
||||||
"@uppy/core": "^3.4.0",
|
"@uppy/core": "^3.4.0",
|
||||||
"@uppy/dashboard": "^3.5.1",
|
"@uppy/dashboard": "^3.5.1",
|
||||||
"@uppy/form": "^3.0.2",
|
"@uppy/form": "^3.0.2",
|
||||||
"@uppy/tus": "^3.1.3",
|
"@uppy/tus": "^3.1.3",
|
||||||
|
"clsx": "^2.1.0",
|
||||||
"diff": "^5.1.0",
|
"diff": "^5.1.0",
|
||||||
"fluid-player": "^3.22.0",
|
"fluid-player": "^3.22.0",
|
||||||
"micromodal": "^0.4.10",
|
"micromodal": "^0.4.10",
|
||||||
"purecss": "^3.0.0",
|
"purecss": "^3.0.0",
|
||||||
|
"react": "^18.2.0",
|
||||||
|
"react-dom": "^18.2.0",
|
||||||
|
"react-helmet-async": "^2.0.5",
|
||||||
|
"react-router-dom": "^6.24.0",
|
||||||
"sha256-wasm": "^2.2.2",
|
"sha256-wasm": "^2.2.2",
|
||||||
|
"swagger-ui-react": "^5.17.14",
|
||||||
"whatwg-fetch": "^3.6.17"
|
"whatwg-fetch": "^3.6.17"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.22.10",
|
"@babel/core": "^7.22.10",
|
||||||
"@babel/plugin-transform-runtime": "^7.22.10",
|
"@babel/plugin-transform-runtime": "^7.22.10",
|
||||||
"@babel/preset-env": "^7.22.10",
|
"@babel/preset-env": "^7.22.10",
|
||||||
|
"@babel/preset-react": "^7.24.7",
|
||||||
|
"@babel/preset-typescript": "^7.24.7",
|
||||||
|
"@hyperjump/json-schema": "^1.9.3",
|
||||||
|
"@types/micromodal": "^0.3.5",
|
||||||
|
"@types/node": "^20.1.0",
|
||||||
|
"@types/react": "^18.2.0",
|
||||||
|
"@types/react-dom": "^18.2.0",
|
||||||
|
"@types/sha256-wasm": "^2.2.3",
|
||||||
|
"@types/swagger-ui-react": "^4.18.3",
|
||||||
|
"@types/webpack-bundle-analyzer": "^4.7.0",
|
||||||
"babel-loader": "^8.3.0",
|
"babel-loader": "^8.3.0",
|
||||||
"buffer": "^6.0.3",
|
"buffer": "^6.0.3",
|
||||||
"copy-webpack-plugin": "^8.1.1",
|
"copy-webpack-plugin": "^8.1.1",
|
||||||
"css-loader": "^5.2.7",
|
"css-loader": "^5.2.7",
|
||||||
"dotenv": "^8.6.0",
|
|
||||||
"fs-extra": "^10.1.0",
|
"fs-extra": "^10.1.0",
|
||||||
"html-webpack-plugin": "^5.5.3",
|
"html-webpack-plugin": "^5.5.3",
|
||||||
"mini-css-extract-plugin": "^1.6.2",
|
"mini-css-extract-plugin": "^1.6.2",
|
||||||
|
@ -43,10 +70,14 @@
|
||||||
"sass-loader": "^11.1.1",
|
"sass-loader": "^11.1.1",
|
||||||
"stream-browserify": "^3.0.0",
|
"stream-browserify": "^3.0.0",
|
||||||
"style-loader": "^2.0.0",
|
"style-loader": "^2.0.0",
|
||||||
|
"ts-loader": "^9.5.1",
|
||||||
|
"typescript": "^5.3.3",
|
||||||
"webpack": "^5.88.2",
|
"webpack": "^5.88.2",
|
||||||
|
"webpack-bundle-analyzer": "^4.10.2",
|
||||||
"webpack-cli": "^5.1.4",
|
"webpack-cli": "^5.1.4",
|
||||||
"webpack-dev-server": "^4.15.1",
|
"webpack-dev-server": "^4.15.1",
|
||||||
"webpack-manifest-plugin": "^5.0.0",
|
"webpack-manifest-plugin": "^5.0.0",
|
||||||
"webpack-merge": "^5.9.0"
|
"webpack-merge": "^5.9.0",
|
||||||
|
"yaml": "^2.4.5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
29
client/scripts/validate.mjs
Normal file
29
client/scripts/validate.mjs
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
// @ts-check
|
||||||
|
import path from "node:path";
|
||||||
|
import fs from "node:fs/promises";
|
||||||
|
import { cwd } from "node:process";
|
||||||
|
import { validate } from "@hyperjump/json-schema/openapi-3-1";
|
||||||
|
import YAML from "yaml";
|
||||||
|
|
||||||
|
const schemaPath = path.join(cwd(), "..", "src", "pages", "api", "schema.yaml");
|
||||||
|
|
||||||
|
run().catch((error) => {
|
||||||
|
console.error(error);
|
||||||
|
process.exitCode = 1;
|
||||||
|
});
|
||||||
|
|
||||||
|
async function run() {
|
||||||
|
const fileContent = await fs.readFile(schemaPath, { encoding: "utf8" });
|
||||||
|
const parsedSchema = YAML.parse(fileContent)
|
||||||
|
const output = await validate(
|
||||||
|
"https://spec.openapis.org/oas/3.1/schema-base",
|
||||||
|
parsedSchema,
|
||||||
|
// the library doesn't support values beyond `"FLAG"` and `"BASIC"`
|
||||||
|
// but it's the only library in js which can validate OpenAPI 3.1 schemas
|
||||||
|
"BASIC"
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!output.valid) {
|
||||||
|
throw new Error("Failed to validate OpenAPI Schema.")
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,2 +0,0 @@
|
||||||
export { kemonoAPI } from "./kemono/_index";
|
|
||||||
export { paysitesAPI } from "./paysites/_index";
|
|
19
client/src/api/account/account.ts
Normal file
19
client/src/api/account/account.ts
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
import { IAccount } from "#entities/account";
|
||||||
|
import { apiFetch } from "../fetch";
|
||||||
|
|
||||||
|
interface IResult {
|
||||||
|
props: {
|
||||||
|
currentPage: "account";
|
||||||
|
title: string;
|
||||||
|
account: IAccount;
|
||||||
|
notifications_count: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchAccount() {
|
||||||
|
const path = "/account";
|
||||||
|
|
||||||
|
const result = await apiFetch<IResult>(path, { method: "GET" });
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
40
client/src/api/account/administrator/accounts.ts
Normal file
40
client/src/api/account/administrator/accounts.ts
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
import { IAccontRole, IAccount } from "#entities/account";
|
||||||
|
import { IPagination } from "#lib/pagination";
|
||||||
|
import { apiFetch } from "../../fetch";
|
||||||
|
|
||||||
|
interface IResult {
|
||||||
|
pagination: IPagination;
|
||||||
|
accounts: IAccount[];
|
||||||
|
role_list: IAccontRole[];
|
||||||
|
currentPage: "admin";
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchAccounts(
|
||||||
|
page?: number,
|
||||||
|
name?: string,
|
||||||
|
role?: string,
|
||||||
|
limit?: number
|
||||||
|
) {
|
||||||
|
const path = `/account/administrator/accounts`;
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
|
||||||
|
if (page) {
|
||||||
|
params.set("page", String(page));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name) {
|
||||||
|
params.set("name", name);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (role) {
|
||||||
|
params.set("role", role);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (limit) {
|
||||||
|
params.set("limit", String(limit));
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await apiFetch<IResult>(path, { method: "GET" }, params);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
26
client/src/api/account/administrator/change-roles.ts
Normal file
26
client/src/api/account/administrator/change-roles.ts
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
import { apiFetch } from "../../fetch";
|
||||||
|
|
||||||
|
interface IBody {
|
||||||
|
moderator?: number[];
|
||||||
|
consumer?: number[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchChangeRolesOfAccounts(
|
||||||
|
moderators?: string[],
|
||||||
|
consumers?: string[]
|
||||||
|
) {
|
||||||
|
const path = `/account/administrator/accounts`;
|
||||||
|
const body: IBody = {};
|
||||||
|
|
||||||
|
if (moderators && moderators.length !== 0) {
|
||||||
|
body.moderator = moderators.map((id) => Number(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (consumers && consumers.length !== 0) {
|
||||||
|
body.consumer = consumers.map((id) => Number(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
await apiFetch(path, { method: "POST", body });
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
2
client/src/api/account/administrator/index.ts
Normal file
2
client/src/api/account/administrator/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
export { fetchAccounts } from "./accounts";
|
||||||
|
export { fetchChangeRolesOfAccounts } from "./change-roles";
|
19
client/src/api/account/auto-import-keys/get.ts
Normal file
19
client/src/api/account/auto-import-keys/get.ts
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
import { IAutoImportKey } from "#entities/account";
|
||||||
|
import { apiFetch } from "../../fetch";
|
||||||
|
|
||||||
|
interface IResult {
|
||||||
|
props: {
|
||||||
|
currentPage: "account";
|
||||||
|
title: "Your service keys";
|
||||||
|
service_keys: IAutoImportKey[];
|
||||||
|
};
|
||||||
|
import_ids: { key_id: string; import_id: string }[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchAccountAutoImportKeys() {
|
||||||
|
const path = `/account/keys`;
|
||||||
|
|
||||||
|
const result = await apiFetch<IResult>(path, { method: "GET" });
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
2
client/src/api/account/auto-import-keys/index.ts
Normal file
2
client/src/api/account/auto-import-keys/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
export { fetchAccountAutoImportKeys } from "./get";
|
||||||
|
export { fetchRevokeAutoImportKeys } from "./revoke";
|
15
client/src/api/account/auto-import-keys/revoke.ts
Normal file
15
client/src/api/account/auto-import-keys/revoke.ts
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
import { apiFetch } from "../../fetch";
|
||||||
|
|
||||||
|
interface IBody {
|
||||||
|
revoke: number[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchRevokeAutoImportKeys(keyIDs: number[]) {
|
||||||
|
const path = `/account/keys`;
|
||||||
|
const body: IBody = {
|
||||||
|
revoke: keyIDs,
|
||||||
|
};
|
||||||
|
await apiFetch(path, { method: "POST", body });
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
24
client/src/api/account/change-password.ts
Normal file
24
client/src/api/account/change-password.ts
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
import { apiFetch } from "../fetch";
|
||||||
|
|
||||||
|
interface IBody {
|
||||||
|
"current-password": string;
|
||||||
|
"new-password": string;
|
||||||
|
"new-password-confirmation": string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchAccountChangePassword(
|
||||||
|
currentPassword: string,
|
||||||
|
newPassword: string,
|
||||||
|
newPasswordConfirmation: string
|
||||||
|
) {
|
||||||
|
const path = `/account/change_password`;
|
||||||
|
const body: IBody = {
|
||||||
|
"current-password": currentPassword,
|
||||||
|
"new-password": newPassword,
|
||||||
|
"new-password-confirmation": newPasswordConfirmation,
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await apiFetch<true>(path, { method: "POST", body });
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
22
client/src/api/account/dms/get.ts
Normal file
22
client/src/api/account/dms/get.ts
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
import { IUnapprovedDM } from "#entities/dms";
|
||||||
|
import { apiFetch } from "../../fetch";
|
||||||
|
|
||||||
|
interface IResult {
|
||||||
|
currentPage: "import";
|
||||||
|
account_id: number;
|
||||||
|
status: "ignored" | "pending";
|
||||||
|
dms: IUnapprovedDM[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchDMsForReview(status?: "ignored" | "pending") {
|
||||||
|
const path = `/account/review_dms`;
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
|
||||||
|
if (status) {
|
||||||
|
params.set("status", status);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await apiFetch<IResult>(path, { method: "GET" }, params);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
2
client/src/api/account/dms/index.ts
Normal file
2
client/src/api/account/dms/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
export { fetchDMsForReview } from "./get";
|
||||||
|
export { fetchApproveDMs } from "./review"
|
21
client/src/api/account/dms/review.ts
Normal file
21
client/src/api/account/dms/review.ts
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
import { apiFetch } from "../../fetch";
|
||||||
|
|
||||||
|
interface IBody {
|
||||||
|
approved_hashes: string[];
|
||||||
|
delete_ignored?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchApproveDMs(
|
||||||
|
hashes: string[],
|
||||||
|
isIgnoredDeleted?: boolean
|
||||||
|
) {
|
||||||
|
const path = `/account/review_dms`;
|
||||||
|
const body: IBody = {
|
||||||
|
approved_hashes: hashes,
|
||||||
|
delete_ignored: isIgnoredDeleted,
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await apiFetch<true>(path, { method: "POST", body });
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
25
client/src/api/account/favorites/favorite-post.ts
Normal file
25
client/src/api/account/favorites/favorite-post.ts
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
import { apiFetch } from "../../fetch";
|
||||||
|
|
||||||
|
export async function apiFavoritePost(
|
||||||
|
service: string,
|
||||||
|
profileID: string,
|
||||||
|
postID: string
|
||||||
|
) {
|
||||||
|
const path = `/favorites/post/${service}/${profileID}/${postID}`;
|
||||||
|
|
||||||
|
await apiFetch(path, { method: "POST" });
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function apiUnfavoritePost(
|
||||||
|
service: string,
|
||||||
|
profileID: string,
|
||||||
|
postID: string
|
||||||
|
) {
|
||||||
|
const path = `/favorites/post/${service}/${profileID}/${postID}`;
|
||||||
|
|
||||||
|
await apiFetch(path, { method: "DELETE" });
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
17
client/src/api/account/favorites/favorite-profile.ts
Normal file
17
client/src/api/account/favorites/favorite-profile.ts
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
import { apiFetch } from "../../fetch";
|
||||||
|
|
||||||
|
export async function apiFavoriteProfile(service: string, profileID: string) {
|
||||||
|
const path = `/favorites/creator/${service}/${profileID}`;
|
||||||
|
|
||||||
|
await apiFetch(path, { method: "POST" });
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function apiUnfavoriteProfile(service: string, profileID: string) {
|
||||||
|
const path = `/favorites/creator/${service}/${profileID}`;
|
||||||
|
|
||||||
|
await apiFetch(path, { method: "DELETE" });
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
15
client/src/api/account/favorites/get-favourite-artists.ts
Normal file
15
client/src/api/account/favorites/get-favourite-artists.ts
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
import { IFavouriteArtist } from "#entities/account";
|
||||||
|
import { apiFetch } from "../../fetch";
|
||||||
|
|
||||||
|
export async function fetchFavouriteProfiles() {
|
||||||
|
const path = `/account/favorites`;
|
||||||
|
const params = new URLSearchParams([["type", "artist"]]);
|
||||||
|
|
||||||
|
const data = await apiFetch<IFavouriteArtist[]>(
|
||||||
|
path,
|
||||||
|
{ method: "GET" },
|
||||||
|
params
|
||||||
|
);
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
15
client/src/api/account/favorites/get-favourite-posts.ts
Normal file
15
client/src/api/account/favorites/get-favourite-posts.ts
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
import { IFavouritePost } from "#entities/account";
|
||||||
|
import { apiFetch } from "../../fetch";
|
||||||
|
|
||||||
|
export async function fetchFavouritePosts() {
|
||||||
|
const path = `/account/favorites`;
|
||||||
|
const params = new URLSearchParams([["type", "post"]]);
|
||||||
|
|
||||||
|
const data = await apiFetch<IFavouritePost[]>(
|
||||||
|
path,
|
||||||
|
{ method: "GET" },
|
||||||
|
params
|
||||||
|
);
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
4
client/src/api/account/favorites/index.ts
Normal file
4
client/src/api/account/favorites/index.ts
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
export { fetchFavouriteProfiles } from "./get-favourite-artists";
|
||||||
|
export { fetchFavouritePosts } from "./get-favourite-posts";
|
||||||
|
export { apiFavoritePost, apiUnfavoritePost } from "./favorite-post";
|
||||||
|
export { apiFavoriteProfile, apiUnfavoriteProfile } from "./favorite-profile";
|
4
client/src/api/account/index.ts
Normal file
4
client/src/api/account/index.ts
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
export { fetchAccount } from "./account";
|
||||||
|
export { fetchAccountNotifications } from "./notifications";
|
||||||
|
export { fetchAddProfileLink } from "./profiles";
|
||||||
|
export { fetchAccountChangePassword } from "./change-password";
|
5
client/src/api/account/moderator/index.ts
Normal file
5
client/src/api/account/moderator/index.ts
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
export {
|
||||||
|
fetchProfileLinkRequests,
|
||||||
|
fetchApproveLinkRequest,
|
||||||
|
fetchRejectLinkRequest,
|
||||||
|
} from "./profile-link-requests";
|
30
client/src/api/account/moderator/profile-link-requests.ts
Normal file
30
client/src/api/account/moderator/profile-link-requests.ts
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
import { IProfileLinkRequest } from "#entities/account";
|
||||||
|
import { apiFetch } from "../../fetch";
|
||||||
|
|
||||||
|
export async function fetchProfileLinkRequests() {
|
||||||
|
const path = `/account/moderator/tasks/creator_links`;
|
||||||
|
|
||||||
|
const linkRequests = await apiFetch<IProfileLinkRequest[]>(path, {
|
||||||
|
method: "GET",
|
||||||
|
});
|
||||||
|
|
||||||
|
return linkRequests;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchApproveLinkRequest(requestID: string) {
|
||||||
|
const path = `/account/moderator/creator_link_requests/${requestID}/approve`;
|
||||||
|
const resp = await apiFetch<{ response: "approved" }>(path, {
|
||||||
|
method: "POST",
|
||||||
|
});
|
||||||
|
|
||||||
|
return resp;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchRejectLinkRequest(requestID: string) {
|
||||||
|
const path = `/account/moderator/creator_link_requests/${requestID}/reject`;
|
||||||
|
const resp = await apiFetch<{ response: "rejected" }>(path, {
|
||||||
|
method: "POST",
|
||||||
|
});
|
||||||
|
|
||||||
|
return resp;
|
||||||
|
}
|
17
client/src/api/account/notifications.ts
Normal file
17
client/src/api/account/notifications.ts
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
import { INotification } from "#entities/account";
|
||||||
|
import { apiFetch } from "../fetch";
|
||||||
|
|
||||||
|
interface IResult {
|
||||||
|
props: {
|
||||||
|
currentPage: "account";
|
||||||
|
notifications: INotification[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchAccountNotifications() {
|
||||||
|
const path = "/account/notifications";
|
||||||
|
|
||||||
|
const result = await apiFetch<IResult>(path, { method: "GET" });
|
||||||
|
|
||||||
|
return result.props;
|
||||||
|
}
|
42
client/src/api/account/profiles.ts
Normal file
42
client/src/api/account/profiles.ts
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
import { IArtist } from "#entities/profiles";
|
||||||
|
import { apiFetch } from "../fetch";
|
||||||
|
|
||||||
|
interface IResult {
|
||||||
|
message: string
|
||||||
|
props: {
|
||||||
|
id: string
|
||||||
|
service: string
|
||||||
|
artist: IArtist
|
||||||
|
share_count: number
|
||||||
|
has_links: "✔️" | "0"
|
||||||
|
display_data: {
|
||||||
|
service: string
|
||||||
|
href: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IBody {
|
||||||
|
service: string;
|
||||||
|
artist_id: string;
|
||||||
|
reason?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchAddProfileLink(
|
||||||
|
service: string,
|
||||||
|
profileID: string,
|
||||||
|
linkService: string,
|
||||||
|
linkProfileID: string,
|
||||||
|
reason?: string
|
||||||
|
) {
|
||||||
|
const path = `/${service}/user/${profileID}/links/new`;
|
||||||
|
const body: IBody = {
|
||||||
|
service: linkService,
|
||||||
|
artist_id: linkProfileID,
|
||||||
|
reason,
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await apiFetch<IResult>(path, { method: "POST", body });
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
3
client/src/api/authentication/index.ts
Normal file
3
client/src/api/authentication/index.ts
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
export { fetchRegisterAccount } from "./register";
|
||||||
|
export { fetchLoginAccount } from "./login";
|
||||||
|
export { fetchLogoutAccount } from "./logout";
|
29
client/src/api/authentication/login.ts
Normal file
29
client/src/api/authentication/login.ts
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
import { IAccount } from "#entities/account";
|
||||||
|
import { apiFetch } from "../fetch";
|
||||||
|
import { ensureAPIError } from "#lib/api";
|
||||||
|
import { fetchAccount } from "../account/account";
|
||||||
|
|
||||||
|
export async function fetchLoginAccount(username: string, password: string) {
|
||||||
|
const path = `/authentication/login`;
|
||||||
|
const body = {
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await apiFetch<IAccount>(path, { method: "POST", body });
|
||||||
|
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
ensureAPIError(error);
|
||||||
|
|
||||||
|
// account is already logged in
|
||||||
|
if (error.response.status !== 409) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await fetchAccount();
|
||||||
|
|
||||||
|
return result.props.account;
|
||||||
|
}
|
||||||
|
}
|
9
client/src/api/authentication/logout.ts
Normal file
9
client/src/api/authentication/logout.ts
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
import { apiFetch } from "../fetch";
|
||||||
|
|
||||||
|
export async function fetchLogoutAccount() {
|
||||||
|
const path = `/authentication/logout`;
|
||||||
|
|
||||||
|
const result = await apiFetch<true>(path, { method: "POST"});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
20
client/src/api/authentication/register.ts
Normal file
20
client/src/api/authentication/register.ts
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
import { apiFetch } from "../fetch";
|
||||||
|
|
||||||
|
export async function fetchRegisterAccount(
|
||||||
|
userName: string,
|
||||||
|
password: string,
|
||||||
|
confirmPassword: string,
|
||||||
|
favorites?: string
|
||||||
|
) {
|
||||||
|
const path = `/authentication/register`;
|
||||||
|
const body = {
|
||||||
|
username: userName,
|
||||||
|
password,
|
||||||
|
confirm_password: confirmPassword,
|
||||||
|
favorites_json: favorites,
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await apiFetch<true>(path, { method: "POST", body });
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
29
client/src/api/dms/all.ts
Normal file
29
client/src/api/dms/all.ts
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
import { IApprovedDM } from "#entities/dms";
|
||||||
|
import { apiFetch } from "../fetch";
|
||||||
|
|
||||||
|
interface IResult {
|
||||||
|
props: {
|
||||||
|
currentPage: "artists";
|
||||||
|
count: number;
|
||||||
|
limit: number;
|
||||||
|
dms: IApprovedDM[];
|
||||||
|
};
|
||||||
|
base: {};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchDMs(offset?: number, query?: string) {
|
||||||
|
const path = "/dms";
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
|
||||||
|
if (offset) {
|
||||||
|
params.set("o", String(offset));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (query) {
|
||||||
|
params.set("q", query);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await apiFetch<IResult>(path, { method: "GET" }, params);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
8
client/src/api/dms/has-pending.ts
Normal file
8
client/src/api/dms/has-pending.ts
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
import { apiFetch } from "../fetch";
|
||||||
|
|
||||||
|
export async function fetchHasPendingDMs() {
|
||||||
|
const path = `/has_pending_dms`;
|
||||||
|
const result = await apiFetch<boolean>(path, { method: "GET" });
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
3
client/src/api/dms/index.ts
Normal file
3
client/src/api/dms/index.ts
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
export { fetchDMs } from "./all";
|
||||||
|
export { fetchProfileDMs } from "./profile";
|
||||||
|
export { fetchHasPendingDMs } from "./has-pending";
|
27
client/src/api/dms/profile.ts
Normal file
27
client/src/api/dms/profile.ts
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
import { IArtist } from "#entities/profiles";
|
||||||
|
import { IApprovedDM } from "#entities/dms";
|
||||||
|
import { apiFetch } from "../fetch";
|
||||||
|
|
||||||
|
interface IResult {
|
||||||
|
props: {
|
||||||
|
id: string;
|
||||||
|
service: string;
|
||||||
|
artist: IArtist;
|
||||||
|
display_data: {
|
||||||
|
service: string;
|
||||||
|
href: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
share_count: number;
|
||||||
|
dm_count: number;
|
||||||
|
dms: IApprovedDM[];
|
||||||
|
has_links: "✔️" | "0";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchProfileDMs(service: string, profileID: string) {
|
||||||
|
const path = `/${service}/user/${profileID}/dms`;
|
||||||
|
const result = await apiFetch<IResult>(path, { method: "GET" });
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
144
client/src/api/fetch.ts
Normal file
144
client/src/api/fetch.ts
Normal file
|
@ -0,0 +1,144 @@
|
||||||
|
import { logoutAccount } from "#entities/account";
|
||||||
|
import { HTTP_STATUS } from "#lib/http";
|
||||||
|
import { APIError } from "#lib/api";
|
||||||
|
|
||||||
|
const urlBase = `/api/v1`;
|
||||||
|
const jsonHeaders = new Headers();
|
||||||
|
jsonHeaders.append("Content-Type", "application/json");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TODO: discriminated union with JSONable body signature
|
||||||
|
*/
|
||||||
|
interface IOptions extends Omit<RequestInit, "headers"> {
|
||||||
|
method: "GET" | "POST" | "DELETE";
|
||||||
|
body?: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generic request for Kemono API.
|
||||||
|
* @param path
|
||||||
|
* A path to the endpoint, realtive to the base API path.
|
||||||
|
*/
|
||||||
|
export async function apiFetch<ReturnShape>(
|
||||||
|
path: string,
|
||||||
|
options: IOptions,
|
||||||
|
searchParams?: URLSearchParams
|
||||||
|
): Promise<ReturnShape> {
|
||||||
|
// `URL` constructor requires a full origin
|
||||||
|
// to be present in either of arguments
|
||||||
|
// but the argument for `fetch()` accepts relative paths just fine
|
||||||
|
// so we are doing some gymnastics in order not to depend
|
||||||
|
// on browser context (does not exist on server)
|
||||||
|
// or an env variable (not needed if the origin is the same).
|
||||||
|
const url = new URL(`${urlBase}${path}`, "https://example.com");
|
||||||
|
url.search = !searchParams ? "" : String(searchParams);
|
||||||
|
|
||||||
|
url.searchParams.sort();
|
||||||
|
|
||||||
|
const apiPath = `${url.pathname}${
|
||||||
|
// `URL.search` param includes `?` even with no params
|
||||||
|
// so we include it conditionally
|
||||||
|
searchParams?.size !== 0 ? url.search : ""
|
||||||
|
}`;
|
||||||
|
|
||||||
|
let finalOptions: RequestInit;
|
||||||
|
{
|
||||||
|
if (!options.body) {
|
||||||
|
finalOptions = {
|
||||||
|
...options,
|
||||||
|
credentials: "same-origin",
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
const jsonBody = JSON.stringify(options.body);
|
||||||
|
finalOptions = {
|
||||||
|
...options,
|
||||||
|
headers: jsonHeaders,
|
||||||
|
body: jsonBody,
|
||||||
|
credentials: "same-origin",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const request = new Request(apiPath, finalOptions);
|
||||||
|
const response = await fetch(request);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
// server logged the account out
|
||||||
|
if (response.status === 401) {
|
||||||
|
await logoutAccount(true);
|
||||||
|
|
||||||
|
throw new APIError(
|
||||||
|
`Failed to fetch from API due to lack of credentials. Reason: ${response.status} - ${response.statusText}.`,
|
||||||
|
{ request, response }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.status === 400 || response.status === 422) {
|
||||||
|
let body: string | undefined;
|
||||||
|
// doing it this way because response doesn't allow
|
||||||
|
// parsing body several times
|
||||||
|
// and cloning response is a bit too much
|
||||||
|
const text = (await response.text()).trim();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const json = JSON.parse(text);
|
||||||
|
body = JSON.stringify(json);
|
||||||
|
} catch (error) {
|
||||||
|
body = text;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new APIError(
|
||||||
|
`Failed to fetch from API due to client inputs. Reason: ${
|
||||||
|
response.status
|
||||||
|
} - ${response.statusText}.${!body ? "" : ` ${body}`}`,
|
||||||
|
{ request, response }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.status === 404) {
|
||||||
|
let body: string | undefined;
|
||||||
|
// doing it this way because response doesn't allow
|
||||||
|
// parsing body several times
|
||||||
|
// and cloning response is a bit too much
|
||||||
|
const text = (await response.text()).trim();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const json = JSON.parse(text);
|
||||||
|
body = JSON.stringify(json);
|
||||||
|
} catch (error) {
|
||||||
|
body = text;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new APIError(
|
||||||
|
`Failed to fetch from API because path "${
|
||||||
|
response.url
|
||||||
|
}" doesn't exist. Reason: ${response.status} - ${response.statusText}.${
|
||||||
|
!body ? "" : ` ${body}`
|
||||||
|
}`,
|
||||||
|
{ request, response }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.status === HTTP_STATUS.SERVICE_UNAVAILABLE) {
|
||||||
|
throw new APIError("API is in maintenance or not available.", {
|
||||||
|
request,
|
||||||
|
response,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.status >= 500) {
|
||||||
|
throw new APIError("Failed to fetch from API due to server error.", {
|
||||||
|
request,
|
||||||
|
response,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new APIError("Failed to fetch from API for unknown reasons.", {
|
||||||
|
request,
|
||||||
|
response,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const resultBody: ReturnShape = await response.json();
|
||||||
|
|
||||||
|
return resultBody;
|
||||||
|
}
|
30
client/src/api/files/archive-file.ts
Normal file
30
client/src/api/files/archive-file.ts
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
import { IArchiveFile } from "#entities/files";
|
||||||
|
import { apiFetch } from "../fetch";
|
||||||
|
|
||||||
|
interface IResult {
|
||||||
|
archive: IArchiveFile | null
|
||||||
|
file_serving_enabled: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchArchiveFile(fileHash: string) {
|
||||||
|
const path = `/posts/archives/${fileHash}`
|
||||||
|
|
||||||
|
const result = await apiFetch<IResult>( path, { method: "GET" })
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchSetArchiveFilePassword(
|
||||||
|
archiveHash: string,
|
||||||
|
password: string
|
||||||
|
) {
|
||||||
|
const path = `/set_password`;
|
||||||
|
const params = new URLSearchParams([
|
||||||
|
["file_hash", archiveHash],
|
||||||
|
["password", password],
|
||||||
|
]);
|
||||||
|
|
||||||
|
const result = await apiFetch(path, { method: "GET" }, params);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
2
client/src/api/files/index.ts
Normal file
2
client/src/api/files/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
export { fetchArchiveFile, fetchSetArchiveFilePassword } from "./archive-file";
|
||||||
|
export { fetchSearchFileByHash } from "./search-by-hash";
|
56
client/src/api/files/search-by-hash.ts
Normal file
56
client/src/api/files/search-by-hash.ts
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
import { apiFetch } from "../fetch";
|
||||||
|
|
||||||
|
interface IResult {
|
||||||
|
id: number;
|
||||||
|
hash: string;
|
||||||
|
mtime: string;
|
||||||
|
|
||||||
|
ctime: string;
|
||||||
|
|
||||||
|
mime: string;
|
||||||
|
ext: string;
|
||||||
|
added: string;
|
||||||
|
|
||||||
|
size: number;
|
||||||
|
ihash: string;
|
||||||
|
posts: IPostResult[];
|
||||||
|
|
||||||
|
discord_posts: IDiscordPostResult[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IPostResult {
|
||||||
|
file_id: number;
|
||||||
|
id: string;
|
||||||
|
user: string;
|
||||||
|
service: string;
|
||||||
|
title: string;
|
||||||
|
substring: string;
|
||||||
|
published: string;
|
||||||
|
|
||||||
|
file: {
|
||||||
|
name: string;
|
||||||
|
path: string;
|
||||||
|
};
|
||||||
|
attachments: { name: string; path: string }[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IDiscordPostResult {
|
||||||
|
file_id: number;
|
||||||
|
id: string;
|
||||||
|
server: string;
|
||||||
|
channel: string;
|
||||||
|
substring: string;
|
||||||
|
published: string;
|
||||||
|
|
||||||
|
embeds: unknown[];
|
||||||
|
mentions: unknown[];
|
||||||
|
attachments: { name: string; path: string }[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchSearchFileByHash(fileHash: string) {
|
||||||
|
const path = `/search_hash/${fileHash}`;
|
||||||
|
|
||||||
|
const result = await apiFetch<IResult>(path, { method: "GET" });
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
26
client/src/api/imports/create-import.ts
Normal file
26
client/src/api/imports/create-import.ts
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
import { apiFetch } from "../fetch";
|
||||||
|
|
||||||
|
interface IBody {
|
||||||
|
session_key: string;
|
||||||
|
service: string;
|
||||||
|
auto_import?: string;
|
||||||
|
save_session_key?: string;
|
||||||
|
save_dms?: boolean;
|
||||||
|
channel_ids?: string;
|
||||||
|
"x-bc"?: string;
|
||||||
|
auth_id?: string;
|
||||||
|
user_agent?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IResult {
|
||||||
|
import_id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchCreateImport(input: IBody) {
|
||||||
|
const path = `/importer/submit`;
|
||||||
|
const body: IBody = input;
|
||||||
|
|
||||||
|
const result = await apiFetch<IResult>(path, { method: "POST", body });
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
9
client/src/api/imports/get-import.ts
Normal file
9
client/src/api/imports/get-import.ts
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
import { apiFetch } from "../fetch";
|
||||||
|
|
||||||
|
export async function fetchImportLogs(importID: string) {
|
||||||
|
const path = `/importer/logs/${importID}`;
|
||||||
|
|
||||||
|
const result = await apiFetch<string[]>(path, { method: "GET" });
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
2
client/src/api/imports/index.ts
Normal file
2
client/src/api/imports/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
export { fetchImportLogs } from "./get-import";
|
||||||
|
export { fetchCreateImport } from "./create-import";
|
|
@ -1,14 +0,0 @@
|
||||||
import { favorites } from "./favorites";
|
|
||||||
import { posts } from "./posts";
|
|
||||||
import { api } from "./api";
|
|
||||||
import { dms } from "./dms";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @type {KemonoAPI}
|
|
||||||
*/
|
|
||||||
export const kemonoAPI = {
|
|
||||||
favorites,
|
|
||||||
posts,
|
|
||||||
api,
|
|
||||||
dms,
|
|
||||||
};
|
|
|
@ -1,100 +0,0 @@
|
||||||
import { KemonoError } from "@wp/utils";
|
|
||||||
import { kemonoFetch } from "./kemono-fetch";
|
|
||||||
import { CREATORS_LOCATION } from "@wp/env/env-vars";
|
|
||||||
|
|
||||||
export const api = {
|
|
||||||
bans,
|
|
||||||
bannedArtist,
|
|
||||||
creators,
|
|
||||||
logs,
|
|
||||||
};
|
|
||||||
|
|
||||||
async function bans() {
|
|
||||||
try {
|
|
||||||
const response = await kemonoFetch("/api/v1/creators/bans", { method: "GET" });
|
|
||||||
|
|
||||||
if (!response || !response.ok) {
|
|
||||||
alert(new KemonoError(6));
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @type {KemonoAPI.API.BanItem[]}
|
|
||||||
*/
|
|
||||||
const banItems = await response.json();
|
|
||||||
|
|
||||||
return banItems;
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {string} id
|
|
||||||
* @param {string} service
|
|
||||||
*/
|
|
||||||
async function bannedArtist(id, service) {
|
|
||||||
const params = new URLSearchParams([["service", service]]).toString();
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await kemonoFetch(`/api/v1/lookup/cache/${id}?${params}`);
|
|
||||||
|
|
||||||
if (!response || !response.ok) {
|
|
||||||
alert(new KemonoError(7));
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @type {KemonoAPI.API.BannedArtist}
|
|
||||||
*/
|
|
||||||
const artist = await response.json();
|
|
||||||
|
|
||||||
return artist;
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function creators() {
|
|
||||||
try {
|
|
||||||
const response = await kemonoFetch(CREATORS_LOCATION || "/api/v1/creators", {
|
|
||||||
method: "GET",
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response || !response.ok) {
|
|
||||||
alert(new KemonoError(8));
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @type {KemonoAPI.User[]}
|
|
||||||
*/
|
|
||||||
const artists = await response.json();
|
|
||||||
|
|
||||||
return artists;
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function logs(importID) {
|
|
||||||
try {
|
|
||||||
const response = await kemonoFetch(`/api/v1/importer/logs/${importID}`, {
|
|
||||||
method: "GET",
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response || !response.ok) {
|
|
||||||
alert(new KemonoError(9));
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @type {KemonoAPI.API.LogItem[]}
|
|
||||||
*/
|
|
||||||
const logs = await response.json();
|
|
||||||
|
|
||||||
return logs;
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,22 +0,0 @@
|
||||||
import { KemonoError } from "@wp/utils";
|
|
||||||
import { kemonoFetch } from "./kemono-fetch";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @type {KemonoAPI.DMs}
|
|
||||||
*/
|
|
||||||
export const dms = {
|
|
||||||
retrieveHasPendingDMs,
|
|
||||||
};
|
|
||||||
|
|
||||||
async function retrieveHasPendingDMs() {
|
|
||||||
try {
|
|
||||||
const response = await kemonoFetch(`/api/v1/has_pending_dms`);
|
|
||||||
|
|
||||||
if (!response || !response.ok) {
|
|
||||||
throw new Error(`Error ${response.status}: ${response.statusText}`);
|
|
||||||
}
|
|
||||||
return await response.json();
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,142 +0,0 @@
|
||||||
import { KemonoError } from "@wp/utils";
|
|
||||||
import { kemonoFetch } from "./kemono-fetch";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @type {KemonoAPI.Favorites}
|
|
||||||
*/
|
|
||||||
export const favorites = {
|
|
||||||
retrieveFavoriteArtists,
|
|
||||||
favoriteArtist,
|
|
||||||
unfavoriteArtist,
|
|
||||||
retrieveFavoritePosts,
|
|
||||||
favoritePost,
|
|
||||||
unfavoritePost,
|
|
||||||
};
|
|
||||||
|
|
||||||
async function retrieveFavoriteArtists() {
|
|
||||||
const params = new URLSearchParams([["type", "artist"]]).toString();
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await kemonoFetch(`/api/v1/account/favorites?${params}`);
|
|
||||||
|
|
||||||
if (!response || !response.ok) {
|
|
||||||
throw new Error(`Error ${response.status}: ${response.statusText}`);
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* @type {string}
|
|
||||||
*/
|
|
||||||
const favs = await response.text();
|
|
||||||
return favs;
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {string} service
|
|
||||||
* @param {string} userID
|
|
||||||
*/
|
|
||||||
async function favoriteArtist(service, userID) {
|
|
||||||
try {
|
|
||||||
const response = await kemonoFetch(`/api/v1/favorites/creator/${service}/${userID}`, { method: "POST" });
|
|
||||||
|
|
||||||
if (!response || !response.ok) {
|
|
||||||
alert(new KemonoError(3));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {string} service
|
|
||||||
* @param {string} userID
|
|
||||||
*/
|
|
||||||
async function unfavoriteArtist(service, userID) {
|
|
||||||
try {
|
|
||||||
const response = await kemonoFetch(`/api/v1/favorites/creator/${service}/${userID}`, { method: "DELETE" });
|
|
||||||
|
|
||||||
if (!response || !response.ok) {
|
|
||||||
alert(new KemonoError(4));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function retrieveFavoritePosts() {
|
|
||||||
const params = new URLSearchParams([["type", "post"]]).toString();
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await kemonoFetch(`/api/v1/account/favorites?${params}`);
|
|
||||||
|
|
||||||
if (!response || !response.ok) {
|
|
||||||
throw new Error(`Error ${response.status}: ${response.statusText}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @type {KemonoAPI.Post[]}
|
|
||||||
*/
|
|
||||||
const favs = await response.json();
|
|
||||||
/**
|
|
||||||
* @type {KemonoAPI.Favorites.Post[]}
|
|
||||||
*/
|
|
||||||
const transformedFavs = favs.map((post) => {
|
|
||||||
return {
|
|
||||||
id: post.id,
|
|
||||||
service: post.service,
|
|
||||||
user: post.user,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
return JSON.stringify(transformedFavs);
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {string} service
|
|
||||||
* @param {string} user
|
|
||||||
* @param {string} post_id
|
|
||||||
*/
|
|
||||||
async function favoritePost(service, user, post_id) {
|
|
||||||
try {
|
|
||||||
const response = await kemonoFetch(`/api/v1/favorites/post/${service}/${user}/${post_id}`, { method: "POST" });
|
|
||||||
|
|
||||||
if (!response || !response.ok) {
|
|
||||||
alert(new KemonoError(1));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {string} service
|
|
||||||
* @param {string} user
|
|
||||||
* @param {string} post_id
|
|
||||||
*/
|
|
||||||
async function unfavoritePost(service, user, post_id) {
|
|
||||||
try {
|
|
||||||
const response = await kemonoFetch(`/api/v1/favorites/post/${service}/${user}/${post_id}`, { method: "DELETE" });
|
|
||||||
|
|
||||||
if (!response || !response.ok) {
|
|
||||||
alert(new KemonoError(2));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,46 +0,0 @@
|
||||||
import { isLoggedIn } from "@wp/js/account";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generic request for Kemono API.
|
|
||||||
* @param {RequestInfo} endpoint
|
|
||||||
* @param {RequestInit} options
|
|
||||||
* @returns {Promise<Response>}
|
|
||||||
*/
|
|
||||||
export async function kemonoFetch(endpoint, options) {
|
|
||||||
try {
|
|
||||||
const response = await fetch(endpoint, options);
|
|
||||||
|
|
||||||
// doing this because the server returns `401` before redirecting
|
|
||||||
// in case of favs
|
|
||||||
if (response.status === 401) {
|
|
||||||
// server logged the account out
|
|
||||||
if (isLoggedIn) {
|
|
||||||
localStorage.removeItem("logged_in");
|
|
||||||
localStorage.removeItem("role");
|
|
||||||
localStorage.removeItem("favs");
|
|
||||||
localStorage.removeItem("post_favs");
|
|
||||||
location.href = "/account/logout";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const loginURL = new URL("/account/login", location.origin).toString();
|
|
||||||
location = addURLParam(loginURL, "location", location.pathname);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
return response;
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`Kemono request error: ${error}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {string} url
|
|
||||||
* @param {string} paramName
|
|
||||||
* @param {string} paramValue
|
|
||||||
* @returns {string}
|
|
||||||
*/
|
|
||||||
function addURLParam(url, paramName, paramValue) {
|
|
||||||
var newURL = new URL(url);
|
|
||||||
newURL.searchParams.set(paramName, paramValue);
|
|
||||||
return newURL.toString();
|
|
||||||
}
|
|
|
@ -1,26 +0,0 @@
|
||||||
import { kemonoFetch } from "./kemono-fetch";
|
|
||||||
import { KemonoError } from "@wp/utils";
|
|
||||||
|
|
||||||
export const posts = {
|
|
||||||
attemptFlag,
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {string} service
|
|
||||||
* @param {string} user
|
|
||||||
* @param {string} post_id
|
|
||||||
*/
|
|
||||||
async function attemptFlag(service, user, post_id) {
|
|
||||||
try {
|
|
||||||
const response = await kemonoFetch(`/api/v1/${service}/user/${user}/post/${post_id}/flag`, { method: "POST" });
|
|
||||||
|
|
||||||
if (!response || !response.ok) {
|
|
||||||
alert(new KemonoError(5));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1 +0,0 @@
|
||||||
export const paysitesAPI = {};
|
|
9
client/src/api/posts/announcements.ts
Normal file
9
client/src/api/posts/announcements.ts
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
import { IAnnouncement } from "#entities/posts";
|
||||||
|
import { apiFetch } from "../fetch";
|
||||||
|
|
||||||
|
export async function fetchAnnouncements(service: string, profileID: string) {
|
||||||
|
const path = `/${service}/user/${profileID}/announcements`;
|
||||||
|
const result = await apiFetch<IAnnouncement[]>(path, { method: "GET" });
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
25
client/src/api/posts/flag.ts
Normal file
25
client/src/api/posts/flag.ts
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
import { ensureAPIError } from "#lib/api";
|
||||||
|
import { HTTP_STATUS } from "#lib/http";
|
||||||
|
import { apiFetch } from "../fetch";
|
||||||
|
|
||||||
|
export async function flagPost(
|
||||||
|
service: string,
|
||||||
|
profileID: string,
|
||||||
|
postID: string
|
||||||
|
) {
|
||||||
|
const path = `/${service}/user/${profileID}/post/${postID}/flag`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await apiFetch(path, { method: "POST" });
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
ensureAPIError(error);
|
||||||
|
|
||||||
|
if (error.response.status !== HTTP_STATUS.CONFLICT) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
7
client/src/api/posts/index.ts
Normal file
7
client/src/api/posts/index.ts
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
export { fetchPosts } from "./posts";
|
||||||
|
export { fetchPost, fetchPostComments, fetchPostData } from "./post";
|
||||||
|
export { fetchPostRevision } from "./revision";
|
||||||
|
export { fetchPopularPosts } from "./popular";
|
||||||
|
export { fetchAnnouncements } from "./announcements";
|
||||||
|
export { fetchRandomPost } from "./random";
|
||||||
|
export { flagPost } from "./flag";
|
53
client/src/api/posts/popular.ts
Normal file
53
client/src/api/posts/popular.ts
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
import { IPopularPostsPeriod, IPostWithFavorites } from "#entities/posts";
|
||||||
|
import { apiFetch } from "../fetch";
|
||||||
|
|
||||||
|
interface IResult {
|
||||||
|
info: {
|
||||||
|
date: string;
|
||||||
|
min_date: string;
|
||||||
|
max_date: string;
|
||||||
|
navigation_dates: Record<IPopularPostsPeriod, [string, string, string]>;
|
||||||
|
range_desc: string;
|
||||||
|
scale: IPopularPostsPeriod;
|
||||||
|
};
|
||||||
|
props: {
|
||||||
|
currentPage: "popular_posts";
|
||||||
|
today: string;
|
||||||
|
earliest_date_for_popular: string;
|
||||||
|
limit: number;
|
||||||
|
count: number;
|
||||||
|
};
|
||||||
|
results: IPostWithFavorites[];
|
||||||
|
base: {};
|
||||||
|
result_previews: (
|
||||||
|
| { type: "thumbnail"; server: string; name: string; path: string }
|
||||||
|
| { type: "embed"; url: string; subject: string; description: string }
|
||||||
|
)[];
|
||||||
|
result_attachments: { server: string; name: string; path: string }[];
|
||||||
|
result_is_image: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchPopularPosts(
|
||||||
|
date?: string,
|
||||||
|
scale?: IPopularPostsPeriod,
|
||||||
|
offset?: number
|
||||||
|
) {
|
||||||
|
const path = `/posts/popular`;
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
|
||||||
|
if (date) {
|
||||||
|
params.set("date", date);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (scale) {
|
||||||
|
params.set("period", scale);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (offset) {
|
||||||
|
params.set("o", String(offset));
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await apiFetch<IResult>(path, { method: "GET" });
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
57
client/src/api/posts/post.ts
Normal file
57
client/src/api/posts/post.ts
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
import {
|
||||||
|
IComment,
|
||||||
|
IPost,
|
||||||
|
IPostAttachment,
|
||||||
|
IPostPreview,
|
||||||
|
IPostRevision,
|
||||||
|
IPostVideo,
|
||||||
|
} from "#entities/posts";
|
||||||
|
import { apiFetch } from "../fetch";
|
||||||
|
|
||||||
|
interface IResult {
|
||||||
|
post: IPost;
|
||||||
|
attachments: IPostAttachment[];
|
||||||
|
previews: IPostPreview[];
|
||||||
|
videos: IPostVideo[];
|
||||||
|
props: {
|
||||||
|
service: string;
|
||||||
|
flagged?: 0;
|
||||||
|
revisions: [number, IPost][];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchPost(
|
||||||
|
service: string,
|
||||||
|
profileID: string,
|
||||||
|
postID: string
|
||||||
|
) {
|
||||||
|
const path = `/${service}/user/${profileID}/post/${postID}`;
|
||||||
|
const result = await apiFetch<IResult>(path, { method: "GET" });
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchPostComments(
|
||||||
|
service: string,
|
||||||
|
profileID: string,
|
||||||
|
postID: string
|
||||||
|
) {
|
||||||
|
const path = `/${service}/user/${profileID}/post/${postID}/comments`;
|
||||||
|
const result = await apiFetch<IComment[]>(path, { method: "GET" });
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IPostData {
|
||||||
|
service: string;
|
||||||
|
artist_id: string;
|
||||||
|
post_id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchPostData(service: string, postID: string) {
|
||||||
|
const path = `/${service}/post/${postID}`;
|
||||||
|
|
||||||
|
const result = await apiFetch<IPostData>(path, { method: "GET" });
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
35
client/src/api/posts/posts.ts
Normal file
35
client/src/api/posts/posts.ts
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
import { IPost } from "#entities/posts";
|
||||||
|
import { apiFetch } from "../fetch";
|
||||||
|
|
||||||
|
interface IResult {
|
||||||
|
count: number;
|
||||||
|
true_count: number;
|
||||||
|
posts: IPost[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchPosts(
|
||||||
|
offset?: number,
|
||||||
|
query?: string,
|
||||||
|
tags?: string[]
|
||||||
|
) {
|
||||||
|
const path = "/posts";
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
|
||||||
|
if (offset) {
|
||||||
|
params.set("o", String(offset));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (query) {
|
||||||
|
params.set("q", query);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tags) {
|
||||||
|
for (const tag of tags) {
|
||||||
|
params.set("tag", tag);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await apiFetch<IResult>(path, { method: "GET" }, params);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
15
client/src/api/posts/random.ts
Normal file
15
client/src/api/posts/random.ts
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
import { apiFetch } from "../fetch";
|
||||||
|
|
||||||
|
interface IResult {
|
||||||
|
service: string;
|
||||||
|
artist_id: string;
|
||||||
|
post_id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchRandomPost() {
|
||||||
|
const path = `/posts/random`;
|
||||||
|
|
||||||
|
const result = await apiFetch<IResult>(path, { method: "GET" });
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
39
client/src/api/posts/revision.ts
Normal file
39
client/src/api/posts/revision.ts
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
import { IArtistDetails } from "#entities/profiles";
|
||||||
|
import {
|
||||||
|
IComment,
|
||||||
|
IPost,
|
||||||
|
IPostAttachment,
|
||||||
|
IPostPreview,
|
||||||
|
IPostRevision,
|
||||||
|
IPostVideo,
|
||||||
|
} from "#entities/posts";
|
||||||
|
import { apiFetch } from "../fetch";
|
||||||
|
|
||||||
|
interface IResult {
|
||||||
|
props: {
|
||||||
|
currentPage: "revisions";
|
||||||
|
service: string;
|
||||||
|
artist: IArtistDetails;
|
||||||
|
flagged?: 0;
|
||||||
|
revisions: [number, IPostRevision][];
|
||||||
|
};
|
||||||
|
post: IPost;
|
||||||
|
comments: IComment[];
|
||||||
|
result_previews: IPostPreview[];
|
||||||
|
result_attachments: IPostAttachment[];
|
||||||
|
videos: IPostVideo[];
|
||||||
|
archives_enabled: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchPostRevision(
|
||||||
|
service: string,
|
||||||
|
profileID: string,
|
||||||
|
postID: string,
|
||||||
|
revisionID: string
|
||||||
|
) {
|
||||||
|
const path = `/${service}/user/${profileID}/post/${postID}/revision/${revisionID}`;
|
||||||
|
|
||||||
|
const result = await apiFetch<IResult>(path, { method: "GET" });
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
29
client/src/api/profiles/discord/index.ts
Normal file
29
client/src/api/profiles/discord/index.ts
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
import { IDiscordChannelMessage } from "#entities/posts";
|
||||||
|
import { apiFetch } from "../../fetch";
|
||||||
|
|
||||||
|
export async function fetchDiscordServer(serverID: string) {
|
||||||
|
const path = `/discord/channel/lookup/${serverID}`;
|
||||||
|
|
||||||
|
const result = await apiFetch<{ id: string; name: string }[]>(path, {
|
||||||
|
method: "GET",
|
||||||
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchDiscordChannel(channelID: string, offset?: number) {
|
||||||
|
const path = `/discord/channel/${channelID}`;
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
|
||||||
|
if (offset) {
|
||||||
|
params.set("o", String(offset));
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await apiFetch<IDiscordChannelMessage[]>(
|
||||||
|
path,
|
||||||
|
{ method: "GET" },
|
||||||
|
params
|
||||||
|
);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
9
client/src/api/profiles/fancards.ts
Normal file
9
client/src/api/profiles/fancards.ts
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
import { IFanCard } from "#entities/files";
|
||||||
|
import { apiFetch } from "../fetch";
|
||||||
|
|
||||||
|
export async function fetchFanboxProfileFancards(profileID: string) {
|
||||||
|
const path = `/fanbox/user/${profileID}/fancards`;
|
||||||
|
const cards = await apiFetch<IFanCard[]>(path, { method: "GET" });
|
||||||
|
|
||||||
|
return cards;
|
||||||
|
}
|
6
client/src/api/profiles/index.ts
Normal file
6
client/src/api/profiles/index.ts
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
export { fetchProfiles } from "./profiles";
|
||||||
|
export { fetchRandomArtist } from "./random";
|
||||||
|
export { fetchArtistProfile } from "./profile";
|
||||||
|
export { fetchFanboxProfileFancards } from "./fancards";
|
||||||
|
export { fetchProfileLinks } from "./links";
|
||||||
|
export { fetchProfilePosts } from "./posts";
|
21
client/src/api/profiles/links.ts
Normal file
21
client/src/api/profiles/links.ts
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
import { apiFetch } from "../fetch";
|
||||||
|
|
||||||
|
interface IResult
|
||||||
|
extends Array<{
|
||||||
|
id: string;
|
||||||
|
|
||||||
|
public_id: string | null;
|
||||||
|
service: string;
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
indexed: string;
|
||||||
|
|
||||||
|
updated: string;
|
||||||
|
}> {}
|
||||||
|
|
||||||
|
export async function fetchProfileLinks(service: string, profileID: string) {
|
||||||
|
const path = `/${service}/user/${profileID}/links`;
|
||||||
|
const links = await apiFetch<IResult>(path, { method: "GET" });
|
||||||
|
|
||||||
|
return links;
|
||||||
|
}
|
58
client/src/api/profiles/posts.ts
Normal file
58
client/src/api/profiles/posts.ts
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
import { IArtist } from "#entities/profiles";
|
||||||
|
import { IPost } from "#entities/posts";
|
||||||
|
import { apiFetch } from "../fetch";
|
||||||
|
|
||||||
|
interface IResult {
|
||||||
|
props: {
|
||||||
|
currentPage: "posts";
|
||||||
|
id: string;
|
||||||
|
service: string;
|
||||||
|
name: string;
|
||||||
|
count: number;
|
||||||
|
limit: number;
|
||||||
|
artist: IArtist;
|
||||||
|
display_data: {
|
||||||
|
service: string;
|
||||||
|
href: string;
|
||||||
|
};
|
||||||
|
dm_count: number;
|
||||||
|
share_count: number;
|
||||||
|
has_links: "0" | "✔️";
|
||||||
|
};
|
||||||
|
|
||||||
|
base: Record<string, unknown>
|
||||||
|
results: IPost[]
|
||||||
|
result_previews: Record<string, unknown>[]
|
||||||
|
result_atachments: Record<string, unknown>[]
|
||||||
|
result_is_image: boolean[]
|
||||||
|
disable_service_icons: true
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchProfilePosts(
|
||||||
|
service: string,
|
||||||
|
profileID: string,
|
||||||
|
offset?: number,
|
||||||
|
query?: string,
|
||||||
|
tags?: string[]
|
||||||
|
) {
|
||||||
|
const path = `/${service}/user/${profileID}/posts-legacy`;
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
|
||||||
|
if (offset) {
|
||||||
|
params.set("o", String(offset));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (query) {
|
||||||
|
params.set("q", query);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tags && tags.length) {
|
||||||
|
for (const tag of tags) {
|
||||||
|
params.append("tag", tag);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await apiFetch<IResult>(path, { method: "GET" }, params);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
14
client/src/api/profiles/profile.ts
Normal file
14
client/src/api/profiles/profile.ts
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
import { IArtistDetails } from "#entities/profiles";
|
||||||
|
import { apiFetch } from "../fetch";
|
||||||
|
|
||||||
|
export async function fetchArtistProfile(
|
||||||
|
service: string,
|
||||||
|
artistID: string
|
||||||
|
): Promise<IArtistDetails> {
|
||||||
|
const path = `/${service}/user/${artistID}/profile`;
|
||||||
|
const result = await apiFetch<IArtistDetails>(path, {
|
||||||
|
method: "GET",
|
||||||
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
12
client/src/api/profiles/profiles.ts
Normal file
12
client/src/api/profiles/profiles.ts
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
import { IArtistWithFavs } from "#entities/profiles";
|
||||||
|
import { IS_DEVELOPMENT } from "#env/derived-vars";
|
||||||
|
import { apiFetch } from "../fetch";
|
||||||
|
|
||||||
|
export async function fetchProfiles(): Promise<IArtistWithFavs[]> {
|
||||||
|
const path = IS_DEVELOPMENT ? "/creators" : "/creators.txt";
|
||||||
|
const result = await apiFetch<IArtistWithFavs[]>(path, {
|
||||||
|
method: "GET",
|
||||||
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
14
client/src/api/profiles/random.ts
Normal file
14
client/src/api/profiles/random.ts
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
import { apiFetch } from "../fetch";
|
||||||
|
|
||||||
|
interface IArtistData {
|
||||||
|
service: string;
|
||||||
|
artist_id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchRandomArtist(): Promise<IArtistData> {
|
||||||
|
const result = await apiFetch<IArtistData>("/artists/random", {
|
||||||
|
method: "GET",
|
||||||
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
3
client/src/api/shares/index.ts
Normal file
3
client/src/api/shares/index.ts
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
export { fetchShares } from "./shares";
|
||||||
|
export { fetchShare } from "./share";
|
||||||
|
export { fetchProfileShares } from "./profile";
|
37
client/src/api/shares/profile.ts
Normal file
37
client/src/api/shares/profile.ts
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
import { IArtist } from "#entities/profiles";
|
||||||
|
import { IShare } from "#entities/files";
|
||||||
|
import { apiFetch } from "../fetch";
|
||||||
|
|
||||||
|
interface IResult {
|
||||||
|
results: IShare[];
|
||||||
|
base: Record<string, unknown>;
|
||||||
|
props: {
|
||||||
|
display_data: {
|
||||||
|
service: string;
|
||||||
|
href: string;
|
||||||
|
};
|
||||||
|
service: string;
|
||||||
|
artist: IArtist;
|
||||||
|
id: string;
|
||||||
|
dm_count: number;
|
||||||
|
share_count: number;
|
||||||
|
has_links: "✔️" | "0";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchProfileShares(
|
||||||
|
service: string,
|
||||||
|
profileID: string,
|
||||||
|
offset?: number
|
||||||
|
) {
|
||||||
|
const path = `/${service}/user/${profileID}/shares`;
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
|
||||||
|
if (offset) {
|
||||||
|
params.set("o", String(offset));
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await apiFetch<IResult>(path, { method: "GET" }, params);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
15
client/src/api/shares/share.ts
Normal file
15
client/src/api/shares/share.ts
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
import { IShare, IShareFile } from "#entities/files";
|
||||||
|
import { apiFetch } from "../fetch";
|
||||||
|
|
||||||
|
interface IResult {
|
||||||
|
share: IShare;
|
||||||
|
share_files: IShareFile[];
|
||||||
|
base: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchShare(shareID: string) {
|
||||||
|
const path = `/share/${shareID}`;
|
||||||
|
const result = await apiFetch<IResult>(path, { method: "GET" });
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
24
client/src/api/shares/shares.ts
Normal file
24
client/src/api/shares/shares.ts
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
import { IShare } from "#entities/files";
|
||||||
|
import { apiFetch } from "../fetch";
|
||||||
|
|
||||||
|
interface IResult {
|
||||||
|
base: Record<string, unknown>;
|
||||||
|
props: {
|
||||||
|
currentPage: "shares";
|
||||||
|
count: number;
|
||||||
|
shares: IShare[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchShares(offset?: number) {
|
||||||
|
const path = `/shares`;
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
|
||||||
|
if (offset) {
|
||||||
|
params.set("o", String(offset));
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await apiFetch<IResult>(path, { method: "GET" }, params);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
14
client/src/api/tags/all.ts
Normal file
14
client/src/api/tags/all.ts
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
import { ITag } from "#entities/tags"
|
||||||
|
import { apiFetch } from "../fetch"
|
||||||
|
|
||||||
|
interface IResult {
|
||||||
|
props: { currentPage: "tags" }
|
||||||
|
tags: ITag[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchTags() {
|
||||||
|
const path = "/posts/tags"
|
||||||
|
const result = await apiFetch<IResult>(path, { method: "GET" })
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
2
client/src/api/tags/index.ts
Normal file
2
client/src/api/tags/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
export { fetchTags } from "./all";
|
||||||
|
export { fetchProfileTags } from "./profile";
|
29
client/src/api/tags/profile.ts
Normal file
29
client/src/api/tags/profile.ts
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
import { IArtist } from "#entities/profiles";
|
||||||
|
import { ITag } from "#entities/tags";
|
||||||
|
import { apiFetch } from "../fetch";
|
||||||
|
|
||||||
|
interface IResult {
|
||||||
|
props: {
|
||||||
|
display_data: {
|
||||||
|
service: string;
|
||||||
|
href: string;
|
||||||
|
};
|
||||||
|
artist: IArtist;
|
||||||
|
service: string;
|
||||||
|
id: string;
|
||||||
|
share_count: number;
|
||||||
|
dm_count: number;
|
||||||
|
has_links: "✔️" | "0";
|
||||||
|
};
|
||||||
|
|
||||||
|
tags: ITag[];
|
||||||
|
service: string;
|
||||||
|
artist: IArtist;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchProfileTags(service: string, profileID: string) {
|
||||||
|
const path = `/${service}/user/${profileID}/tags`;
|
||||||
|
const tags = await apiFetch<IResult>(path, { method: "GET" });
|
||||||
|
|
||||||
|
return tags;
|
||||||
|
}
|
3
client/src/browser/hooks/index.ts
Normal file
3
client/src/browser/hooks/index.ts
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
export { ClientProvider, useClient } from "./use-client";
|
||||||
|
export { useRoutePathPattern } from "./use-route-path-pattern";
|
||||||
|
export { useInterval } from "./use-interval";
|
38
client/src/browser/hooks/use-client.tsx
Normal file
38
client/src/browser/hooks/use-client.tsx
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
import {
|
||||||
|
ReactNode,
|
||||||
|
createContext,
|
||||||
|
useContext,
|
||||||
|
useEffect,
|
||||||
|
useState,
|
||||||
|
} from "react";
|
||||||
|
|
||||||
|
interface IClientContext {
|
||||||
|
isClient: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultContext: IClientContext = { isClient: false };
|
||||||
|
const ClientContext = createContext<IClientContext>(defaultContext);
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
children?: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ClientProvider({ children }: IProps) {
|
||||||
|
const [isClient, switchIsClient] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
switchIsClient(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ClientContext.Provider value={{ isClient }}>
|
||||||
|
{children}
|
||||||
|
</ClientContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useClient(): boolean {
|
||||||
|
const { isClient } = useContext(ClientContext);
|
||||||
|
|
||||||
|
return isClient;
|
||||||
|
}
|
27
client/src/browser/hooks/use-interval.tsx
Normal file
27
client/src/browser/hooks/use-interval.tsx
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
import { useEffect, useRef } from "react";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stolen from
|
||||||
|
* https://overreacted.io/making-setinterval-declarative-with-react-hooks/
|
||||||
|
*/
|
||||||
|
export function useInterval(callback: () => void, delay: number | null) {
|
||||||
|
const savedCallback = useRef<typeof callback>();
|
||||||
|
|
||||||
|
// Remember the latest callback.
|
||||||
|
useEffect(() => {
|
||||||
|
savedCallback.current = callback;
|
||||||
|
}, [callback]);
|
||||||
|
|
||||||
|
// Set up the interval.
|
||||||
|
useEffect(() => {
|
||||||
|
if (delay !== null) {
|
||||||
|
let id = setInterval(tick, delay);
|
||||||
|
|
||||||
|
return () => clearInterval(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
function tick() {
|
||||||
|
savedCallback.current!();
|
||||||
|
}
|
||||||
|
}, [delay]);
|
||||||
|
}
|
11
client/src/browser/hooks/use-route-path-pattern.tsx
Normal file
11
client/src/browser/hooks/use-route-path-pattern.tsx
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
import { useLocation } from "react-router-dom";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TODO: path pattern without circular reference
|
||||||
|
* on the route config object.
|
||||||
|
*/
|
||||||
|
export function useRoutePathPattern(): string {
|
||||||
|
const location = useLocation();
|
||||||
|
|
||||||
|
return location.pathname;
|
||||||
|
}
|
46
client/src/browser/storage/local/index.ts
Normal file
46
client/src/browser/storage/local/index.ts
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
const storageNames = [
|
||||||
|
"favorites",
|
||||||
|
"logged_in",
|
||||||
|
"role",
|
||||||
|
"favs",
|
||||||
|
"post_favs",
|
||||||
|
"has_pending_review_dms",
|
||||||
|
"last_checked_has_pending_review_dms",
|
||||||
|
"sidebar_state",
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export interface ILocalStorageSchema {
|
||||||
|
favs: {
|
||||||
|
service: string;
|
||||||
|
id: string;
|
||||||
|
}[];
|
||||||
|
post_favs: {
|
||||||
|
id: string;
|
||||||
|
service: string;
|
||||||
|
user: string;
|
||||||
|
}[];
|
||||||
|
}
|
||||||
|
|
||||||
|
type ILocalStorageName = (typeof storageNames)[number];
|
||||||
|
|
||||||
|
export function getLocalStorageItem(name: ILocalStorageName) {
|
||||||
|
return localStorage.getItem(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setLocalStorageItem(name: ILocalStorageName, value: string) {
|
||||||
|
localStorage.setItem(name, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deleteLocalStorageItem(name: ILocalStorageName) {
|
||||||
|
localStorage.removeItem(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isLocalStorageAvailable() {
|
||||||
|
try {
|
||||||
|
localStorage.setItem("__storage_test__", "__storage_test__");
|
||||||
|
localStorage.removeItem("__storage_test__");
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
11
client/src/components/_index.scss
Normal file
11
client/src/components/_index.scss
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
@use "layout";
|
||||||
|
@use "pages";
|
||||||
|
@use "images";
|
||||||
|
@use "links";
|
||||||
|
@use "dates";
|
||||||
|
@use "cards";
|
||||||
|
@use "loading";
|
||||||
|
@use "buttons";
|
||||||
|
@use "tooltip";
|
||||||
|
@use "pagination";
|
||||||
|
@use "importer_states";
|
59
client/src/components/ads/ads.tsx
Normal file
59
client/src/components/ads/ads.tsx
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
import { useLocation } from "react-router-dom";
|
||||||
|
import { HEADER_AD, MIDDLE_AD, FOOTER_AD, SLIDER_AD } from "#env/env-vars";
|
||||||
|
import { DangerousContent } from "#components/dangerous-content";
|
||||||
|
|
||||||
|
export function HeaderAd() {
|
||||||
|
const location = useLocation();
|
||||||
|
const key = `${location.pathname}${location.search}`;
|
||||||
|
|
||||||
|
return !HEADER_AD ? undefined : (
|
||||||
|
<DangerousContent
|
||||||
|
key={key}
|
||||||
|
className="ad-container"
|
||||||
|
html={atob(HEADER_AD)}
|
||||||
|
allowRerender
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function MiddleAd() {
|
||||||
|
const location = useLocation();
|
||||||
|
const key = `${location.pathname}${location.search}`;
|
||||||
|
|
||||||
|
return !MIDDLE_AD ? undefined : (
|
||||||
|
<DangerousContent
|
||||||
|
key={key}
|
||||||
|
className="ad-container"
|
||||||
|
html={atob(MIDDLE_AD)}
|
||||||
|
allowRerender
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function FooterAd() {
|
||||||
|
const location = useLocation();
|
||||||
|
const key = `${location.pathname}${location.search}`;
|
||||||
|
|
||||||
|
return !FOOTER_AD ? undefined : (
|
||||||
|
<DangerousContent
|
||||||
|
key={key}
|
||||||
|
className="ad-container"
|
||||||
|
html={atob(FOOTER_AD)}
|
||||||
|
allowRerender
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SliderAd() {
|
||||||
|
const location = useLocation();
|
||||||
|
const key = `${location.pathname}${location.search}`;
|
||||||
|
|
||||||
|
return !SLIDER_AD ? undefined : (
|
||||||
|
<DangerousContent
|
||||||
|
key={key}
|
||||||
|
className="ad-container"
|
||||||
|
html={atob(SLIDER_AD)}
|
||||||
|
allowRerender
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
1
client/src/components/ads/index.ts
Normal file
1
client/src/components/ads/index.ts
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export { MiddleAd, HeaderAd, FooterAd, SliderAd } from "./ads";
|
1
client/src/components/buttons/_index.scss
Normal file
1
client/src/components/buttons/_index.scss
Normal file
|
@ -0,0 +1 @@
|
||||||
|
@use "./buttons";
|
18
client/src/components/buttons/buttons.tsx
Normal file
18
client/src/components/buttons/buttons.tsx
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
import { ReactNode } from "react";
|
||||||
|
import { IBlockProps, createBlockComponent } from "#components/meta";
|
||||||
|
|
||||||
|
interface IProps extends IBlockProps<"button"> {
|
||||||
|
className?: string;
|
||||||
|
isFocusable?: boolean;
|
||||||
|
children?: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Button = createBlockComponent("button", Component);
|
||||||
|
|
||||||
|
export function Component({ isFocusable = true, children, ...props }: IProps) {
|
||||||
|
return (
|
||||||
|
<button {...props} tabIndex={!isFocusable ? undefined : -1}>
|
||||||
|
{children}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
1
client/src/components/buttons/index.ts
Normal file
1
client/src/components/buttons/index.ts
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export { Button } from "./buttons";
|
|
@ -1,6 +1,7 @@
|
||||||
|
@use "card_list";
|
||||||
@use "base";
|
@use "base";
|
||||||
@use "account";
|
@use "account";
|
||||||
@use "post";
|
@use "post";
|
||||||
@use "user";
|
@use "profile";
|
||||||
@use "dm";
|
@use "dm";
|
||||||
@use "no_results";
|
@use "no_results";
|
|
@ -1,4 +1,4 @@
|
||||||
@use "../../../css/sass-mixins" as mixins;
|
@use "../../css/sass-mixins" as mixins;
|
||||||
|
|
||||||
.account-card {
|
.account-card {
|
||||||
@include mixins.article-card();
|
@include mixins.article-card();
|
28
client/src/components/cards/account.tsx
Normal file
28
client/src/components/cards/account.tsx
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
import { Timestamp } from "#components/dates";
|
||||||
|
import { IAccount } from "#entities/account";
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
account: IAccount;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AccountCard({ account }: IProps) {
|
||||||
|
const { id, username, role, created_at } = account;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<article className="account-card" data-id={id}>
|
||||||
|
<header className="account-card__header">
|
||||||
|
<h2 className="account-card__name">{username}</h2>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<section className="account-card__body">
|
||||||
|
<p className="account-card__role">
|
||||||
|
Role: <span>{role}</span>
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<footer className="account-card__footer">
|
||||||
|
<Timestamp time={created_at} />
|
||||||
|
</footer>
|
||||||
|
</article>
|
||||||
|
);
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
@use "../../../css/config/variables" as *;
|
@use "../../css/config/variables" as *;
|
||||||
|
|
||||||
.card {
|
.card {
|
||||||
display: grid;
|
display: grid;
|
41
client/src/components/cards/base.tsx
Normal file
41
client/src/components/cards/base.tsx
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
import clsx from "clsx";
|
||||||
|
import { ReactNode } from "react";
|
||||||
|
|
||||||
|
interface ICardProps {
|
||||||
|
className?: string;
|
||||||
|
children?: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ICardHeaderProps {
|
||||||
|
className?: string;
|
||||||
|
children?: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ICardBodyProps {
|
||||||
|
className?: string;
|
||||||
|
children?: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ICardFooterProps {
|
||||||
|
className?: string;
|
||||||
|
children?: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Card({ className, children }: ICardProps) {
|
||||||
|
return <article className={clsx("card", className)}>{children}</article>;
|
||||||
|
}
|
||||||
|
export function CardHeader({ className, children }: ICardHeaderProps) {
|
||||||
|
return (
|
||||||
|
<header className={clsx("card__header", className)}>{children}</header>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
export function CardBody({ className, children }: ICardBodyProps) {
|
||||||
|
return (
|
||||||
|
<section className={clsx("card__body", className)}>{children}</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
export function CardFooter({ className, children }: ICardFooterProps) {
|
||||||
|
return (
|
||||||
|
<footer className={clsx("card__footer", className)}>{children}</footer>
|
||||||
|
);
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user