1 Commits

Author SHA1 Message Date
Zoe
7620577fa0 V0.3.3: Even more optimization
Some checks failed
Build and Push Docker Image to GHCR / build-and-push (push) Has been cancelled
In this realease, I have further optimized Passport. The css that
Passport now uses is entirely handrolled and build via postcss (sadly).
Several bugs have also been fixed in this release, as well as a few
performance improvements relating to the admin UI.
2025-10-04 22:43:21 -05:00
17 changed files with 305 additions and 462 deletions

1
.gitignore vendored
View File

@@ -1,5 +1,4 @@
passport
passport-*
.env
passport.db*
public

View File

@@ -5,6 +5,21 @@ RUN apt update && apt install -y upx unzip
RUN curl -fsSL https://bun.com/install | BUN_INSTALL=/usr bash
ARG TARGETARCH
RUN set -eux; \
echo "Building for architecture: ${TARGETARCH}"; \
case "${TARGETARCH}" in \
"amd64") \
arch_suffix='x64' ;; \
"arm64") \
arch_suffix='arm64' ;; \
*) \
echo "Unsupported architecture: ${TARGETARCH}" && exit 1 ;; \
esac; \
curl -sLO "https://github.com/tailwindlabs/tailwindcss/releases/download/v4.1.13/tailwindcss-linux-${arch_suffix}"; \
mv "tailwindcss-linux-${arch_suffix}" /usr/local/bin/tailwindcss; \
chmod +x /usr/local/bin/tailwindcss;
RUN go install github.com/juls0730/zqdgr@latest
@@ -20,6 +35,7 @@ COPY . .
RUN bun install
RUN zqdgr build
RUN upx passport
# ---- Runtime Stage ----
FROM gcr.io/distroless/static-debian12 AS runner

View File

@@ -72,23 +72,23 @@ You can then run the binary.
The weather integration is optional, and will be enabled automatically if you provide an API key. The following only applies if you are using the OpenWeatherMap integration.
| Environment Variable | Description | Required | Default |
| -------------------------- | ------------------------------------------------------------------------- | -------- | -------------- |
| `PASSPORT_WEATHER_API_KEY` | The OpenWeather API key | true | |
| `WEATHER_PROVIDER` | The weather provider to use, currently only `openweathermap` is supported | false | openweathermap |
| `WEATHER_TEMP_UNITS` | The temperature units to use, either `metric` or `imperial` | false | metric |
| `WEATHER_LAT` | The latitude of your location | true | |
| `WEATHER_LON` | The longitude of your location | true | |
| `WEATHER_UPDATE_INTERVAL` | The interval in minutes to update the weather data | false | 15 |
| Environment Variable | Description | Required | Default |
| ------------------------- | ------------------------------------------------------------------------- | -------- | -------------- |
| `WEATHER_PROVIDER` | The weather provider to use, currently only `openweathermap` is supported | false | openweathermap |
| `WEATHER_API_KEY` | The OpenWeather API key | true | |
| `WEATHER_TEMP_UNITS` | The temperature units to use, either `metric` or `imperial` | false | metric |
| `WEATHER_LAT` | The latitude of your location | true | |
| `WEATHER_LON` | The longitude of your location | true | |
| `WEATHER_UPDATE_INTERVAL` | The interval in minutes to update the weather data | false | 15 |
#### Uptime configuration
The uptime integration is optional, and will be enabled automatically if you provide an API key. The following only applies if you are using the UptimeRobot integration.
| Environment Variable | Description | Required | Default |
| ------------------------- | ------------------------------------------------- | -------- | ------- |
| `PASSPORT_UPTIME_API_KEY` | The UptimeRobot API key | true | |
| `UPTIME_UPDATE_INTERVAL` | The interval in seconds to update the uptime data | false | 300 |
| Environment Variable | Description | Required | Default |
| ------------------------ | ------------------------------------------------- | -------- | ------- |
| `UPTIME_API_KEY` | The UptimeRobot API key | true | |
| `UPTIME_UPDATE_INTERVAL` | The interval in seconds to update the uptime data | false | 300 |
### Adding links and categories

View File

@@ -5,7 +5,6 @@
"name": "passport-css-compiler",
"devDependencies": {
"@fullhuman/postcss-purgecss": "^7.0.2",
"baseline-browser-mapping": "^2.9.2",
"cssnano": "^7.1.1",
"postcss": "^8.4.35",
"postcss-cli": "^11.0.0",
@@ -123,7 +122,7 @@
"autoprefixer": ["autoprefixer@10.4.21", "", { "dependencies": { "browserslist": "^4.24.4", "caniuse-lite": "^1.0.30001702", "fraction.js": "^4.3.7", "normalize-range": "^0.1.2", "picocolors": "^1.1.1", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.1.0" }, "bin": { "autoprefixer": "bin/autoprefixer" } }, "sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ=="],
"baseline-browser-mapping": ["baseline-browser-mapping@2.9.2", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-PxSsosKQjI38iXkmb3d0Y32efqyA0uW4s41u4IVBsLlWLhCiYNpH/AfNOVWRqCQBlD8TFJTz6OUWNd4DFJCnmw=="],
"baseline-browser-mapping": ["baseline-browser-mapping@2.8.10", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-uLfgBi+7IBNay8ECBO2mVMGZAc1VgZWEChxm4lv+TobGdG82LnXMjuNGo/BSSZZL4UmkWhxEHP2f5ziLNwGWMA=="],
"binary-extensions": ["binary-extensions@2.3.0", "", {}, "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw=="],
@@ -479,8 +478,6 @@
"anymatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
"browserslist/baseline-browser-mapping": ["baseline-browser-mapping@2.8.10", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-uLfgBi+7IBNay8ECBO2mVMGZAc1VgZWEChxm4lv+TobGdG82LnXMjuNGo/BSSZZL4UmkWhxEHP2f5ziLNwGWMA=="],
"css-blank-pseudo/postcss-selector-parser": ["postcss-selector-parser@7.1.0", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA=="],
"css-has-pseudo/postcss-selector-parser": ["postcss-selector-parser@7.1.0", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA=="],

View File

@@ -9,7 +9,6 @@
},
"devDependencies": {
"@fullhuman/postcss-purgecss": "^7.0.2",
"baseline-browser-mapping": "^2.9.2",
"cssnano": "^7.1.1",
"postcss": "^8.4.35",
"postcss-cli": "^11.0.0",

View File

@@ -80,15 +80,7 @@ socket.addEventListener('message', (event) => {
setTimeout(testPage, 150);
}
});
</script>
<style>
html {
outline-color: yellow;
outline-width: 5px;
outline-style: dashed;
outline-offset: -5px;
}
</style>`
</script>`
var (
insertCategoryStmt *sql.Stmt
@@ -725,6 +717,10 @@ func main() {
engine := handlebars.NewFileSystem(http.FS(templatesDir), ".hbs")
engine.AddFunc("eq", func(a, b any) bool {
return a == b
})
engine.AddFunc("embedFile", func(fileToEmbed string) string {
content, err := fs.ReadFile(embeddedAssets, fileToEmbed)
if err != nil {
@@ -1019,13 +1015,8 @@ func main() {
iconPath, err := UploadFile(file, contentType, c)
if err != nil {
slog.Error("Failed to upload file", "error", err)
status := fiber.StatusInternalServerError
if strings.Contains(err.Error(), "unsupported file type") {
status = fiber.StatusBadRequest
}
return c.Status(status).JSON(fiber.Map{
"message": "Failed to upload file: " + err.Error(),
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"message": "Failed to upload file, please try again!",
})
}
@@ -1116,13 +1107,8 @@ func main() {
iconPath, err := UploadFile(file, contentType, c)
if err != nil {
slog.Error("Failed to upload file", "error", err)
status := fiber.StatusInternalServerError
if strings.Contains(err.Error(), "unsupported file type") {
status = fiber.StatusBadRequest
}
return c.Status(status).JSON(fiber.Map{
"message": "Failed to upload file: " + err.Error(),
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"message": "Failed to upload file, please try again!",
})
}
@@ -1239,13 +1225,8 @@ func main() {
iconPath, err := UploadFile(file, contentType, c)
if err != nil {
slog.Error("Failed to upload file", "error", err)
status := fiber.StatusInternalServerError
if strings.Contains(err.Error(), "unsupported file type") {
status = fiber.StatusBadRequest
}
return c.Status(status).JSON(fiber.Map{
"message": "Failed to upload file: " + err.Error(),
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"message": "Failed to upload file, please try again!",
})
}

View File

@@ -11,13 +11,38 @@ let activeModal = null;
let teleportStorage = document.getElementById("teleport-storage");
let confirmActions = document.getElementById("confirm-actions");
let selectIconButton = document.getElementById("select-icon-button");
let loadingSpinner = document.getElementById("template-loading-spinner");
document.addEventListener("DOMContentLoaded", () => {
modalContainer.classList.remove("hidden");
modalContainer.classList.add("flex");
});
/**
* Submits a form to the given URL
* @param {Event} event - The event that triggered the function
* @param {string} url - The URL to submit the form to
* @param {"category" | "link"} target - The target to close the modal for
* @returns {Promise<void>}
*/
async function submitRequest(event, url, target) {
event.preventDefault();
let data = new FormData(event.target);
let res = await fetch(url, {
method: "POST",
body: data,
});
if (res.status === 201) {
closeModal(target);
document.getElementById(`${target}-form`).reset();
location.reload();
} else {
let json = await res.json();
document.getElementById(`${target}-message`).innerText = json.message;
}
}
/**
* Adds an event listener for the given from to error check after the first submit
* @param {"category" | "link"} form - The form to initialize
@@ -51,7 +76,9 @@ function addErrorListener(form) {
function cloneEditActions(primaryActions) {
let editActions = document
.getElementById("template-edit-actions")
.content.firstElementChild.cloneNode(true);
.cloneNode(true);
editActions.removeAttribute("id");
editActions.classList.remove("hidden");
let i = 0;
for (i = 0; i < primaryActions.length; i++) {
@@ -67,46 +94,6 @@ function cloneEditActions(primaryActions) {
return editActions;
}
function strToBase64(str) {
// TextEncoder: Always UTF8
const uint8Array = new TextEncoder().encode(str);
let binary = "";
for (let i = 0; i < uint8Array.length; ++i) {
binary += String.fromCharCode(uint8Array[i]);
}
return btoa(binary);
}
function base64ToStr(base64Str) {
return atob(base64Str);
}
/**
* Sets the edit actions to be loading
* @param {HTMLElement} confirmButton The confirm button to set to loading
*/
function setConfirmLoading(confirmButton) {
const originalContents = strToBase64(confirmButton.innerHTML);
const loadingContents = `${loadingSpinner.innerHTML}`;
confirmButton.disabled = true;
// disable the cancel button too
confirmButton.nextElementSibling.disabled = true;
confirmButton.dataset.originContents = originalContents;
confirmButton.innerHTML = loadingContents;
}
/**
* Clears the loading state of the confirm button
* @param {HTMLElement} confirmButton The confirm button to clear the loading state of
*/
function clearConfirmLoading(confirmButton) {
confirmButton.disabled = false;
confirmButton.nextElementSibling.disabled = false;
confirmButton.innerHTML = base64ToStr(confirmButton.dataset.originContents);
confirmButton.dataset.originContents = "";
}
addErrorListener("link");
document
.getElementById("link-form")
@@ -114,77 +101,66 @@ document
event.preventDefault();
let data = new FormData(event.target);
const submitButton = event.target.querySelector("button");
let originalContents = submitButton.innerHTML;
submitButton.disabled = true;
submitButton.innerHTML = `${loadingSpinner.innerHTML}<span>Adding link...</span>`;
await fetch(`/api/category/${targetCategoryID}/link`, {
let res = await fetch(`/api/category/${targetCategoryID}/link`, {
method: "POST",
body: data,
})
.then(async (res) => {
let json = await res.json();
});
if (!res.ok) {
throw new Error(json.message);
}
if (res.status === 201) {
let json = await res.json();
let category = document.getElementById(
`${targetCategoryID}_category`
);
let linkGrid = category.nextElementSibling;
let category = document.getElementById(
`${targetCategoryID}_category`
);
let linkGrid = category.nextElementSibling;
let newLinkCard = document
.getElementById("template-link-card")
.content.firstElementChild.cloneNode(true);
let newLinkCard = document
.getElementById("template-link-card")
.cloneNode(true);
let newLinkImgElement = newLinkCard.querySelector(
"div:first-child img"
);
newLinkCard.classList.remove("hidden");
newLinkCard.classList.add("link-card", "admin", "relative");
newLinkImgElement.src = await processFile(data.get("icon"));
newLinkImgElement.alt = data.get("name");
let newLinkImgElement = newLinkCard.querySelector(
"div:first-child img"
);
newLinkCard.querySelector("h3").textContent = data.get("name");
newLinkCard.querySelector("p").textContent =
data.get("description");
newLinkImgElement.src = await processFile(data.get("icon"));
newLinkImgElement.alt = data.get("name");
newLinkCard.setAttribute("id", `${json.link.id}_link`);
newLinkCard.querySelector("h3").textContent = data.get("name");
newLinkCard.querySelector("p").textContent =
data.get("description");
let editActions = cloneEditActions([
{
clickAction: "editLink(this)",
label: "Edit link",
},
{
clickAction: "deleteLink(this)",
label: "Delete link",
},
]);
newLinkCard.setAttribute("id", `${json.link.id}_link`);
editActions.classList.add("absolute", "right-1", "top-1");
let editActions = cloneEditActions([
{
clickAction: "editLink(this)",
label: "Edit link",
},
{
clickAction: "deleteLink(this)",
label: "Delete link",
},
]);
newLinkCard.appendChild(editActions);
editActions.classList.add("absolute", "right-1", "top-1");
// append the card as the second to last element
linkGrid.insertBefore(newLinkCard, linkGrid.lastElementChild);
closeModal("link");
newLinkCard.appendChild(editActions);
// after the close animation plays
setTimeout(() => {
document.getElementById(`link-form`).reset();
document.getElementById(`link-message`).innerText = "";
}, 300);
})
.catch((err) => {
document.getElementById(`link-message`).innerText = err.message;
})
.finally(() => {
submitButton.disabled = false;
submitButton.innerHTML = originalContents;
});
// append the card as the second to last element
linkGrid.insertBefore(newLinkCard, linkGrid.lastElementChild);
closeModal("link");
// after the close animation plays
setTimeout(() => {
document.getElementById(`link-form`).reset();
}, 300);
} else {
let json = await res.json();
document.getElementById(`link-message`).innerText = json.message;
}
});
addErrorListener("category");
@@ -194,94 +170,74 @@ document
event.preventDefault();
let data = new FormData(event.target);
const submitButton = event.target.querySelector("button");
const originalContents = submitButton.innerHTML;
submitButton.disabled = true;
submitButton.innerHTML = `${loadingSpinner.innerHTML}<span>Adding category...</span>`;
await fetch(`/api/category`, {
let res = await fetch(`/api/category`, {
method: "POST",
body: data,
})
.then(async (res) => {
let json = await res.json();
});
if (!res.ok) {
throw new Error(json.message);
}
if (res.status === 201) {
let json = await res.json();
let newCategory = document
.getElementById("template-category")
.content.firstElementChild.cloneNode(true);
let newCategory = document
.getElementById("template-category")
.cloneNode(true);
let linkGrid = newCategory.querySelector("div:nth-child(2)");
let categoryHeader =
newCategory.querySelector(".category-header");
categoryHeader.setAttribute(
"id",
`${json.category.id}_category`
);
categoryHeader.querySelector("h2").textContent =
json.category.name;
let linkGrid = newCategory.querySelector("div:nth-child(2)");
let categoryHeader = newCategory.querySelector(".category-header");
categoryHeader.setAttribute("id", `${json.category.id}_category`);
categoryHeader.querySelector("h2").textContent = json.category.name;
let editActions = cloneEditActions([
{
clickAction: "editCategory(this)",
label: "Edit category",
},
{
clickAction: "deleteCategory(this)",
label: "Delete category",
},
]);
let editActions = cloneEditActions([
{
clickAction: "editCategory(this)",
label: "Edit category",
},
{
clickAction: "deleteCategory(this)",
label: "Delete category",
},
]);
editActions.classList.add("pl-2", "flex-shrink-0");
editActions.classList.add("pl-2");
categoryHeader.appendChild(editActions);
categoryHeader.appendChild(editActions);
let categoryImg =
categoryHeader.querySelector("div:first-child");
let categoryImg = categoryHeader.querySelector("div:first-child");
categoryImg.querySelector("img").src = await processFile(
data.get("icon")
categoryImg.querySelector("img").src = await processFile(
data.get("icon")
);
linkGrid
.querySelector("div")
.setAttribute(
"onclick",
`openModal('link', ${json.category.id})`
);
linkGrid
.querySelector("div")
.setAttribute(
"onclick",
`openModal('link', ${json.category.id})`
);
let addCategoryButton = document.getElementById(
"add-category-button"
);
addCategoryButton.parentElement.insertBefore(
categoryHeader,
addCategoryButton
);
addCategoryButton.parentElement.insertBefore(
linkGrid,
addCategoryButton
);
let addCategoryButton = document.getElementById(
"add-category-button"
);
addCategoryButton.parentElement.insertBefore(
categoryHeader,
addCategoryButton
);
addCategoryButton.parentElement.insertBefore(
linkGrid,
addCategoryButton
);
closeModal("category");
closeModal("category");
// after the close animation plays
setTimeout(() => {
document.getElementById(`category-form`).reset();
document.getElementById(`category-message`).innerText = "";
}, 300);
})
.catch((err) => {
document.getElementById(`category-message`).innerText =
err.message;
})
.finally(() => {
submitButton.disabled = false;
submitButton.innerHTML = originalContents;
});
// after the close animation plays
setTimeout(() => {
document.getElementById(`category-form`).reset();
}, 300);
} else {
let json = await res.json();
document.getElementById(`category-message`).innerText =
json.message;
}
});
// when the background is clicked, close the modal
@@ -374,9 +330,6 @@ function closeModal() {
document
.getElementById(activeModal + "-contents")
.classList.add("hidden");
if (document.getElementById(`${activeModal}-message`) !== null) {
document.getElementById(`${activeModal}-message`).innerText = "";
}
activeModal = null;
}, 300);
@@ -421,37 +374,22 @@ function unteleportElement(element) {
teleportElement(element, teleportStorage);
}
/**
* Confirms the edit
* @param {Event} ev The event that triggered the function
*/
async function confirmEdit(ev) {
function confirmEdit() {
if (currentlyEditing.cleanup !== undefined) {
// this function could be called via deleting something, which doesn't have a cleanup function
currentlyEditing.cleanup();
}
let confirmButton = ev.target.closest("button");
try {
setConfirmLoading(confirmButton);
switch (currentlyEditing.type) {
case "link":
await confirmLinkEdit();
break;
case "category":
await confirmCategoryEdit();
break;
default:
console.error("Unknown currentlyEditing type");
break;
}
} catch (err) {
// TODO: tell the user that something went wrong?
console.error(err);
} finally {
clearConfirmLoading(confirmButton);
switch (currentlyEditing.type) {
case "link":
confirmLinkEdit();
break;
case "category":
confirmCategoryEdit();
break;
default:
console.error("Unknown currentlyEditing type");
break;
}
}
@@ -512,7 +450,7 @@ function editLink(target) {
throw new Error("failed to find link ID or category ID");
}
iconUploadInput.accept = "image/jpeg,image/png,image/webp,image/svg+xml";
iconUploadInput.accept = "image/*";
targetedImageElement = linkImg;
teleportElement(selectIconButton, linkImg.parentElement);
@@ -532,10 +470,6 @@ function editLink(target) {
});
}
/**
* Confirms the edit of the link
* @param {Event} ev The event that triggered the function
*/
async function confirmLinkEdit() {
let linkEl = document.getElementById(`${currentlyEditing.linkID}_link`);
let linkNameInput = linkEl.querySelector("textarea");
@@ -570,19 +504,23 @@ async function confirmLinkEdit() {
return;
}
await fetch(
let res = await fetch(
`/api/category/${currentlyEditing.categoryID}/link/${currentlyEditing.linkID}`,
{
method: "PATCH",
body: formData,
}
).then(() => {
);
if (res.status === 200) {
iconUploadInput.value = "";
currentlyEditing.icon = undefined;
cancelLinkEdit(linkNameInput.value, linkDescInput.value);
currentlyEditing = {};
});
} else {
console.error("Failed to edit category");
}
}
function cancelLinkEdit(
@@ -618,7 +556,7 @@ function cancelLinkEdit(
*/
function deleteLink(target) {
// we do it in this dynamic way so that if we add a new link without refreshing the page, it still works
let linkEl = target.closest("[data-card]");
let linkEl = target.closest(".link-card");
let linkID = parseInt(linkEl.id);
let categoryID = parseInt(linkEl.parentElement.previousElementSibling.id);
@@ -639,47 +577,21 @@ function deleteLink(target) {
openModal("link-delete");
}
/**
* Confirms the deletion of the link
* @param {Event} ev The event that triggered the function
*/
async function confirmDeleteLink(ev) {
const originalContents = ev.target.innerHTML;
const deleteButton = ev.target;
const cancelButton = deleteButton.nextElementSibling;
deleteButton.disabled = true;
cancelButton.disabled = true;
deleteButton.innerHTML = `${loadingSpinner.innerHTML}<span>Deleting link...</span>`;
await fetch(
async function confirmDeleteLink() {
let res = await fetch(
`/api/category/${currentlyEditing.categoryID}/link/${currentlyEditing.linkID}`,
{
method: "DELETE",
}
)
.then(async (res) => {
if (!res.ok) {
let json = await res.json();
throw new Error(json.message);
}
);
let linkEl = document.getElementById(
`${currentlyEditing.linkID}_link`
);
linkEl.remove();
if (res.status === 200) {
let linkEl = document.getElementById(`${currentlyEditing.linkID}_link`);
linkEl.remove();
closeModal();
currentlyEditing = {};
})
.catch((err) => {
document.getElementById(`delete-link-message`).innerText =
err.message;
})
.finally(() => {
deleteButton.disabled = false;
cancelButton.disabled = false;
deleteButton.innerHTML = originalContents;
});
closeModal();
currentlyEditing = {};
}
}
/**
@@ -759,10 +671,12 @@ async function confirmCategoryEdit() {
return;
}
await fetch(`/api/category/${currentlyEditing.categoryID}`, {
let res = await fetch(`/api/category/${currentlyEditing.categoryID}`, {
method: "PATCH",
body: formData,
}).then(() => {
});
if (res.status === 200) {
iconUploadInput.value = "";
currentlyEditing.icon = undefined;
@@ -770,7 +684,9 @@ async function confirmCategoryEdit() {
cancelCategoryEdit(categoryInput.value);
currentlyEditing = {};
});
} else {
console.error("Failed to edit category");
}
}
function cancelCategoryEdit(text = currentlyEditing.originalText) {
@@ -823,42 +739,22 @@ function deleteCategory(target) {
}
async function confirmDeleteCategory() {
const originalContents = ev.target.innerHTML;
const deleteButton = ev.target;
const cancelButton = deleteButton.nextElementSibling;
deleteButton.disabled = true;
cancelButton.disabled = true;
deleteButton.innerHTML = `${loadingSpinner.innerHTML}<span>Deleting category...</span>`;
await fetch(`/api/category/${currentlyEditing.categoryID}`, {
let res = await fetch(`/api/category/${currentlyEditing.categoryID}`, {
method: "DELETE",
})
.then(async (res) => {
if (!res.ok) {
let json = await res.json();
throw new Error(json.message);
}
});
let categoryEl = document.getElementById(
`${currentlyEditing.categoryID}_category`
);
// get the next element and remove it (its the link grid)
let nextEl = categoryEl.nextElementSibling;
nextEl.remove();
categoryEl.remove();
if (res.status === 200) {
let categoryEl = document.getElementById(
`${currentlyEditing.categoryID}_category`
);
// get the next element and remove it (its the link grid)
let nextEl = categoryEl.nextElementSibling;
nextEl.remove();
categoryEl.remove();
closeModal();
currentlyEditing = {};
})
.catch((err) => {
document.getElementById(`delete-category-message`).innerText =
err.message;
})
.finally(() => {
deleteButton.disabled = false;
cancelButton.disabled = false;
deleteButton.innerHTML = originalContents;
});
closeModal();
currentlyEditing = {};
}
}
function roundToNearestHundredth(num) {
@@ -875,7 +771,6 @@ const stylesToCopy = [
"letter-spacing",
"text-transform",
"text-align",
"text-wrap-style",
];
let _textMeasuringSpan,
@@ -893,6 +788,8 @@ let _textMeasuringSpan,
* @returns (() => void) A cleanup function to remove event listeners
*/
function replaceWithResizableTextarea(targetEls) {
let startTime = performance.now();
/**
* @typedef {Object} TargetInfo
* @property {HTMLElement} targetEl The element to replace.
@@ -927,7 +824,7 @@ function replaceWithResizableTextarea(targetEls) {
parseFloat(computedStyle.borderTopWidth) +
parseFloat(computedStyle.borderBottomWidth);
let maxWidth = parentBoundingRect.width;
let maxWidth = parentBoundingRect.width - borderWidth;
// take care of category headers specifically because the parent bounding box contains two other elements
if (targetEl.tagName === "H2") {
let imageComputedStyle = window.getComputedStyle(
@@ -1060,6 +957,9 @@ function replaceWithResizableTextarea(targetEls) {
function resize(inputElement, fill = false) {
const currentInputComputedStyle = window.getComputedStyle(inputElement);
const currentInputBorderWidth =
parseFloat(currentInputComputedStyle.borderLeftWidth) +
parseFloat(currentInputComputedStyle.borderRightWidth);
const currentParentElBoundingRectWidth =
inputElement.parentElement.getBoundingClientRect().width;
@@ -1077,13 +977,14 @@ function replaceWithResizableTextarea(targetEls) {
let imageWidth =
inputElement.previousElementSibling.getBoundingClientRect()
.width +
parseFloat(imageComputedStyle.marginLeft) +
parseFloat(imageComputedStyle.marginRight);
imageComputedStyle.marginLeft +
imageComputedStyle.marginRight;
let actionButtonWidth =
inputElement.nextElementSibling.getBoundingClientRect().width;
// the brain cells rub together and this vaguely makes sense to me I think but I cant explain it
maxWidth -= imageWidth + actionButtonWidth;
maxWidth -= imageWidth + actionButtonWidth + caretBuffer;
maxWidth += currentInputBorderWidth;
}
let currentContentWidth;
@@ -1116,7 +1017,9 @@ function replaceWithResizableTextarea(targetEls) {
_textMeasuringSpan.getBoundingClientRect().width;
currentContentWidth = Math.min(
roundToNearestHundredth(measuredTextWidth) + caretBuffer,
roundToNearestHundredth(
measuredTextWidth + currentInputBorderWidth
) + caretBuffer,
maxWidth
);
} else {

View File

@@ -24,7 +24,6 @@ type UptimeRobotSite struct {
FriendlyName string `json:"friendly_name"`
Url string `json:"url"`
Status int `json:"status"`
Up bool `json:"-"`
}
type UptimeManager struct {
@@ -105,10 +104,6 @@ func (u *UptimeManager) update() {
return
}
for i, monitor := range monitors.Monitors {
monitors.Monitors[i].Up = monitor.Status == 2
}
u.mutex.Lock()
u.sites = monitors.Monitors
u.lastUpdate = time.Now()

View File

@@ -82,9 +82,6 @@
}
& > button {
display: flex;
justify-content: center;
column-gap: calc(var(--spacing) * 2);
background-color: var(--color-accent);
color: #fff;
border-radius: calc(var(--spacing) * 1.5);
@@ -107,9 +104,6 @@
row-gap: calc(var(--spacing) * 2);
& > button {
display: flex;
justify-content: center;
column-gap: calc(var(--spacing) * 2);
padding-inline: calc(var(--spacing) * 4);
padding-block: calc(var(--spacing) * 2);
border-radius: calc(var(--spacing) * 1.5);
@@ -126,14 +120,12 @@
background-color: #0000;
}
&:not([disabled]) {
&:hover {
filter: brightness(125%);
}
&:hover {
filter: brightness(125%);
}
&:active {
filter: brightness(95%);
}
&:active {
filter: brightness(95%);
}
}
}
@@ -200,14 +192,12 @@
transition: filter 0.15s cubic-bezier(0.45, 0, 0.55, 1);
contain: layout style paint;
&:not([disabled]) {
&:hover {
filter: brightness(125%);
}
&:hover {
filter: brightness(125%);
}
&:active {
filter: brightness(95%);
}
&:active {
filter: brightness(95%);
}
}
@@ -224,11 +214,6 @@
align-items: center;
}
button[disabled] {
cursor: not-allowed;
filter: brightness(75%);
}
header {
display: flex;
width: 100%;

View File

@@ -111,6 +111,7 @@
.category-header h2 {
text-transform: capitalize;
word-break: break-all;
border-width: 1px;
border-color: #0000;
}
@@ -145,13 +146,3 @@
}
}
}
@layer utilities {
.flex-shrink-0 {
flex-shrink: 0;
}
.pl-2 {
padding-left: calc(var(--spacing) * 2);
}
}

View File

@@ -5,9 +5,9 @@
<div>
<img width="32" height="32" draggable="false" alt="{{this.Name}}" src="{{this.Icon}}" />
</div>
<h2>{{this.Name}}</h2>
<h2 class="capitalize break-all">{{this.Name}}</h2>
{{#if IsAdmin}}
<div class="flex-shrink-0 pl-2">
<div>
<div class="action-container">
<button aria-label="Edit category" onclick="editCategory(this)" class="action-button">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">

View File

@@ -4,9 +4,8 @@
undone.
All links associated with this category will also be deleted. Are you sure you want to continue?</p>
<div>
<button onclick="confirmDeleteCategory(event)">Delete
<button onclick="confirmDeleteCategory()">Delete
category</button>
<button onclick="closeModal()">Cancel</button>
</div>
<span id="delete-category-message"></span>
</div>

View File

@@ -4,9 +4,8 @@
you sure you
want to continue?</p>
<div>
<button onclick="confirmDeleteLink(event)">Delete
<button onclick="confirmDeleteLink()">Delete
link</button>
<button onclick="closeModal()">Cancel</button>
</div>
<span id="delete-link-message"></span>
</div>

View File

@@ -15,8 +15,7 @@
</div>
<div>
<label for="linkIcon">Icon</label>
<input required type="file" name="icon" id="linkIcon"
accept="image/jpeg,image/png,image/webp,image/svg+xml" />
<input required type="file" name="icon" id="linkIcon" accept="image/*" />
</div>
<button type="submit">Add
link</button>

View File

@@ -40,85 +40,65 @@
</div>
</div>
<template id="template-loading-spinner">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<!-- Icon from SVG Spinners by Utkarsh Verma - https://github.com/n3r4zzurr0/svg-spinners/blob/main/LICENSE
-->
<path fill="#EAEAEA" d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z"
opacity=".25" />
<path fill="#EAEAEA"
d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z">
<animateTransform attributeName="transform" dur="0.75s" repeatCount="indefinite" type="rotate" values="0
12 12;360 12 12" />
</path>
</svg>
</template>
<!-- store a blank link card so that if we add a new link we can clone it to make the editing experience easier -->
<template id="template-link-card">
<div data-card>
<div>
<img width="64" height="64" draggable="false" />
</div>
<div>
<h3></h3>
<!-- add 2 to the height to account for the border -->
<p></p>
</div>
</div>
</template>
<template id="template-category">
<div id="template-link-card" class="hidden">
<div>
<div class="category-header">
<img width="64" height="64" draggable="false" />
</div>
<div>
<h3></h3>
<!-- add 2 to the height to account for the border -->
<p></p>
</div>
</div>
<div id="template-category" class="hidden">
<div class="category-header">
<div>
<img width="32" height="32" draggable="false" />
</div>
<h2></h2>
</div>
<div class="link-grid">
<div class="new-link-card link-card admin">
<svg class="mr-2" xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 24 24">
<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
stroke-width="2" d="M12 5v14m-7-7h14" />
</svg>
<div>
<img width="32" height="32" draggable="false" />
</div>
<h2></h2>
</div>
<div class="link-grid">
<div class="new-link-card link-card admin">
<svg class="mr-2" xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 24 24">
<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
stroke-width="2" d="M12 5v14m-7-7h14" />
</svg>
<div>
<h3>Add a link</h3>
</div>
<h3>Add a link</h3>
</div>
</div>
</div>
</template>
</div>
<template id="template-edit-actions">
<div>
<div class="action-container">
<button class="action-button">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24"
viewBox="0 0 24 24"><!-- Icon from Tabler Icons by Paweł Kuna - https://github.com/tabler/tabler-icons/blob/master/LICENSE -->
<g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
stroke-width="2">
<path d="M7 7H6a2 2 0 0 0-2 2v9a2 2 0 0 0 2 2h9a2 2 0 0 0 2-2v-1" />
<path d="M20.385 6.585a2.1 2.1 0 0 0-2.97-2.97L9 12v3h3zM16 5l3 3" />
</g>
</svg>
</button>
<button class="text-error action-button">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24"
viewBox="0 0 24 24"><!-- Icon from Tabler Icons by Paweł Kuna - https://github.com/tabler/tabler-icons/blob/master/LICENSE -->
<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
stroke-width="2"
d="M4 7h16m-10 4v6m4-6v6M5 7l1 12a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2l1-12M9 7V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v3" />
</svg>
</button>
</div>
<div id="template-edit-actions" class="hidden">
<div class="flex flex-row gap-2">
<button class="action-button">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24"
viewBox="0 0 24 24"><!-- Icon from Tabler Icons by Paweł Kuna - https://github.com/tabler/tabler-icons/blob/master/LICENSE -->
<g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
stroke-width="2">
<path d="M7 7H6a2 2 0 0 0-2 2v9a2 2 0 0 0 2 2h9a2 2 0 0 0 2-2v-1" />
<path d="M20.385 6.585a2.1 2.1 0 0 0-2.97-2.97L9 12v3h3zM16 5l3 3" />
</g>
</svg>
</button>
<button class="text-error action-button">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24"
viewBox="0 0 24 24"><!-- Icon from Tabler Icons by Paweł Kuna - https://github.com/tabler/tabler-icons/blob/master/LICENSE -->
<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
stroke-width="2"
d="M4 7h16m-10 4v6m4-6v6M5 7l1 12a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2l1-12M9 7V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v3" />
</svg>
</button>
</div>
</template>
</div>
<div id="teleport-storage" class="absolute -top-full -left-full hidden">
<!-- These are the elements that appear when the user enters edit mode, they allow for the cancelation/confirmation of the edit -->
<div class="action-container" id="confirm-actions">
<button class="action-button text-success" onclick="confirmEdit(event)">
<button class="action-button text-success" onclick="confirmEdit()">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24"
viewBox="0 0 24 24"><!-- Icon from Tabler Icons by Paweł Kuna - https://github.com/tabler/tabler-icons/blob/master/LICENSE -->
<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"

View File

@@ -39,11 +39,13 @@
</span>
<div class="uptime-status">
<svg viewBox="0 0 10 10">
<use href="#status-dot" class="{{#if this.Up}}text-success{{else}}text-error{{/if}}">
<use href="#status-dot"
class="{{#if (eq this.Status 2)}}text-success{{else}}text-error{{/if}}">
</use>
</svg>
<svg viewBox="0 0 10 10">
<use href="#status-dot" class="{{#if this.Up}}text-success{{else}}text-error{{/if}}">
<use href="#status-dot"
class="{{#if (eq this.Status 2)}}text-success{{else}}text-error{{/if}}">
</use>
</svg>
</div>

View File

@@ -1,6 +1,6 @@
{
"name": "passport",
"version": "0.3.4",
"version": "0.3.3",
"description": "Passport is a simple, lightweight, and fast dashboard/new tab page for your browser.",
"author": "juls0730",
"license": "BSL-1.0",
@@ -11,15 +11,13 @@
},
"scripts": {
"generate": "go generate ./src/",
"dev": "zqdgr generate; PASSPORT_DEV_MODE=true go run src/main.go",
"build": "zqdgr generate && go build -tags netgo,prod -ldflags=\"-w -s\" -o passport src/main.go && upx passport",
"build:all": "zqdgr build && zqdgr build:arm64 && zqdgr build:amd64",
"build:amd64": "zqdgr generate && GOOS=linux GOARCH=amd64 go build -tags netgo,prod -ldflags=\"-w -s\" -o passport-linux-amd64 src/main.go && upx passport-linux-amd64",
"build:arm64": "zqdgr generate && GOOS=linux GOARCH=arm64 go build -tags netgo,prod -ldflags=\"-w -s\" -o passport-linux-arm64 src/main.go && upx passport-linux-arm64"
"build": "zqdgr generate && go build -tags netgo,prod -ldflags=\"-w -s\" -o passport src/main.go",
"build:amd64": "zqdgr generate && GOOS=linux GOARCH=amd64 go build -tags netgo,prod -ldflags=\"-w -s\" -o passport src/main.go",
"build:arm64": "zqdgr generate && GOOS=linux GOARCH=arm64 go build -tags netgo,prod -ldflags=\"-w -s\" -o passport src/main.go"
},
"pattern": "src/**/*.{go,hbs,css,js,svg,png,jpg,jpeg,webp,woff2,ico,webp}",
"excluded_files": [
"src/assets/styles"
],
"excluded_files": ["src/assets/styles"],
"shutdown_signal": "SIGINT"
}