Compare commits
No commits in common. "develop" and "production" have entirely different histories.
develop
...
production
|
@ -2,7 +2,6 @@ test/
|
|||
storage/
|
||||
dist/
|
||||
client/dev/
|
||||
client/dist/
|
||||
client/node_modules/
|
||||
__pycache__
|
||||
venv
|
||||
|
|
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -15,7 +15,7 @@ dev_*
|
|||
client/dev
|
||||
|
||||
# Dev file server
|
||||
/storage/
|
||||
storage/
|
||||
|
||||
# Javascript packages
|
||||
node_modules
|
||||
|
|
29
client/.dockerignore
Normal file
29
client/.dockerignore
Normal file
|
@ -0,0 +1,29 @@
|
|||
# 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
Normal file
3
client/.vscode/extensions.json
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"recommendations": []
|
||||
}
|
12
client/.vscode/settings.json
vendored
12
client/.vscode/settings.json
vendored
|
@ -1,9 +1,14 @@
|
|||
{
|
||||
"typescript.tsdk": "./node_modules/typescript/lib",
|
||||
"typescript.enablePromptUseWorkspaceTsdk": true,
|
||||
"files.exclude": {
|
||||
"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": {
|
||||
"**/node_modules": true,
|
||||
"**/bower_components": true,
|
||||
|
@ -14,6 +19,9 @@
|
|||
"javascript.preferences.importModuleSpecifierEnding": "js",
|
||||
"javascript.preferences.quoteStyle": "double",
|
||||
"javascript.format.semicolons": "insert",
|
||||
"[jinja-html]": {
|
||||
"editor.tabSize": 2
|
||||
},
|
||||
"[javascript]": {
|
||||
"editor.tabSize": 2
|
||||
},
|
||||
|
|
16
client/Dockerfile
Normal file
16
client/Dockerfile
Normal file
|
@ -0,0 +1,16 @@
|
|||
# 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" ]
|
15
client/Dockerfile.dev
Normal file
15
client/Dockerfile.dev
Normal file
|
@ -0,0 +1,15 @@
|
|||
# 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"]
|
92
client/configs/build-templates.js
Normal file
92
client/configs/build-templates.js
Normal file
|
@ -0,0 +1,92 @@
|
|||
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,
|
||||
};
|
5
client/configs/emmet/snippets.json
Normal file
5
client/configs/emmet/snippets.json
Normal file
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"html": {
|
||||
"snippets": {}
|
||||
}
|
||||
}
|
|
@ -1,167 +1,21 @@
|
|||
// @ts-check
|
||||
const path = require("path");
|
||||
const fs = require("fs");
|
||||
|
||||
/**
|
||||
* @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]
|
||||
*/
|
||||
require("dotenv").config({
|
||||
path: path.resolve(__dirname, "..", ".."),
|
||||
});
|
||||
|
||||
/**
|
||||
* @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 kemonoSite = process.env.KEMONO_SITE || "http://localhost:5000";
|
||||
const nodeEnv = process.env.NODE_ENV || "production";
|
||||
const iconsPrepend = process.env.ICONS_PREPEND || "";
|
||||
const bannersPrepend = process.env.BANNERS_PREPEND || "";
|
||||
const thumbnailsPrepend = process.env.THUMBNAILS_PREPEND || "";
|
||||
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 = {
|
||||
kemonoSite,
|
||||
sentryDSN,
|
||||
siteName,
|
||||
nodeEnv,
|
||||
iconsPrepend,
|
||||
bannersPrepend,
|
||||
thumbnailsPrepend,
|
||||
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
10
client/extra.d.ts
vendored
|
@ -1,10 +0,0 @@
|
|||
// 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
|
||||
}
|
9
client/jsconfig.json
Normal file
9
client/jsconfig.json
Normal file
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"module": "commonJS",
|
||||
"target": "es2015",
|
||||
"moduleResolution": "node"
|
||||
},
|
||||
"exclude": ["node_modules", "dist", "dev", "src"]
|
||||
}
|
2964
client/package-lock.json
generated
2964
client/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
|
@ -1,64 +1,37 @@
|
|||
{
|
||||
"name": "kemono-2-client",
|
||||
"version": "1.0.0",
|
||||
"version": "0.2.1",
|
||||
"description": "frontend for kemono 2",
|
||||
"private": true,
|
||||
"author": "BassOfBass",
|
||||
"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"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "BassOfBass",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.22.10",
|
||||
"@uppy/core": "^3.4.0",
|
||||
"@uppy/dashboard": "^3.5.1",
|
||||
"@uppy/form": "^3.0.2",
|
||||
"@uppy/tus": "^3.1.3",
|
||||
"clsx": "^2.1.0",
|
||||
"diff": "^5.1.0",
|
||||
"fluid-player": "^3.22.0",
|
||||
"micromodal": "^0.4.10",
|
||||
"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",
|
||||
"swagger-ui-react": "^5.17.14",
|
||||
"whatwg-fetch": "^3.6.17"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.22.10",
|
||||
"@babel/plugin-transform-runtime": "^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",
|
||||
"buffer": "^6.0.3",
|
||||
"copy-webpack-plugin": "^8.1.1",
|
||||
"css-loader": "^5.2.7",
|
||||
"dotenv": "^8.6.0",
|
||||
"fs-extra": "^10.1.0",
|
||||
"html-webpack-plugin": "^5.5.3",
|
||||
"mini-css-extract-plugin": "^1.6.2",
|
||||
|
@ -70,14 +43,10 @@
|
|||
"sass-loader": "^11.1.1",
|
||||
"stream-browserify": "^3.0.0",
|
||||
"style-loader": "^2.0.0",
|
||||
"ts-loader": "^9.5.1",
|
||||
"typescript": "^5.3.3",
|
||||
"webpack": "^5.88.2",
|
||||
"webpack-bundle-analyzer": "^4.10.2",
|
||||
"webpack-cli": "^5.1.4",
|
||||
"webpack-dev-server": "^4.15.1",
|
||||
"webpack-manifest-plugin": "^5.0.0",
|
||||
"webpack-merge": "^5.9.0",
|
||||
"yaml": "^2.4.5"
|
||||
"webpack-merge": "^5.9.0"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,29 +0,0 @@
|
|||
// @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.")
|
||||
}
|
||||
}
|
2
client/src/api/_index.js
Normal file
2
client/src/api/_index.js
Normal file
|
@ -0,0 +1,2 @@
|
|||
export { kemonoAPI } from "./kemono/_index";
|
||||
export { paysitesAPI } from "./paysites/_index";
|
|
@ -1,19 +0,0 @@
|
|||
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;
|
||||
}
|
|
@ -1,40 +0,0 @@
|
|||
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;
|
||||
}
|
|
@ -1,26 +0,0 @@
|
|||
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;
|
||||
}
|
|
@ -1,2 +0,0 @@
|
|||
export { fetchAccounts } from "./accounts";
|
||||
export { fetchChangeRolesOfAccounts } from "./change-roles";
|
|
@ -1,19 +0,0 @@
|
|||
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;
|
||||
}
|
|
@ -1,2 +0,0 @@
|
|||
export { fetchAccountAutoImportKeys } from "./get";
|
||||
export { fetchRevokeAutoImportKeys } from "./revoke";
|
|
@ -1,15 +0,0 @@
|
|||
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;
|
||||
}
|
|
@ -1,24 +0,0 @@
|
|||
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;
|
||||
}
|
|
@ -1,22 +0,0 @@
|
|||
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;
|
||||
}
|
|
@ -1,2 +0,0 @@
|
|||
export { fetchDMsForReview } from "./get";
|
||||
export { fetchApproveDMs } from "./review"
|
|
@ -1,21 +0,0 @@
|
|||
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;
|
||||
}
|
|
@ -1,25 +0,0 @@
|
|||
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;
|
||||
}
|
|
@ -1,17 +0,0 @@
|
|||
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;
|
||||
}
|
|
@ -1,15 +0,0 @@
|
|||
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;
|
||||
}
|
|
@ -1,15 +0,0 @@
|
|||
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;
|
||||
}
|
|
@ -1,4 +0,0 @@
|
|||
export { fetchFavouriteProfiles } from "./get-favourite-artists";
|
||||
export { fetchFavouritePosts } from "./get-favourite-posts";
|
||||
export { apiFavoritePost, apiUnfavoritePost } from "./favorite-post";
|
||||
export { apiFavoriteProfile, apiUnfavoriteProfile } from "./favorite-profile";
|
|
@ -1,4 +0,0 @@
|
|||
export { fetchAccount } from "./account";
|
||||
export { fetchAccountNotifications } from "./notifications";
|
||||
export { fetchAddProfileLink } from "./profiles";
|
||||
export { fetchAccountChangePassword } from "./change-password";
|
|
@ -1,5 +0,0 @@
|
|||
export {
|
||||
fetchProfileLinkRequests,
|
||||
fetchApproveLinkRequest,
|
||||
fetchRejectLinkRequest,
|
||||
} from "./profile-link-requests";
|
|
@ -1,30 +0,0 @@
|
|||
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;
|
||||
}
|
|
@ -1,17 +0,0 @@
|
|||
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;
|
||||
}
|
|
@ -1,42 +0,0 @@
|
|||
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;
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
export { fetchRegisterAccount } from "./register";
|
||||
export { fetchLoginAccount } from "./login";
|
||||
export { fetchLogoutAccount } from "./logout";
|
|
@ -1,29 +0,0 @@
|
|||
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;
|
||||
}
|
||||
}
|
|
@ -1,9 +0,0 @@
|
|||
import { apiFetch } from "../fetch";
|
||||
|
||||
export async function fetchLogoutAccount() {
|
||||
const path = `/authentication/logout`;
|
||||
|
||||
const result = await apiFetch<true>(path, { method: "POST"});
|
||||
|
||||
return result;
|
||||
}
|
|
@ -1,20 +0,0 @@
|
|||
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;
|
||||
}
|
|
@ -1,29 +0,0 @@
|
|||
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;
|
||||
}
|
|
@ -1,8 +0,0 @@
|
|||
import { apiFetch } from "../fetch";
|
||||
|
||||
export async function fetchHasPendingDMs() {
|
||||
const path = `/has_pending_dms`;
|
||||
const result = await apiFetch<boolean>(path, { method: "GET" });
|
||||
|
||||
return result;
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
export { fetchDMs } from "./all";
|
||||
export { fetchProfileDMs } from "./profile";
|
||||
export { fetchHasPendingDMs } from "./has-pending";
|
|
@ -1,27 +0,0 @@
|
|||
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;
|
||||
}
|
|
@ -1,144 +0,0 @@
|
|||
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;
|
||||
}
|
|
@ -1,30 +0,0 @@
|
|||
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;
|
||||
}
|
|
@ -1,2 +0,0 @@
|
|||
export { fetchArchiveFile, fetchSetArchiveFilePassword } from "./archive-file";
|
||||
export { fetchSearchFileByHash } from "./search-by-hash";
|
|
@ -1,56 +0,0 @@
|
|||
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;
|
||||
}
|
|
@ -1,26 +0,0 @@
|
|||
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;
|
||||
}
|
|
@ -1,9 +0,0 @@
|
|||
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;
|
||||
}
|
|
@ -1,2 +0,0 @@
|
|||
export { fetchImportLogs } from "./get-import";
|
||||
export { fetchCreateImport } from "./create-import";
|
14
client/src/api/kemono/_index.js
Normal file
14
client/src/api/kemono/_index.js
Normal file
|
@ -0,0 +1,14 @@
|
|||
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,
|
||||
};
|
100
client/src/api/kemono/api.js
Normal file
100
client/src/api/kemono/api.js
Normal file
|
@ -0,0 +1,100 @@
|
|||
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);
|
||||
}
|
||||
}
|
22
client/src/api/kemono/dms.js
Normal file
22
client/src/api/kemono/dms.js
Normal file
|
@ -0,0 +1,22 @@
|
|||
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);
|
||||
}
|
||||
}
|
142
client/src/api/kemono/favorites.js
Normal file
142
client/src/api/kemono/favorites.js
Normal file
|
@ -0,0 +1,142 @@
|
|||
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);
|
||||
}
|
||||
}
|
46
client/src/api/kemono/kemono-fetch.js
Normal file
46
client/src/api/kemono/kemono-fetch.js
Normal file
|
@ -0,0 +1,46 @@
|
|||
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();
|
||||
}
|
26
client/src/api/kemono/posts.js
Normal file
26
client/src/api/kemono/posts.js
Normal file
|
@ -0,0 +1,26 @@
|
|||
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
client/src/api/paysites/_index.js
Normal file
1
client/src/api/paysites/_index.js
Normal file
|
@ -0,0 +1 @@
|
|||
export const paysitesAPI = {};
|
|
@ -1,9 +0,0 @@
|
|||
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;
|
||||
}
|
|
@ -1,25 +0,0 @@
|
|||
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;
|
||||
}
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
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";
|
|
@ -1,53 +0,0 @@
|
|||
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;
|
||||
}
|
|
@ -1,57 +0,0 @@
|
|||
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;
|
||||
}
|
|
@ -1,35 +0,0 @@
|
|||
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;
|
||||
}
|
|
@ -1,15 +0,0 @@
|
|||
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;
|
||||
}
|
|
@ -1,39 +0,0 @@
|
|||
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;
|
||||
}
|
|
@ -1,29 +0,0 @@
|
|||
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;
|
||||
}
|
|
@ -1,9 +0,0 @@
|
|||
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;
|
||||
}
|
|
@ -1,6 +0,0 @@
|
|||
export { fetchProfiles } from "./profiles";
|
||||
export { fetchRandomArtist } from "./random";
|
||||
export { fetchArtistProfile } from "./profile";
|
||||
export { fetchFanboxProfileFancards } from "./fancards";
|
||||
export { fetchProfileLinks } from "./links";
|
||||
export { fetchProfilePosts } from "./posts";
|
|
@ -1,21 +0,0 @@
|
|||
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;
|
||||
}
|
|
@ -1,58 +0,0 @@
|
|||
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;
|
||||
}
|
|
@ -1,14 +0,0 @@
|
|||
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;
|
||||
}
|
|
@ -1,12 +0,0 @@
|
|||
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;
|
||||
}
|
|
@ -1,14 +0,0 @@
|
|||
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;
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
export { fetchShares } from "./shares";
|
||||
export { fetchShare } from "./share";
|
||||
export { fetchProfileShares } from "./profile";
|
|
@ -1,37 +0,0 @@
|
|||
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;
|
||||
}
|
|
@ -1,15 +0,0 @@
|
|||
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;
|
||||
}
|
|
@ -1,24 +0,0 @@
|
|||
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;
|
||||
}
|
|
@ -1,14 +0,0 @@
|
|||
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
|
||||
}
|
|
@ -1,2 +0,0 @@
|
|||
export { fetchTags } from "./all";
|
||||
export { fetchProfileTags } from "./profile";
|
|
@ -1,29 +0,0 @@
|
|||
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;
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
export { ClientProvider, useClient } from "./use-client";
|
||||
export { useRoutePathPattern } from "./use-route-path-pattern";
|
||||
export { useInterval } from "./use-interval";
|
|
@ -1,38 +0,0 @@
|
|||
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;
|
||||
}
|
|
@ -1,27 +0,0 @@
|
|||
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]);
|
||||
}
|
|
@ -1,11 +0,0 @@
|
|||
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;
|
||||
}
|
|
@ -1,46 +0,0 @@
|
|||
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;
|
||||
}
|
||||
}
|
|
@ -1,11 +0,0 @@
|
|||
@use "layout";
|
||||
@use "pages";
|
||||
@use "images";
|
||||
@use "links";
|
||||
@use "dates";
|
||||
@use "cards";
|
||||
@use "loading";
|
||||
@use "buttons";
|
||||
@use "tooltip";
|
||||
@use "pagination";
|
||||
@use "importer_states";
|
|
@ -1,59 +0,0 @@
|
|||
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 +0,0 @@
|
|||
export { MiddleAd, HeaderAd, FooterAd, SliderAd } from "./ads";
|
|
@ -1 +0,0 @@
|
|||
@use "./buttons";
|
|
@ -1,18 +0,0 @@
|
|||
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 +0,0 @@
|
|||
export { Button } from "./buttons";
|
|
@ -1,28 +0,0 @@
|
|||
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,41 +0,0 @@
|
|||
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>
|
||||
);
|
||||
}
|
|
@ -1,122 +0,0 @@
|
|||
import clsx from "clsx";
|
||||
import { ReactNode, useEffect, useRef } from "react";
|
||||
|
||||
interface IProps {
|
||||
layout?: "legacy" | "phone";
|
||||
className?: string;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
const defaultThumbSize = 180;
|
||||
|
||||
export function CardList({ layout = "legacy", className, children }: IProps) {
|
||||
const cardListRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (layout === "phone") {
|
||||
return;
|
||||
}
|
||||
|
||||
const ref = cardListRef.current;
|
||||
|
||||
if (!ref) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const cookies = getCookies();
|
||||
const thumbSizeValue = parseInt(cookies?.thumbSize);
|
||||
let thumbSizeSetting = isNaN(thumbSizeValue) ? undefined : thumbSizeValue;
|
||||
|
||||
if (!thumbSizeSetting) {
|
||||
thumbSizeSetting = defaultThumbSize;
|
||||
addCookie("thumbSize", String(defaultThumbSize), 399);
|
||||
}
|
||||
|
||||
const thumbSize =
|
||||
parseInt(String(thumbSizeSetting)) ===
|
||||
parseInt(String(defaultThumbSize))
|
||||
? undefined
|
||||
: thumbSizeSetting;
|
||||
|
||||
function handleResize() {
|
||||
updateThumbsizes(ref!, defaultThumbSize, thumbSize);
|
||||
}
|
||||
|
||||
updateThumbsizes(ref!, defaultThumbSize, thumbSize);
|
||||
window.addEventListener("resize", handleResize);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("resize", handleResize);
|
||||
};
|
||||
} catch (error) {
|
||||
return console.error(error);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className={clsx("card-list", `card-list--${layout}`, className)}>
|
||||
<div className="card-list__layout"></div>
|
||||
<div ref={cardListRef} className="card-list__items">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function getCookies(): Record<string, string> {
|
||||
const cookies = document.cookie.split(";").reduce(
|
||||
(cookies, cookie) => (
|
||||
// @ts-expect-error whatever
|
||||
(cookies[cookie.split("=")[0].trim()] = decodeURIComponent(
|
||||
cookie.split("=")[1]
|
||||
)),
|
||||
cookies
|
||||
),
|
||||
{}
|
||||
);
|
||||
|
||||
return cookies;
|
||||
}
|
||||
|
||||
function setCookie(name: "thumbSize", value: string, daysToExpire: number) {
|
||||
const date = new Date();
|
||||
date.setTime(date.getTime() + daysToExpire * 24 * 60 * 60 * 1000);
|
||||
const expires = "expires=" + date.toUTCString();
|
||||
document.cookie = name + "=" + value + "; " + expires + ";path=/";
|
||||
}
|
||||
|
||||
function addCookie(name: "thumbSize", newValue: string, daysToExpire: number) {
|
||||
const existingCookie = document.cookie
|
||||
.split(";")
|
||||
.find((cookie) => cookie.trim().startsWith(name + "="));
|
||||
|
||||
if (!existingCookie) {
|
||||
setCookie(name, newValue, daysToExpire);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO: move into card component
|
||||
*/
|
||||
function updateThumbsizes(
|
||||
element: HTMLDivElement,
|
||||
defaultSize: number,
|
||||
thumbSizeSetting?: number
|
||||
) {
|
||||
let thumbSize = thumbSizeSetting ? thumbSizeSetting : defaultSize;
|
||||
|
||||
if (!thumbSizeSetting) {
|
||||
let viewportWidth = window.innerWidth;
|
||||
let offset = 24;
|
||||
let viewportWidthExcludingMargin = viewportWidth - offset;
|
||||
let howManyFit = viewportWidthExcludingMargin / thumbSize;
|
||||
|
||||
if (howManyFit < 2.0 && 1.5 < howManyFit) {
|
||||
thumbSize = viewportWidthExcludingMargin / 2;
|
||||
} else if (howManyFit > 12) {
|
||||
thumbSize = defaultSize * 1.5;
|
||||
}
|
||||
}
|
||||
element.style.setProperty("--card-size", `${thumbSize}px`);
|
||||
}
|
|
@ -1,84 +0,0 @@
|
|||
import clsx from "clsx";
|
||||
import { createProfilePageURL } from "#lib/urls";
|
||||
import { IArtist } from "#entities/profiles";
|
||||
import { IApprovedDM } from "#entities/dms";
|
||||
import { paysites } from "#entities/paysites";
|
||||
import { FancyLink } from "../links";
|
||||
|
||||
interface IProps {
|
||||
dm: IApprovedDM;
|
||||
isPrivate?: boolean;
|
||||
isGlobal?: boolean;
|
||||
artist?: IArtist;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function DMCard({
|
||||
dm,
|
||||
isGlobal = false,
|
||||
isPrivate = false,
|
||||
artist,
|
||||
className,
|
||||
}: IProps) {
|
||||
const { service, user } = dm;
|
||||
const paysite = paysites[service];
|
||||
const profilePageURL = String(
|
||||
createProfilePageURL({ service, profileID: user })
|
||||
);
|
||||
const remoteProfilePageURL = paysite.user.profile(artist?.id ?? user);
|
||||
|
||||
return (
|
||||
<article
|
||||
className={clsx("dm-card", className)}
|
||||
data-id={!isPrivate ? undefined : dm.hash}
|
||||
>
|
||||
{!isGlobal ? undefined : (
|
||||
<header className="dm-card-header">
|
||||
<FancyLink url={profilePageURL} className="dms__user-link">
|
||||
<span className="dm-card__user">{artist?.name ?? user}</span>
|
||||
</FancyLink>
|
||||
<FancyLink
|
||||
url={remoteProfilePageURL}
|
||||
className="dms__remote-user-link"
|
||||
isNoop
|
||||
>
|
||||
<span className="dm-card__service">({paysite.title})</span>
|
||||
</FancyLink>
|
||||
</header>
|
||||
)}
|
||||
|
||||
{!isPrivate ? undefined : (
|
||||
<header className="dm-card-header">
|
||||
<FancyLink url={profilePageURL} className="dms__user-link">
|
||||
<span className="dm-card__user">{artist?.name ?? user}</span>
|
||||
</FancyLink>
|
||||
<FancyLink
|
||||
url={remoteProfilePageURL}
|
||||
className="dms__remote-user-link"
|
||||
isNoop
|
||||
>
|
||||
<span className="dm-card__service">({paysite.title})</span>
|
||||
</FancyLink>
|
||||
</header>
|
||||
)}
|
||||
|
||||
<section className="dm-card__body" tabIndex={0}>
|
||||
<div className="dm-card__content">
|
||||
<pre>{dm.content}</pre>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<footer className="dm-card__footer">
|
||||
{dm.published ? (
|
||||
<div className="dm-card__added">
|
||||
Published: {dm.published.slice(0, 7)}
|
||||
</div>
|
||||
) : /* this is to detect if its not DM */ dm.user_id ? (
|
||||
<div className="dm-card__added">Added: {dm.added.slice(0, 7)}</div>
|
||||
) : (
|
||||
<div className="dm-card__added">Added: {dm.added}</div>
|
||||
)}
|
||||
</footer>
|
||||
</article>
|
||||
);
|
||||
}
|
|
@ -1,8 +0,0 @@
|
|||
export { CardList } from "./card_list";
|
||||
export { NoResults } from "./no_results";
|
||||
export { Card, CardHeader, CardBody, CardFooter } from "./base";
|
||||
export { AccountCard } from "./account";
|
||||
export { PostCard, PostFavoriteCard } from "./post";
|
||||
export { ArtistCard } from "./profile";
|
||||
export { DMCard } from "./dm";
|
||||
export { ShareCard } from "./share";
|
|
@ -1,21 +0,0 @@
|
|||
import { Card, CardBody, CardHeader } from "./base";
|
||||
|
||||
interface IProps {
|
||||
title?: string;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
export function NoResults({
|
||||
title = "Nobody here but us chickens!",
|
||||
message = "There are no items found.",
|
||||
}: IProps) {
|
||||
return (
|
||||
<Card className="card--no-results">
|
||||
<CardHeader>
|
||||
<h2>{title}</h2>
|
||||
</CardHeader>
|
||||
|
||||
<CardBody>{message}</CardBody>
|
||||
</Card>
|
||||
);
|
||||
}
|
|
@ -1,181 +0,0 @@
|
|||
import clsx from "clsx";
|
||||
import { THUMBNAILS_PREPEND } from "#env/env-vars";
|
||||
import { createPostURL } from "#lib/urls";
|
||||
import { Timestamp } from "#components/dates";
|
||||
import { KemonoLink } from "#components/links";
|
||||
import { IPost, IPostWithFavorites } from "#entities/posts";
|
||||
|
||||
interface IProps {
|
||||
post: IPost;
|
||||
isFavourite?: boolean;
|
||||
isServiceIconsDisabled?: boolean;
|
||||
}
|
||||
|
||||
const fileExtendsions = [".gif", ".jpeg", ".jpg", ".jpe", ".png", ".webp"];
|
||||
const someServices = ["fansly", "candfans", "boosty", "gumroad"];
|
||||
|
||||
export function PostCard({
|
||||
post,
|
||||
isServiceIconsDisabled,
|
||||
isFavourite = false,
|
||||
}: IProps) {
|
||||
const {
|
||||
service,
|
||||
user: artistID,
|
||||
id,
|
||||
title,
|
||||
content,
|
||||
published,
|
||||
attachments,
|
||||
} = post;
|
||||
const postLink = String(createPostURL(service, artistID, id));
|
||||
const srcNS = findNamespace(post);
|
||||
|
||||
return (
|
||||
<article
|
||||
className={clsx(
|
||||
"post-card",
|
||||
srcNS.src && "post-card--preview",
|
||||
isFavourite && "post-card--fav"
|
||||
)}
|
||||
data-id={id}
|
||||
data-service={service}
|
||||
data-user={artistID}
|
||||
>
|
||||
<KemonoLink url={postLink}>
|
||||
<header className="post-card__header">
|
||||
{title && title !== "DM"
|
||||
? title
|
||||
: !content || content?.length < 50
|
||||
? content
|
||||
: `${content.slice(0, 50)}...`}
|
||||
</header>
|
||||
|
||||
{!srcNS.src ? undefined : (
|
||||
<div className="post-card__image-container">
|
||||
<img
|
||||
className="post-card__image"
|
||||
src={`${THUMBNAILS_PREPEND}/thumbnail/data${srcNS.src}`}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<footer className="post-card__footer">
|
||||
<div>
|
||||
<div>
|
||||
{!published ? undefined : <Timestamp time={published} />}
|
||||
<div>
|
||||
{!attachments.length ? (
|
||||
<>No attachments</>
|
||||
) : (
|
||||
<>
|
||||
{attachments.length}{" "}
|
||||
{attachments.length === 1 ? "attachment" : "attachments"}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{isServiceIconsDisabled ? undefined : (
|
||||
<img src={`/static/small_icons/${service}.png`} />
|
||||
)}
|
||||
</div>
|
||||
</footer>
|
||||
</KemonoLink>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
|
||||
interface IFavProps {
|
||||
post: IPostWithFavorites;
|
||||
isServiceIconsDisabled?: boolean;
|
||||
}
|
||||
|
||||
export function PostFavoriteCard({ post, isServiceIconsDisabled }: IFavProps) {
|
||||
const {
|
||||
service,
|
||||
user: profileID,
|
||||
id,
|
||||
title,
|
||||
content,
|
||||
published,
|
||||
attachments,
|
||||
fav_count,
|
||||
} = post;
|
||||
const postLink = String(createPostURL(service, profileID, id));
|
||||
const srcNS = findNamespace(post);
|
||||
|
||||
return (
|
||||
<article
|
||||
className={clsx("post-card", srcNS.src && "post-card--preview")}
|
||||
data-id={id}
|
||||
data-service={service}
|
||||
data-user={profileID}
|
||||
>
|
||||
<KemonoLink url={postLink}>
|
||||
<header className="post-card__header">
|
||||
{title && title !== "DM"
|
||||
? title
|
||||
: !content || content?.length < 50
|
||||
? content
|
||||
: `${content.slice(0, 50)}...`}
|
||||
</header>
|
||||
|
||||
{srcNS.src && (
|
||||
<div className="post-card__image-container">
|
||||
<img
|
||||
className="post-card__image"
|
||||
src={`${THUMBNAILS_PREPEND}/thumbnail/data${srcNS.src}`}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<footer className="post-card__footer">
|
||||
<div>
|
||||
<div>
|
||||
{published && <Timestamp time={published} />}
|
||||
|
||||
<div>
|
||||
{attachments.length === 0 ? (
|
||||
<>No attachments</>
|
||||
) : (
|
||||
<>
|
||||
{attachments.length}{" "}
|
||||
{attachments.length === 1 ? "attachment" : "attachments"}
|
||||
</>
|
||||
)}
|
||||
|
||||
<br />
|
||||
<>
|
||||
{fav_count} {fav_count > 1 ? "favorites" : "favorite"}
|
||||
</>
|
||||
</div>
|
||||
</div>
|
||||
{!isServiceIconsDisabled && (
|
||||
<img src={`/static/small_icons/${service}.png`} />
|
||||
)}
|
||||
</div>
|
||||
</footer>
|
||||
</KemonoLink>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
|
||||
function findNamespace(post: IPost) {
|
||||
const srcNS: { found: boolean; src?: string } = { found: false };
|
||||
const path = post.file?.path?.toLowerCase();
|
||||
const isValidpath = path && fileExtendsions.find((ext) => path.endsWith(ext));
|
||||
|
||||
if (isValidpath) {
|
||||
srcNS.src = path;
|
||||
}
|
||||
|
||||
if (!isValidpath && someServices.includes(post.service)) {
|
||||
const matchedFile = post.attachments.find((file) =>
|
||||
fileExtendsions.find((ext) => file.path?.endsWith(ext))
|
||||
);
|
||||
|
||||
srcNS.src = matchedFile?.path;
|
||||
}
|
||||
|
||||
return srcNS;
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user