diff --git a/package.json b/package.json index 90737a6..5180adf 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "private": false, "name": "bluelib", - "version": "0.10.4", + "version": "0.11.0", "license": "AGPL-3.0-or-later", "source": "src/index.js", "main": "dist/index.js", @@ -10,7 +10,7 @@ "files": ["dist/*"], "scripts": { "start": "preact watch", - "dist": "microbundle && npm publish" + "dist": "git add . && cross-env-shell git commit -m \"$npm_package_version\" && git push && microbundle && npm publish && cross-env-shell hub release create -m \"$npm_package_version\" \"$npm_package_version\"" }, "devDependencies": { "cross-env": "^7.0.2", diff --git a/src/contexts/RoyalnetInstanceUrl.js b/src/contexts/RoyalnetInstanceUrl.js new file mode 100644 index 0000000..dac780a --- /dev/null +++ b/src/contexts/RoyalnetInstanceUrl.js @@ -0,0 +1,3 @@ +import {createContext} from "preact"; + +export default createContext(undefined); diff --git a/src/hooks/useFormValidator.js b/src/hooks/useFormValidator.js new file mode 100644 index 0000000..dbde452 --- /dev/null +++ b/src/hooks/useFormValidator.js @@ -0,0 +1,14 @@ +import { useEffect, useState } from 'preact/hooks'; + +export default function(value, validator) { + const [status, setStatus] = useState({ + validity: null, + message: "" + }); + + useEffect(() => { + validator(value, setStatus); + }, [value]); + + return status; +} diff --git a/src/hooks/useRoyalnetData.js b/src/hooks/useRoyalnetData.js new file mode 100644 index 0000000..b9ffcdb --- /dev/null +++ b/src/hooks/useRoyalnetData.js @@ -0,0 +1,15 @@ +import RoyalnetInstanceUrl from '../contexts/RoyalnetInstanceUrl'; +import {useContext, useEffect, useState} from 'preact/hooks'; +import {royalnetApiRequest} from '../utils/royalnetApiRequest'; + + +export default function(method, path, body) { + const instanceUrl = useContext(RoyalnetInstanceUrl); + const [data, setData] = useState(undefined); + + useEffect(() => { + royalnetApiRequest(instanceUrl, method, path, body).then(d => setData(d)); + }, [instanceUrl, method, path, body]); + + return data; +} diff --git a/src/hooks/useRoyalnetInstanceValidator.js b/src/hooks/useRoyalnetInstanceValidator.js new file mode 100644 index 0000000..af03822 --- /dev/null +++ b/src/hooks/useRoyalnetInstanceValidator.js @@ -0,0 +1,61 @@ +import { useContext, useState } from 'preact/hooks'; +import RoyalnetInstanceUrl from '../contexts/RoyalnetInstanceUrl'; +import useFormValidator from "./useFormValidator"; +import apiRequest from '../utils/apiRequest'; + +const instanceUrlRegex = /^https?:\/\/.*?[^/]$/; + +export default function() { + const defaultInstanceUrl = useContext(RoyalnetInstanceUrl); + const [instanceUrl, setInstanceUrl] = useState(defaultInstanceUrl); + const [instanceTesterAbort, setInstanceTesterAbort] = useState(null); + + const instanceUrlStatus = useFormValidator(instanceUrl, (value, setStatus) => { + if(value.length === 0) { + setStatus({ + validity: null, + message: "" + }); + return; + } + + if(!Boolean(instanceUrlRegex.test(value))) { + setStatus({ + validity: false, + message: "Invalid URL" + }); + return; + } + + if(instanceTesterAbort !== null) { + instanceTesterAbort.abort(); + } + let abort = new AbortController(); + setInstanceTesterAbort(abort); + + apiRequest(value, "GET", "/api/royalnet/version/v1", undefined, abort.signal).then((data) => { + if(value === instanceUrl) { + setStatus({ + validity: true, + message: `Royalnet ${data["semantic"]}` + }); + } + else { + console.log("wtf?") + } + }).catch((err) => { + if(value === instanceUrl) { + setStatus({ + validity: false, + message: "Royalnet not found" + }); + } + }); + setStatus({ + validity: null, + message: "" + }); + }); + + return [instanceUrl, setInstanceUrl, instanceUrlStatus]; +} diff --git a/src/utils/royalnetApiRequest.js b/src/utils/royalnetApiRequest.js new file mode 100644 index 0000000..2395da4 --- /dev/null +++ b/src/utils/royalnetApiRequest.js @@ -0,0 +1,55 @@ +// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error +class RoyalnetApiError extends Error { + constructor(errorCode, errorType, errorArgs, ...params) { + // noinspection JSCheckFunctionSignatures + super(...params); + if(Error.captureStackTrace) Error.captureStackTrace(this, RoyalnetApiError); + + this.name = "RoyalnetApiError"; + this.errorCode = errorCode; + this.errorType = errorType; + this.errorArgs = errorArgs; + this.message = `${errorCode} | ${errorType} | ${errorArgs.join("|")}`; + } +} + + +async function royalnetApiRequest(baseUrl, method, path, args, abortSignal) { + if(args === undefined || args === null) { + args = {}; + } + + let body; + let url; + if(method === "GET") { + body = undefined; + //Create a query string + let params = new URLSearchParams(); + //Use the items in the args object as key-value pairs for the query string + Object.keys(args).forEach(key => { + let arg = args[key]; + params.append(key, arg); + }); + url = `${baseUrl}${path}?${params.toString()}`; + } + else { + body = JSON.stringify(args); + url = `${baseUrl}${path}`; + } + + //Make the request + let response = await fetch(url, { + method: method, + body: body, + signal: abortSignal, + }); + //Parse the response as JSON + let json = await response.json(); + //Check if the request was a success + if(json["success"] === false) { + throw new RoyalnetApiError(json["error_code"], json["error_type"], json["error_args"]) + } + return json["data"] +} + +export {royalnetApiRequest, RoyalnetApiError};