let logo = ` ___ ___
(o o) (o o)
_____ ( V ) ( V ) _____
( ___ )------------------------ /--m-m- /--m-m-------------------------( ___ )
| | | |
| | | |
| | ░█▀▄░█▀▀░░░░░█▄█░█▀█░█▀▀░█░█░░░░░█▀▀░█▀▀░█▀▄░█░█░█▀▀░█▀▄ | |
| | ░█▀▄░▀▀█░▄▄▄░█░█░█░█░█░░░█▀▄░▄▄▄░▀▀█░█▀▀░█▀▄░▀▄▀░█▀▀░█▀▄ | |
| | ░▀░▀░▀▀▀░░░░░▀░▀░▀▀▀░▀▀▀░▀░▀░░░░░▀▀▀░▀▀▀░▀░▀░░▀░░▀▀▀░▀░▀ | |
| | | |
|___| |___|
(_____)----------------------------------------------------------------(_____)`;
window.addEventListener("DOMContentLoaded", () => {
document.getElementById("logo").appendChild(document.createTextNode(logo));
const navElement = document.getElementById("routes");
createRouteNavBar(navElement, mock_routes);
});
const REGEX_PARAM = /^{(.+)}$/;
function isArg(key) {
return REGEX_PARAM.test(key);
}
function createRouteTree(routes) {
const root = {};
routes.forEach((route) => {
const parts = route.route.split("/").filter(Boolean);
let current = root;
let params = [];
parts.forEach((part, index) => {
if (!current[part]) {
current[part] = {};
}
current = current[part];
if (isArg(part) && index < parts.length - 1) {
params.push(part);
}
});
current.routeConfigs = current.routeConfigs || { methods: [] };
current.routeConfigs.methods.push({
method: route.method,
options: route.options || [],
params,
});
});
return root;
}
function createRouteNavBar(navElement, routes) {
const routeTree = createRouteTree(routes);
buildNavList(navElement, routeTree, "");
}
function isEndArgKey(leaf, key) {
return (
isArg(key) && leaf && Object.keys(leaf).length == 1 && leaf.routeConfigs
);
}
function buildNavList(navList, leaf, path, param, ulParent) {
const ul = ulParent ?? document.createElement("ul");
Object.keys(leaf)
.sort((a, b) => {
if (isArg(a)) return 1;
if (isArg(b)) return -1;
return 0;
})
.forEach((key) => {
if (key === "routeConfigs") {
leaf.routeConfigs.methods.forEach((methodInfo) => {
const item = document.createElement("li", {
is: "route-item",
});
item.route = methodInfo.method;
item.path = path;
item.method = methodInfo.method;
item.param = param;
item.params = methodInfo.params;
item.options = methodInfo.options;
ul.appendChild(item);
});
return;
}
const current = leaf[key];
let el = navList;
let newPath = path;
if (isEndArgKey(current, key)) {
buildNavList(navList, current, path, key, ul);
return;
} else {
newPath = `${path}/${key}`;
el = document.createElement("li", { is: "route-item" });
el.route = key;
el.path = newPath;
ul.appendChild(el);
}
if (Object.keys(current).length > 0) {
let param = isArg(key) ? key : undefined;
buildNavList(el, current, newPath, param);
}
});
navList.appendChild(ul);
}
class RouteItem extends HTMLLIElement {
constructor() {
super();
this._route = "";
this._path = "";
this._method = "";
this._param = "";
this._params = [];
this._options = [];
}
connectedCallback() {
this.render();
}
set route(value) {
this._route = value;
this.render();
}
get route() {
return this._route;
}
set path(value) {
this._path = value;
this.render();
}
get path() {
return this._path;
}
set method(value) {
this._method = value;
this.render();
}
get method() {
return this._method;
}
set param(value) {
this._param = value;
this.render();
}
get param() {
return this._param;
}
set params(value) {
this._params = value;
this.render();
}
get params() {
return this._params;
}
set options(value) {
this._options = value || [];
this.render();
}
get options() {
return this._options;
}
onLinkClick(event) {
event.preventDefault();
if (this.method) {
const contentDiv = document.getElementById("content");
contentDiv.innerHTML = "";
const apiRequestSender =
document.createElement("api-request-sender");
apiRequestSender.setAttribute("method", this.method);
apiRequestSender.setAttribute("route", this.path);
if (this.param) {
apiRequestSender.setAttribute("param", this.param);
}
if (this.params && this.params.length > 0) {
apiRequestSender.setAttribute("params", this.params.join(","));
}
if (this.options && this.options.length > 0) {
apiRequestSender.setAttribute(
"options",
this.options.join(",")
);
}
contentDiv.appendChild(apiRequestSender);
} else {
this.classList.toggle("expanded");
}
}
render() {
if (!this.isConnected || !this.route || !this.path) {
return;
}
if (!this.method) {
this.classList.add("collapsible");
}
let link = this.querySelector(":scope > a");
if (!link) {
link = document.createElement("a");
link.addEventListener("click", this.onLinkClick.bind(this));
this.prepend(link);
}
link.href = this.param
? `#${this.path}/${this.param}`
: `#${this.path}`;
link.textContent = this.param
? `${this.route} ${this.param}`
: this.route;
}
}
customElements.define("route-item", RouteItem, { extends: "li" });
class ApiRequestSender extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: "open" });
const template = document.getElementById("api-request-sender-template");
this.shadowRoot.appendChild(template.content.cloneNode(true));
}
static get observedAttributes() {
return ["method", "route", "param", "options"];
}
attributeChangedCallback() {
this._render();
}
connectedCallback() {
this._render();
}
_render() {
const method = this.getAttribute("method")?.toUpperCase() || "GET";
const route = this.getAttribute("route") || "/";
const param = this.getAttribute("param");
const params = (this.getAttribute("params") || "")
.split(",")
.map((p) => p.trim())
.filter((p) => p);
const optionsAttr = this.getAttribute("options");
const options = optionsAttr
? optionsAttr.split(",").map((opt) => opt.trim())
: [];
const isDownload = options.includes("download");
const isUpload = options.includes("upload");
const firstRow = this.shadowRoot.getElementById("first-row");
firstRow.innerHTML = `
<span class="method method-${method}">${method}</span>
<span class="route">${route}</span>
${
param
? `<input type="text" id="param-input" placeholder="${param}" />`
: ""
}
${params.map((param, index) => {
return `<input type="text" id="param-input-${index}" placeholder="${param}" />`;
})}
<button id="send-btn">Send</button>
`;
const conditionalContent = this.shadowRoot.getElementById(
"conditional-content"
);
let conditionalHTML = "";
if (isUpload) {
conditionalHTML += `
<div class="file-upload-wrapper">
<input type="file" id="file-input" />
</div>
`;
}
switch (method) {
case "GET":
if (!isDownload) {
conditionalHTML += `
<table id="query-params-table">
<thead><tr><th>Key</th><th>Value</th></tr></thead>
<tbody>
<tr>
<td><input type="text" placeholder="key"></td>
<td><input type="text" placeholder="value"></td>
</tr>
</tbody>
</table>
<button id="add-query-btn" class="add-btn">+</button>
`;
}
break;
case "POST":
case "PUT":
case "PATCH":
conditionalHTML += `<textarea id="body-input" placeholder="Enter JSON body..."></textarea>`;
break;
}
conditionalContent.innerHTML = conditionalHTML;
const resultsContainer =
this.shadowRoot.getElementById("results-container");
resultsContainer.style.display = isDownload ? "none" : "block";
this._addEventListeners();
}
_addEventListeners() {
const sendBtn = this.shadowRoot.getElementById("send-btn");
sendBtn.onclick = () => this._handleSend();
const addQueryBtn = this.shadowRoot.getElementById("add-query-btn");
if (addQueryBtn) {
addQueryBtn.onclick = () => this._addQueryParamRow();
}
}
_addQueryParamRow() {
const tableBody = this.shadowRoot.querySelector(
"#query-params-table tbody"
);
const newRow = document.createElement("tr");
newRow.innerHTML = `
<td><input type="text" placeholder="key"></td>
<td><input type="text" placeholder="value"></td>
`;
tableBody.appendChild(newRow);
}
async _handleSend() {
const method = this.getAttribute("method")?.toUpperCase() || "GET";
let route = this.getAttribute("route") || "/";
const param = this.getAttribute("param");
const params = (this.getAttribute("params") || "")
.split(",")
.map((p) => p.trim())
.filter((p) => p);
const optionsAttr = this.getAttribute("options");
const options = optionsAttr
? optionsAttr.split(",").map((opt) => opt.trim())
: [];
const isDownload = options.includes("download");
const resultsDiv = this.shadowRoot.getElementById("results");
resultsDiv.textContent = "Loading...";
if (param) {
const paramInput = this.shadowRoot.getElementById("param-input");
const paramInputValue = paramInput.value || "";
route = `${route.replace("/" + param, "")}/${paramInputValue}`;
}
params.forEach((param, index) => {
const paramInput = this.shadowRoot.getElementById(
`param-input-${index}`
);
route = route.replace(param, paramInput.value || "unknown");
});
if (method === "GET") {
const table = this.shadowRoot.getElementById("query-params-table");
if (table) {
const params = new URLSearchParams();
const rows = table.querySelectorAll("tbody tr");
rows.forEach((row) => {
const key = row.cells[0].querySelector("input").value;
const value = row.cells[1].querySelector("input").value;
if (key) params.append(key, value);
});
const queryString = params.toString();
if (queryString) route += `?${queryString}`;
}
}
const fetchOptions = { method };
if (["POST", "PUT", "PATCH"].includes(method)) {
const fileInput = this.shadowRoot.getElementById("file-input");
if (fileInput && fileInput.files.length > 0) {
const formData = new FormData();
const bodyContent =
this.shadowRoot.getElementById("body-input").value;
formData.append("file", fileInput.files[0]);
fetchOptions.body = formData;
} else {
fetchOptions.headers = { "Content-Type": "application/json" };
fetchOptions.body =
this.shadowRoot.getElementById("body-input")?.value || "{}";
}
}
try {
const mockUrl = `${route}`;
const response = await fetch(mockUrl, {
...fetchOptions,
});
if (isDownload) {
let filename = "";
const filenameInput =
this.shadowRoot.getElementById("param-input");
if (filenameInput) {
filenameInput.value || "download.json";
} else if (params.length > 0) {
filename =
params
.map((_, index) => {
return this.shadowRoot.getElementById(
`param-input-${index}`
).value;
})
.join("_") + ".json";
} else {
filename = "download.json";
}
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const a = document.createElement("a");
a.style.display = "none";
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);
resultsDiv.textContent = `Download initiated for ${filename}.`;
return;
}
const contentType = response.headers.get("content-type");
if (contentType && contentType.startsWith("image/")) {
const blob = await response.blob();
const imageUrl = URL.createObjectURL(blob);
resultsDiv.innerHTML = `<img src="${imageUrl}" alt="Response image" style="max-width: 100%; height: auto;" />`;
return;
}
if (contentType && contentType.includes("application/json")) {
const data = await response.json();
resultsDiv.textContent = JSON.stringify(data, null, 2);
} else {
const text = await response.text();
resultsDiv.textContent = text;
}
} catch (error) {
resultsDiv.textContent = `Error: ${error.message}`;
}
}
}
customElements.define("api-request-sender", ApiRequestSender);