Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
3ffd439f88
|
|||
|
68284bc963
|
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,4 +1,5 @@
|
||||
passport
|
||||
passport-*
|
||||
.env
|
||||
passport.db*
|
||||
public
|
||||
|
||||
16
Dockerfile
16
Dockerfile
@@ -4,21 +4,6 @@ FROM golang:1.25 AS builder
|
||||
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
|
||||
@@ -35,7 +20,6 @@ COPY . .
|
||||
RUN bun install
|
||||
|
||||
RUN zqdgr build
|
||||
RUN upx passport
|
||||
|
||||
# ---- Runtime Stage ----
|
||||
FROM gcr.io/distroless/static-debian12 AS runner
|
||||
|
||||
24
README.md
24
README.md
@@ -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 |
|
||||
| ------------------------- | ------------------------------------------------------------------------- | -------- | -------------- |
|
||||
| `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 |
|
||||
| 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 |
|
||||
|
||||
#### 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 |
|
||||
| ------------------------ | ------------------------------------------------- | -------- | ------- |
|
||||
| `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 |
|
||||
| ------------------------- | ------------------------------------------------- | -------- | ------- |
|
||||
| `PASSPORT_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
|
||||
|
||||
|
||||
5
bun.lock
5
bun.lock
@@ -5,6 +5,7 @@
|
||||
"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",
|
||||
@@ -122,7 +123,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.8.10", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-uLfgBi+7IBNay8ECBO2mVMGZAc1VgZWEChxm4lv+TobGdG82LnXMjuNGo/BSSZZL4UmkWhxEHP2f5ziLNwGWMA=="],
|
||||
"baseline-browser-mapping": ["baseline-browser-mapping@2.9.2", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-PxSsosKQjI38iXkmb3d0Y32efqyA0uW4s41u4IVBsLlWLhCiYNpH/AfNOVWRqCQBlD8TFJTz6OUWNd4DFJCnmw=="],
|
||||
|
||||
"binary-extensions": ["binary-extensions@2.3.0", "", {}, "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw=="],
|
||||
|
||||
@@ -478,6 +479,8 @@
|
||||
|
||||
"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=="],
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
},
|
||||
"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",
|
||||
|
||||
41
src/main.go
41
src/main.go
@@ -80,7 +80,15 @@ socket.addEventListener('message', (event) => {
|
||||
setTimeout(testPage, 150);
|
||||
}
|
||||
});
|
||||
</script>`
|
||||
</script>
|
||||
<style>
|
||||
html {
|
||||
outline-color: yellow;
|
||||
outline-width: 5px;
|
||||
outline-style: dashed;
|
||||
outline-offset: -5px;
|
||||
}
|
||||
</style>`
|
||||
|
||||
var (
|
||||
insertCategoryStmt *sql.Stmt
|
||||
@@ -717,10 +725,6 @@ 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 {
|
||||
@@ -1015,8 +1019,13 @@ func main() {
|
||||
iconPath, err := UploadFile(file, contentType, c)
|
||||
if err != nil {
|
||||
slog.Error("Failed to upload file", "error", err)
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
||||
"message": "Failed to upload file, please try again!",
|
||||
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(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1107,8 +1116,13 @@ func main() {
|
||||
iconPath, err := UploadFile(file, contentType, c)
|
||||
if err != nil {
|
||||
slog.Error("Failed to upload file", "error", err)
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
||||
"message": "Failed to upload file, please try again!",
|
||||
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(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1225,8 +1239,13 @@ func main() {
|
||||
iconPath, err := UploadFile(file, contentType, c)
|
||||
if err != nil {
|
||||
slog.Error("Failed to upload file", "error", err)
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
||||
"message": "Failed to upload file, please try again!",
|
||||
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(),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -11,38 +11,13 @@ 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
|
||||
@@ -76,9 +51,7 @@ function addErrorListener(form) {
|
||||
function cloneEditActions(primaryActions) {
|
||||
let editActions = document
|
||||
.getElementById("template-edit-actions")
|
||||
.cloneNode(true);
|
||||
editActions.removeAttribute("id");
|
||||
editActions.classList.remove("hidden");
|
||||
.content.firstElementChild.cloneNode(true);
|
||||
|
||||
let i = 0;
|
||||
for (i = 0; i < primaryActions.length; i++) {
|
||||
@@ -94,6 +67,46 @@ 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")
|
||||
@@ -101,66 +114,77 @@ document
|
||||
event.preventDefault();
|
||||
let data = new FormData(event.target);
|
||||
|
||||
let res = await fetch(`/api/category/${targetCategoryID}/link`, {
|
||||
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`, {
|
||||
method: "POST",
|
||||
body: data,
|
||||
});
|
||||
})
|
||||
.then(async (res) => {
|
||||
let json = await res.json();
|
||||
|
||||
if (res.status === 201) {
|
||||
let json = await res.json();
|
||||
if (!res.ok) {
|
||||
throw new Error(json.message);
|
||||
}
|
||||
|
||||
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")
|
||||
.cloneNode(true);
|
||||
let newLinkCard = document
|
||||
.getElementById("template-link-card")
|
||||
.content.firstElementChild.cloneNode(true);
|
||||
|
||||
newLinkCard.classList.remove("hidden");
|
||||
newLinkCard.classList.add("link-card", "admin", "relative");
|
||||
let newLinkImgElement = newLinkCard.querySelector(
|
||||
"div:first-child img"
|
||||
);
|
||||
|
||||
let newLinkImgElement = newLinkCard.querySelector(
|
||||
"div:first-child img"
|
||||
);
|
||||
newLinkImgElement.src = await processFile(data.get("icon"));
|
||||
newLinkImgElement.alt = data.get("name");
|
||||
|
||||
newLinkImgElement.src = await processFile(data.get("icon"));
|
||||
newLinkImgElement.alt = data.get("name");
|
||||
newLinkCard.querySelector("h3").textContent = data.get("name");
|
||||
newLinkCard.querySelector("p").textContent =
|
||||
data.get("description");
|
||||
|
||||
newLinkCard.querySelector("h3").textContent = data.get("name");
|
||||
newLinkCard.querySelector("p").textContent =
|
||||
data.get("description");
|
||||
newLinkCard.setAttribute("id", `${json.link.id}_link`);
|
||||
|
||||
newLinkCard.setAttribute("id", `${json.link.id}_link`);
|
||||
let editActions = cloneEditActions([
|
||||
{
|
||||
clickAction: "editLink(this)",
|
||||
label: "Edit link",
|
||||
},
|
||||
{
|
||||
clickAction: "deleteLink(this)",
|
||||
label: "Delete link",
|
||||
},
|
||||
]);
|
||||
|
||||
let editActions = cloneEditActions([
|
||||
{
|
||||
clickAction: "editLink(this)",
|
||||
label: "Edit link",
|
||||
},
|
||||
{
|
||||
clickAction: "deleteLink(this)",
|
||||
label: "Delete link",
|
||||
},
|
||||
]);
|
||||
editActions.classList.add("absolute", "right-1", "top-1");
|
||||
|
||||
editActions.classList.add("absolute", "right-1", "top-1");
|
||||
newLinkCard.appendChild(editActions);
|
||||
|
||||
newLinkCard.appendChild(editActions);
|
||||
// append the card as the second to last element
|
||||
linkGrid.insertBefore(newLinkCard, linkGrid.lastElementChild);
|
||||
closeModal("link");
|
||||
|
||||
// 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;
|
||||
}
|
||||
// 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;
|
||||
});
|
||||
});
|
||||
|
||||
addErrorListener("category");
|
||||
@@ -170,74 +194,94 @@ document
|
||||
event.preventDefault();
|
||||
let data = new FormData(event.target);
|
||||
|
||||
let res = await fetch(`/api/category`, {
|
||||
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`, {
|
||||
method: "POST",
|
||||
body: data,
|
||||
});
|
||||
})
|
||||
.then(async (res) => {
|
||||
let json = await res.json();
|
||||
|
||||
if (res.status === 201) {
|
||||
let json = await res.json();
|
||||
if (!res.ok) {
|
||||
throw new Error(json.message);
|
||||
}
|
||||
|
||||
let newCategory = document
|
||||
.getElementById("template-category")
|
||||
.cloneNode(true);
|
||||
let newCategory = document
|
||||
.getElementById("template-category")
|
||||
.content.firstElementChild.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");
|
||||
editActions.classList.add("pl-2", "flex-shrink-0");
|
||||
|
||||
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")
|
||||
);
|
||||
|
||||
linkGrid
|
||||
.querySelector("div")
|
||||
.setAttribute(
|
||||
"onclick",
|
||||
`openModal('link', ${json.category.id})`
|
||||
categoryImg.querySelector("img").src = await processFile(
|
||||
data.get("icon")
|
||||
);
|
||||
|
||||
let addCategoryButton = document.getElementById(
|
||||
"add-category-button"
|
||||
);
|
||||
addCategoryButton.parentElement.insertBefore(
|
||||
categoryHeader,
|
||||
addCategoryButton
|
||||
);
|
||||
addCategoryButton.parentElement.insertBefore(
|
||||
linkGrid,
|
||||
addCategoryButton
|
||||
);
|
||||
linkGrid
|
||||
.querySelector("div")
|
||||
.setAttribute(
|
||||
"onclick",
|
||||
`openModal('link', ${json.category.id})`
|
||||
);
|
||||
|
||||
closeModal("category");
|
||||
let addCategoryButton = document.getElementById(
|
||||
"add-category-button"
|
||||
);
|
||||
addCategoryButton.parentElement.insertBefore(
|
||||
categoryHeader,
|
||||
addCategoryButton
|
||||
);
|
||||
addCategoryButton.parentElement.insertBefore(
|
||||
linkGrid,
|
||||
addCategoryButton
|
||||
);
|
||||
|
||||
// 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;
|
||||
}
|
||||
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;
|
||||
});
|
||||
});
|
||||
|
||||
// when the background is clicked, close the modal
|
||||
@@ -330,6 +374,9 @@ function closeModal() {
|
||||
document
|
||||
.getElementById(activeModal + "-contents")
|
||||
.classList.add("hidden");
|
||||
if (document.getElementById(`${activeModal}-message`) !== null) {
|
||||
document.getElementById(`${activeModal}-message`).innerText = "";
|
||||
}
|
||||
activeModal = null;
|
||||
}, 300);
|
||||
|
||||
@@ -374,22 +421,37 @@ function unteleportElement(element) {
|
||||
teleportElement(element, teleportStorage);
|
||||
}
|
||||
|
||||
function confirmEdit() {
|
||||
/**
|
||||
* Confirms the edit
|
||||
* @param {Event} ev The event that triggered the function
|
||||
*/
|
||||
async function confirmEdit(ev) {
|
||||
if (currentlyEditing.cleanup !== undefined) {
|
||||
// this function could be called via deleting something, which doesn't have a cleanup function
|
||||
currentlyEditing.cleanup();
|
||||
}
|
||||
|
||||
switch (currentlyEditing.type) {
|
||||
case "link":
|
||||
confirmLinkEdit();
|
||||
break;
|
||||
case "category":
|
||||
confirmCategoryEdit();
|
||||
break;
|
||||
default:
|
||||
console.error("Unknown currentlyEditing type");
|
||||
break;
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -450,7 +512,7 @@ function editLink(target) {
|
||||
throw new Error("failed to find link ID or category ID");
|
||||
}
|
||||
|
||||
iconUploadInput.accept = "image/*";
|
||||
iconUploadInput.accept = "image/jpeg,image/png,image/webp,image/svg+xml";
|
||||
targetedImageElement = linkImg;
|
||||
|
||||
teleportElement(selectIconButton, linkImg.parentElement);
|
||||
@@ -470,6 +532,10 @@ 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");
|
||||
@@ -504,23 +570,19 @@ async function confirmLinkEdit() {
|
||||
return;
|
||||
}
|
||||
|
||||
let res = await fetch(
|
||||
await fetch(
|
||||
`/api/category/${currentlyEditing.categoryID}/link/${currentlyEditing.linkID}`,
|
||||
{
|
||||
method: "PATCH",
|
||||
body: formData,
|
||||
}
|
||||
);
|
||||
|
||||
if (res.status === 200) {
|
||||
).then(() => {
|
||||
iconUploadInput.value = "";
|
||||
|
||||
currentlyEditing.icon = undefined;
|
||||
cancelLinkEdit(linkNameInput.value, linkDescInput.value);
|
||||
currentlyEditing = {};
|
||||
} else {
|
||||
console.error("Failed to edit category");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function cancelLinkEdit(
|
||||
@@ -556,7 +618,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(".link-card");
|
||||
let linkEl = target.closest("[data-card]");
|
||||
let linkID = parseInt(linkEl.id);
|
||||
let categoryID = parseInt(linkEl.parentElement.previousElementSibling.id);
|
||||
|
||||
@@ -577,21 +639,47 @@ function deleteLink(target) {
|
||||
openModal("link-delete");
|
||||
}
|
||||
|
||||
async function confirmDeleteLink() {
|
||||
let res = await fetch(
|
||||
/**
|
||||
* 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(
|
||||
`/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);
|
||||
}
|
||||
|
||||
if (res.status === 200) {
|
||||
let linkEl = document.getElementById(`${currentlyEditing.linkID}_link`);
|
||||
linkEl.remove();
|
||||
let linkEl = document.getElementById(
|
||||
`${currentlyEditing.linkID}_link`
|
||||
);
|
||||
linkEl.remove();
|
||||
|
||||
closeModal();
|
||||
currentlyEditing = {};
|
||||
}
|
||||
closeModal();
|
||||
currentlyEditing = {};
|
||||
})
|
||||
.catch((err) => {
|
||||
document.getElementById(`delete-link-message`).innerText =
|
||||
err.message;
|
||||
})
|
||||
.finally(() => {
|
||||
deleteButton.disabled = false;
|
||||
cancelButton.disabled = false;
|
||||
deleteButton.innerHTML = originalContents;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -671,12 +759,10 @@ async function confirmCategoryEdit() {
|
||||
return;
|
||||
}
|
||||
|
||||
let res = await fetch(`/api/category/${currentlyEditing.categoryID}`, {
|
||||
await fetch(`/api/category/${currentlyEditing.categoryID}`, {
|
||||
method: "PATCH",
|
||||
body: formData,
|
||||
});
|
||||
|
||||
if (res.status === 200) {
|
||||
}).then(() => {
|
||||
iconUploadInput.value = "";
|
||||
|
||||
currentlyEditing.icon = undefined;
|
||||
@@ -684,9 +770,7 @@ async function confirmCategoryEdit() {
|
||||
cancelCategoryEdit(categoryInput.value);
|
||||
|
||||
currentlyEditing = {};
|
||||
} else {
|
||||
console.error("Failed to edit category");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function cancelCategoryEdit(text = currentlyEditing.originalText) {
|
||||
@@ -739,22 +823,42 @@ function deleteCategory(target) {
|
||||
}
|
||||
|
||||
async function confirmDeleteCategory() {
|
||||
let res = await fetch(`/api/category/${currentlyEditing.categoryID}`, {
|
||||
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}`, {
|
||||
method: "DELETE",
|
||||
});
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (!res.ok) {
|
||||
let json = await res.json();
|
||||
throw new Error(json.message);
|
||||
}
|
||||
|
||||
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();
|
||||
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 = {};
|
||||
}
|
||||
closeModal();
|
||||
currentlyEditing = {};
|
||||
})
|
||||
.catch((err) => {
|
||||
document.getElementById(`delete-category-message`).innerText =
|
||||
err.message;
|
||||
})
|
||||
.finally(() => {
|
||||
deleteButton.disabled = false;
|
||||
cancelButton.disabled = false;
|
||||
deleteButton.innerHTML = originalContents;
|
||||
});
|
||||
}
|
||||
|
||||
function roundToNearestHundredth(num) {
|
||||
@@ -771,6 +875,7 @@ const stylesToCopy = [
|
||||
"letter-spacing",
|
||||
"text-transform",
|
||||
"text-align",
|
||||
"text-wrap-style",
|
||||
];
|
||||
|
||||
let _textMeasuringSpan,
|
||||
@@ -788,8 +893,6 @@ 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.
|
||||
@@ -824,7 +927,7 @@ function replaceWithResizableTextarea(targetEls) {
|
||||
parseFloat(computedStyle.borderTopWidth) +
|
||||
parseFloat(computedStyle.borderBottomWidth);
|
||||
|
||||
let maxWidth = parentBoundingRect.width - borderWidth;
|
||||
let maxWidth = parentBoundingRect.width;
|
||||
// take care of category headers specifically because the parent bounding box contains two other elements
|
||||
if (targetEl.tagName === "H2") {
|
||||
let imageComputedStyle = window.getComputedStyle(
|
||||
@@ -957,9 +1060,6 @@ 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;
|
||||
@@ -977,14 +1077,13 @@ function replaceWithResizableTextarea(targetEls) {
|
||||
let imageWidth =
|
||||
inputElement.previousElementSibling.getBoundingClientRect()
|
||||
.width +
|
||||
imageComputedStyle.marginLeft +
|
||||
imageComputedStyle.marginRight;
|
||||
parseFloat(imageComputedStyle.marginLeft) +
|
||||
parseFloat(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 + caretBuffer;
|
||||
maxWidth += currentInputBorderWidth;
|
||||
maxWidth -= imageWidth + actionButtonWidth;
|
||||
}
|
||||
|
||||
let currentContentWidth;
|
||||
@@ -1017,9 +1116,7 @@ function replaceWithResizableTextarea(targetEls) {
|
||||
_textMeasuringSpan.getBoundingClientRect().width;
|
||||
|
||||
currentContentWidth = Math.min(
|
||||
roundToNearestHundredth(
|
||||
measuredTextWidth + currentInputBorderWidth
|
||||
) + caretBuffer,
|
||||
roundToNearestHundredth(measuredTextWidth) + caretBuffer,
|
||||
maxWidth
|
||||
);
|
||||
} else {
|
||||
|
||||
@@ -24,6 +24,7 @@ type UptimeRobotSite struct {
|
||||
FriendlyName string `json:"friendly_name"`
|
||||
Url string `json:"url"`
|
||||
Status int `json:"status"`
|
||||
Up bool `json:"-"`
|
||||
}
|
||||
|
||||
type UptimeManager struct {
|
||||
@@ -104,6 +105,10 @@ 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()
|
||||
|
||||
@@ -82,6 +82,9 @@
|
||||
}
|
||||
|
||||
& > 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);
|
||||
@@ -104,6 +107,9 @@
|
||||
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);
|
||||
@@ -120,12 +126,14 @@
|
||||
background-color: #0000;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
filter: brightness(125%);
|
||||
}
|
||||
&:not([disabled]) {
|
||||
&:hover {
|
||||
filter: brightness(125%);
|
||||
}
|
||||
|
||||
&:active {
|
||||
filter: brightness(95%);
|
||||
&:active {
|
||||
filter: brightness(95%);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -192,12 +200,14 @@
|
||||
transition: filter 0.15s cubic-bezier(0.45, 0, 0.55, 1);
|
||||
contain: layout style paint;
|
||||
|
||||
&:hover {
|
||||
filter: brightness(125%);
|
||||
}
|
||||
&:not([disabled]) {
|
||||
&:hover {
|
||||
filter: brightness(125%);
|
||||
}
|
||||
|
||||
&:active {
|
||||
filter: brightness(95%);
|
||||
&:active {
|
||||
filter: brightness(95%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -214,6 +224,11 @@
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
button[disabled] {
|
||||
cursor: not-allowed;
|
||||
filter: brightness(75%);
|
||||
}
|
||||
|
||||
header {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
|
||||
@@ -111,7 +111,6 @@
|
||||
|
||||
.category-header h2 {
|
||||
text-transform: capitalize;
|
||||
word-break: break-all;
|
||||
border-width: 1px;
|
||||
border-color: #0000;
|
||||
}
|
||||
@@ -146,3 +145,13 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@layer utilities {
|
||||
.flex-shrink-0 {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.pl-2 {
|
||||
padding-left: calc(var(--spacing) * 2);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,9 +5,9 @@
|
||||
<div>
|
||||
<img width="32" height="32" draggable="false" alt="{{this.Name}}" src="{{this.Icon}}" />
|
||||
</div>
|
||||
<h2 class="capitalize break-all">{{this.Name}}</h2>
|
||||
<h2>{{this.Name}}</h2>
|
||||
{{#if IsAdmin}}
|
||||
<div>
|
||||
<div class="flex-shrink-0 pl-2">
|
||||
<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">
|
||||
|
||||
@@ -4,8 +4,9 @@
|
||||
undone.
|
||||
All links associated with this category will also be deleted. Are you sure you want to continue?</p>
|
||||
<div>
|
||||
<button onclick="confirmDeleteCategory()">Delete
|
||||
<button onclick="confirmDeleteCategory(event)">Delete
|
||||
category</button>
|
||||
<button onclick="closeModal()">Cancel</button>
|
||||
</div>
|
||||
<span id="delete-category-message"></span>
|
||||
</div>
|
||||
@@ -4,8 +4,9 @@
|
||||
you sure you
|
||||
want to continue?</p>
|
||||
<div>
|
||||
<button onclick="confirmDeleteLink()">Delete
|
||||
<button onclick="confirmDeleteLink(event)">Delete
|
||||
link</button>
|
||||
<button onclick="closeModal()">Cancel</button>
|
||||
</div>
|
||||
<span id="delete-link-message"></span>
|
||||
</div>
|
||||
@@ -15,7 +15,8 @@
|
||||
</div>
|
||||
<div>
|
||||
<label for="linkIcon">Icon</label>
|
||||
<input required type="file" name="icon" id="linkIcon" accept="image/*" />
|
||||
<input required type="file" name="icon" id="linkIcon"
|
||||
accept="image/jpeg,image/png,image/webp,image/svg+xml" />
|
||||
</div>
|
||||
<button type="submit">Add
|
||||
link</button>
|
||||
|
||||
@@ -40,65 +40,85 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- store a blank link card so that if we add a new link we can clone it to make the editing experience easier -->
|
||||
<div id="template-link-card" class="hidden">
|
||||
<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 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>
|
||||
|
||||
<div id="template-category" class="hidden">
|
||||
<div class="category-header">
|
||||
<!-- 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="32" height="32" draggable="false" />
|
||||
<img width="64" height="64" draggable="false" />
|
||||
</div>
|
||||
<div>
|
||||
<h3></h3>
|
||||
<!-- add 2 to the height to account for the border -->
|
||||
<p></p>
|
||||
</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>
|
||||
</template>
|
||||
|
||||
<template id="template-category">
|
||||
<div>
|
||||
<div class="category-header">
|
||||
<div>
|
||||
<h3>Add a link</h3>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<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>
|
||||
<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>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<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()">
|
||||
<button class="action-button text-success" onclick="confirmEdit(event)">
|
||||
<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"
|
||||
|
||||
@@ -39,13 +39,11 @@
|
||||
</span>
|
||||
<div class="uptime-status">
|
||||
<svg viewBox="0 0 10 10">
|
||||
<use href="#status-dot"
|
||||
class="{{#if (eq this.Status 2)}}text-success{{else}}text-error{{/if}}">
|
||||
<use href="#status-dot" class="{{#if this.Up}}text-success{{else}}text-error{{/if}}">
|
||||
</use>
|
||||
</svg>
|
||||
<svg viewBox="0 0 10 10">
|
||||
<use href="#status-dot"
|
||||
class="{{#if (eq this.Status 2)}}text-success{{else}}text-error{{/if}}">
|
||||
<use href="#status-dot" class="{{#if this.Up}}text-success{{else}}text-error{{/if}}">
|
||||
</use>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "passport",
|
||||
"version": "0.3.3",
|
||||
"version": "0.3.4",
|
||||
"description": "Passport is a simple, lightweight, and fast dashboard/new tab page for your browser.",
|
||||
"author": "juls0730",
|
||||
"license": "BSL-1.0",
|
||||
@@ -11,13 +11,15 @@
|
||||
},
|
||||
"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",
|
||||
"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"
|
||||
"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"
|
||||
},
|
||||
"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"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user