Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
3ffd439f88
|
|||
|
68284bc963
|
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,4 +1,5 @@
|
|||||||
passport
|
passport
|
||||||
|
passport-*
|
||||||
.env
|
.env
|
||||||
passport.db*
|
passport.db*
|
||||||
public
|
public
|
||||||
|
|||||||
16
Dockerfile
16
Dockerfile
@@ -5,21 +5,6 @@ RUN apt update && apt install -y upx unzip
|
|||||||
|
|
||||||
RUN curl -fsSL https://bun.com/install | BUN_INSTALL=/usr bash
|
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
|
RUN go install github.com/juls0730/zqdgr@latest
|
||||||
|
|
||||||
@@ -35,7 +20,6 @@ COPY . .
|
|||||||
RUN bun install
|
RUN bun install
|
||||||
|
|
||||||
RUN zqdgr build
|
RUN zqdgr build
|
||||||
RUN upx passport
|
|
||||||
|
|
||||||
# ---- Runtime Stage ----
|
# ---- Runtime Stage ----
|
||||||
FROM gcr.io/distroless/static-debian12 AS runner
|
FROM gcr.io/distroless/static-debian12 AS runner
|
||||||
|
|||||||
@@ -73,9 +73,9 @@ 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.
|
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 |
|
| 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_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_TEMP_UNITS` | The temperature units to use, either `metric` or `imperial` | false | metric |
|
||||||
| `WEATHER_LAT` | The latitude of your location | true | |
|
| `WEATHER_LAT` | The latitude of your location | true | |
|
||||||
| `WEATHER_LON` | The longitude of your location | true | |
|
| `WEATHER_LON` | The longitude of your location | true | |
|
||||||
@@ -86,8 +86,8 @@ The weather integration is optional, and will be enabled automatically if you pr
|
|||||||
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.
|
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 |
|
| Environment Variable | Description | Required | Default |
|
||||||
| ------------------------ | ------------------------------------------------- | -------- | ------- |
|
| ------------------------- | ------------------------------------------------- | -------- | ------- |
|
||||||
| `UPTIME_API_KEY` | The UptimeRobot API key | true | |
|
| `PASSPORT_UPTIME_API_KEY` | The UptimeRobot API key | true | |
|
||||||
| `UPTIME_UPDATE_INTERVAL` | The interval in seconds to update the uptime data | false | 300 |
|
| `UPTIME_UPDATE_INTERVAL` | The interval in seconds to update the uptime data | false | 300 |
|
||||||
|
|
||||||
### Adding links and categories
|
### Adding links and categories
|
||||||
|
|||||||
5
bun.lock
5
bun.lock
@@ -5,6 +5,7 @@
|
|||||||
"name": "passport-css-compiler",
|
"name": "passport-css-compiler",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@fullhuman/postcss-purgecss": "^7.0.2",
|
"@fullhuman/postcss-purgecss": "^7.0.2",
|
||||||
|
"baseline-browser-mapping": "^2.9.2",
|
||||||
"cssnano": "^7.1.1",
|
"cssnano": "^7.1.1",
|
||||||
"postcss": "^8.4.35",
|
"postcss": "^8.4.35",
|
||||||
"postcss-cli": "^11.0.0",
|
"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=="],
|
"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=="],
|
"binary-extensions": ["binary-extensions@2.3.0", "", {}, "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw=="],
|
||||||
|
|
||||||
@@ -478,6 +479,8 @@
|
|||||||
|
|
||||||
"anymatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
|
"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-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=="],
|
"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": {
|
"devDependencies": {
|
||||||
"@fullhuman/postcss-purgecss": "^7.0.2",
|
"@fullhuman/postcss-purgecss": "^7.0.2",
|
||||||
|
"baseline-browser-mapping": "^2.9.2",
|
||||||
"cssnano": "^7.1.1",
|
"cssnano": "^7.1.1",
|
||||||
"postcss": "^8.4.35",
|
"postcss": "^8.4.35",
|
||||||
"postcss-cli": "^11.0.0",
|
"postcss-cli": "^11.0.0",
|
||||||
|
|||||||
41
src/main.go
41
src/main.go
@@ -80,7 +80,15 @@ socket.addEventListener('message', (event) => {
|
|||||||
setTimeout(testPage, 150);
|
setTimeout(testPage, 150);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>`
|
</script>
|
||||||
|
<style>
|
||||||
|
html {
|
||||||
|
outline-color: yellow;
|
||||||
|
outline-width: 5px;
|
||||||
|
outline-style: dashed;
|
||||||
|
outline-offset: -5px;
|
||||||
|
}
|
||||||
|
</style>`
|
||||||
|
|
||||||
var (
|
var (
|
||||||
insertCategoryStmt *sql.Stmt
|
insertCategoryStmt *sql.Stmt
|
||||||
@@ -717,10 +725,6 @@ func main() {
|
|||||||
|
|
||||||
engine := handlebars.NewFileSystem(http.FS(templatesDir), ".hbs")
|
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 {
|
engine.AddFunc("embedFile", func(fileToEmbed string) string {
|
||||||
content, err := fs.ReadFile(embeddedAssets, fileToEmbed)
|
content, err := fs.ReadFile(embeddedAssets, fileToEmbed)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -1015,8 +1019,13 @@ func main() {
|
|||||||
iconPath, err := UploadFile(file, contentType, c)
|
iconPath, err := UploadFile(file, contentType, c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Error("Failed to upload file", "error", err)
|
slog.Error("Failed to upload file", "error", err)
|
||||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
status := fiber.StatusInternalServerError
|
||||||
"message": "Failed to upload file, please try again!",
|
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)
|
iconPath, err := UploadFile(file, contentType, c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Error("Failed to upload file", "error", err)
|
slog.Error("Failed to upload file", "error", err)
|
||||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
status := fiber.StatusInternalServerError
|
||||||
"message": "Failed to upload file, please try again!",
|
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)
|
iconPath, err := UploadFile(file, contentType, c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Error("Failed to upload file", "error", err)
|
slog.Error("Failed to upload file", "error", err)
|
||||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
status := fiber.StatusInternalServerError
|
||||||
"message": "Failed to upload file, please try again!",
|
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 teleportStorage = document.getElementById("teleport-storage");
|
||||||
let confirmActions = document.getElementById("confirm-actions");
|
let confirmActions = document.getElementById("confirm-actions");
|
||||||
let selectIconButton = document.getElementById("select-icon-button");
|
let selectIconButton = document.getElementById("select-icon-button");
|
||||||
|
let loadingSpinner = document.getElementById("template-loading-spinner");
|
||||||
|
|
||||||
document.addEventListener("DOMContentLoaded", () => {
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
modalContainer.classList.remove("hidden");
|
modalContainer.classList.remove("hidden");
|
||||||
modalContainer.classList.add("flex");
|
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
|
* Adds an event listener for the given from to error check after the first submit
|
||||||
* @param {"category" | "link"} form - The form to initialize
|
* @param {"category" | "link"} form - The form to initialize
|
||||||
@@ -76,9 +51,7 @@ function addErrorListener(form) {
|
|||||||
function cloneEditActions(primaryActions) {
|
function cloneEditActions(primaryActions) {
|
||||||
let editActions = document
|
let editActions = document
|
||||||
.getElementById("template-edit-actions")
|
.getElementById("template-edit-actions")
|
||||||
.cloneNode(true);
|
.content.firstElementChild.cloneNode(true);
|
||||||
editActions.removeAttribute("id");
|
|
||||||
editActions.classList.remove("hidden");
|
|
||||||
|
|
||||||
let i = 0;
|
let i = 0;
|
||||||
for (i = 0; i < primaryActions.length; i++) {
|
for (i = 0; i < primaryActions.length; i++) {
|
||||||
@@ -94,6 +67,46 @@ function cloneEditActions(primaryActions) {
|
|||||||
return editActions;
|
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");
|
addErrorListener("link");
|
||||||
document
|
document
|
||||||
.getElementById("link-form")
|
.getElementById("link-form")
|
||||||
@@ -101,14 +114,23 @@ document
|
|||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
let data = new FormData(event.target);
|
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",
|
method: "POST",
|
||||||
body: data,
|
body: data,
|
||||||
});
|
})
|
||||||
|
.then(async (res) => {
|
||||||
if (res.status === 201) {
|
|
||||||
let json = await res.json();
|
let json = await res.json();
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
throw new Error(json.message);
|
||||||
|
}
|
||||||
|
|
||||||
let category = document.getElementById(
|
let category = document.getElementById(
|
||||||
`${targetCategoryID}_category`
|
`${targetCategoryID}_category`
|
||||||
);
|
);
|
||||||
@@ -116,10 +138,7 @@ document
|
|||||||
|
|
||||||
let newLinkCard = document
|
let newLinkCard = document
|
||||||
.getElementById("template-link-card")
|
.getElementById("template-link-card")
|
||||||
.cloneNode(true);
|
.content.firstElementChild.cloneNode(true);
|
||||||
|
|
||||||
newLinkCard.classList.remove("hidden");
|
|
||||||
newLinkCard.classList.add("link-card", "admin", "relative");
|
|
||||||
|
|
||||||
let newLinkImgElement = newLinkCard.querySelector(
|
let newLinkImgElement = newLinkCard.querySelector(
|
||||||
"div:first-child img"
|
"div:first-child img"
|
||||||
@@ -156,11 +175,16 @@ document
|
|||||||
// after the close animation plays
|
// after the close animation plays
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
document.getElementById(`link-form`).reset();
|
document.getElementById(`link-form`).reset();
|
||||||
|
document.getElementById(`link-message`).innerText = "";
|
||||||
}, 300);
|
}, 300);
|
||||||
} else {
|
})
|
||||||
let json = await res.json();
|
.catch((err) => {
|
||||||
document.getElementById(`link-message`).innerText = json.message;
|
document.getElementById(`link-message`).innerText = err.message;
|
||||||
}
|
})
|
||||||
|
.finally(() => {
|
||||||
|
submitButton.disabled = false;
|
||||||
|
submitButton.innerHTML = originalContents;
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
addErrorListener("category");
|
addErrorListener("category");
|
||||||
@@ -170,22 +194,36 @@ document
|
|||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
let data = new FormData(event.target);
|
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",
|
method: "POST",
|
||||||
body: data,
|
body: data,
|
||||||
});
|
})
|
||||||
|
.then(async (res) => {
|
||||||
if (res.status === 201) {
|
|
||||||
let json = await res.json();
|
let json = await res.json();
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
throw new Error(json.message);
|
||||||
|
}
|
||||||
|
|
||||||
let newCategory = document
|
let newCategory = document
|
||||||
.getElementById("template-category")
|
.getElementById("template-category")
|
||||||
.cloneNode(true);
|
.content.firstElementChild.cloneNode(true);
|
||||||
|
|
||||||
let linkGrid = newCategory.querySelector("div:nth-child(2)");
|
let linkGrid = newCategory.querySelector("div:nth-child(2)");
|
||||||
let categoryHeader = newCategory.querySelector(".category-header");
|
let categoryHeader =
|
||||||
categoryHeader.setAttribute("id", `${json.category.id}_category`);
|
newCategory.querySelector(".category-header");
|
||||||
categoryHeader.querySelector("h2").textContent = json.category.name;
|
categoryHeader.setAttribute(
|
||||||
|
"id",
|
||||||
|
`${json.category.id}_category`
|
||||||
|
);
|
||||||
|
categoryHeader.querySelector("h2").textContent =
|
||||||
|
json.category.name;
|
||||||
|
|
||||||
let editActions = cloneEditActions([
|
let editActions = cloneEditActions([
|
||||||
{
|
{
|
||||||
@@ -198,11 +236,12 @@ document
|
|||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
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(
|
categoryImg.querySelector("img").src = await processFile(
|
||||||
data.get("icon")
|
data.get("icon")
|
||||||
@@ -232,12 +271,17 @@ document
|
|||||||
// after the close animation plays
|
// after the close animation plays
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
document.getElementById(`category-form`).reset();
|
document.getElementById(`category-form`).reset();
|
||||||
|
document.getElementById(`category-message`).innerText = "";
|
||||||
}, 300);
|
}, 300);
|
||||||
} else {
|
})
|
||||||
let json = await res.json();
|
.catch((err) => {
|
||||||
document.getElementById(`category-message`).innerText =
|
document.getElementById(`category-message`).innerText =
|
||||||
json.message;
|
err.message;
|
||||||
}
|
})
|
||||||
|
.finally(() => {
|
||||||
|
submitButton.disabled = false;
|
||||||
|
submitButton.innerHTML = originalContents;
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// when the background is clicked, close the modal
|
// when the background is clicked, close the modal
|
||||||
@@ -330,6 +374,9 @@ function closeModal() {
|
|||||||
document
|
document
|
||||||
.getElementById(activeModal + "-contents")
|
.getElementById(activeModal + "-contents")
|
||||||
.classList.add("hidden");
|
.classList.add("hidden");
|
||||||
|
if (document.getElementById(`${activeModal}-message`) !== null) {
|
||||||
|
document.getElementById(`${activeModal}-message`).innerText = "";
|
||||||
|
}
|
||||||
activeModal = null;
|
activeModal = null;
|
||||||
}, 300);
|
}, 300);
|
||||||
|
|
||||||
@@ -374,23 +421,38 @@ function unteleportElement(element) {
|
|||||||
teleportElement(element, teleportStorage);
|
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) {
|
if (currentlyEditing.cleanup !== undefined) {
|
||||||
// this function could be called via deleting something, which doesn't have a cleanup function
|
// this function could be called via deleting something, which doesn't have a cleanup function
|
||||||
currentlyEditing.cleanup();
|
currentlyEditing.cleanup();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let confirmButton = ev.target.closest("button");
|
||||||
|
|
||||||
|
try {
|
||||||
|
setConfirmLoading(confirmButton);
|
||||||
|
|
||||||
switch (currentlyEditing.type) {
|
switch (currentlyEditing.type) {
|
||||||
case "link":
|
case "link":
|
||||||
confirmLinkEdit();
|
await confirmLinkEdit();
|
||||||
break;
|
break;
|
||||||
case "category":
|
case "category":
|
||||||
confirmCategoryEdit();
|
await confirmCategoryEdit();
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
console.error("Unknown currentlyEditing type");
|
console.error("Unknown currentlyEditing type");
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
} catch (err) {
|
||||||
|
// TODO: tell the user that something went wrong?
|
||||||
|
console.error(err);
|
||||||
|
} finally {
|
||||||
|
clearConfirmLoading(confirmButton);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function cancelEdit() {
|
function cancelEdit() {
|
||||||
@@ -450,7 +512,7 @@ function editLink(target) {
|
|||||||
throw new Error("failed to find link ID or category ID");
|
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;
|
targetedImageElement = linkImg;
|
||||||
|
|
||||||
teleportElement(selectIconButton, linkImg.parentElement);
|
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() {
|
async function confirmLinkEdit() {
|
||||||
let linkEl = document.getElementById(`${currentlyEditing.linkID}_link`);
|
let linkEl = document.getElementById(`${currentlyEditing.linkID}_link`);
|
||||||
let linkNameInput = linkEl.querySelector("textarea");
|
let linkNameInput = linkEl.querySelector("textarea");
|
||||||
@@ -504,23 +570,19 @@ async function confirmLinkEdit() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let res = await fetch(
|
await fetch(
|
||||||
`/api/category/${currentlyEditing.categoryID}/link/${currentlyEditing.linkID}`,
|
`/api/category/${currentlyEditing.categoryID}/link/${currentlyEditing.linkID}`,
|
||||||
{
|
{
|
||||||
method: "PATCH",
|
method: "PATCH",
|
||||||
body: formData,
|
body: formData,
|
||||||
}
|
}
|
||||||
);
|
).then(() => {
|
||||||
|
|
||||||
if (res.status === 200) {
|
|
||||||
iconUploadInput.value = "";
|
iconUploadInput.value = "";
|
||||||
|
|
||||||
currentlyEditing.icon = undefined;
|
currentlyEditing.icon = undefined;
|
||||||
cancelLinkEdit(linkNameInput.value, linkDescInput.value);
|
cancelLinkEdit(linkNameInput.value, linkDescInput.value);
|
||||||
currentlyEditing = {};
|
currentlyEditing = {};
|
||||||
} else {
|
});
|
||||||
console.error("Failed to edit category");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function cancelLinkEdit(
|
function cancelLinkEdit(
|
||||||
@@ -556,7 +618,7 @@ function cancelLinkEdit(
|
|||||||
*/
|
*/
|
||||||
function deleteLink(target) {
|
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
|
// 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 linkID = parseInt(linkEl.id);
|
||||||
let categoryID = parseInt(linkEl.parentElement.previousElementSibling.id);
|
let categoryID = parseInt(linkEl.parentElement.previousElementSibling.id);
|
||||||
|
|
||||||
@@ -577,21 +639,47 @@ function deleteLink(target) {
|
|||||||
openModal("link-delete");
|
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}`,
|
`/api/category/${currentlyEditing.categoryID}/link/${currentlyEditing.linkID}`,
|
||||||
{
|
{
|
||||||
method: "DELETE",
|
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(
|
||||||
let linkEl = document.getElementById(`${currentlyEditing.linkID}_link`);
|
`${currentlyEditing.linkID}_link`
|
||||||
|
);
|
||||||
linkEl.remove();
|
linkEl.remove();
|
||||||
|
|
||||||
closeModal();
|
closeModal();
|
||||||
currentlyEditing = {};
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let res = await fetch(`/api/category/${currentlyEditing.categoryID}`, {
|
await fetch(`/api/category/${currentlyEditing.categoryID}`, {
|
||||||
method: "PATCH",
|
method: "PATCH",
|
||||||
body: formData,
|
body: formData,
|
||||||
});
|
}).then(() => {
|
||||||
|
|
||||||
if (res.status === 200) {
|
|
||||||
iconUploadInput.value = "";
|
iconUploadInput.value = "";
|
||||||
|
|
||||||
currentlyEditing.icon = undefined;
|
currentlyEditing.icon = undefined;
|
||||||
@@ -684,9 +770,7 @@ async function confirmCategoryEdit() {
|
|||||||
cancelCategoryEdit(categoryInput.value);
|
cancelCategoryEdit(categoryInput.value);
|
||||||
|
|
||||||
currentlyEditing = {};
|
currentlyEditing = {};
|
||||||
} else {
|
});
|
||||||
console.error("Failed to edit category");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function cancelCategoryEdit(text = currentlyEditing.originalText) {
|
function cancelCategoryEdit(text = currentlyEditing.originalText) {
|
||||||
@@ -739,11 +823,22 @@ function deleteCategory(target) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function confirmDeleteCategory() {
|
async function confirmDeleteCategory() {
|
||||||
let res = await fetch(`/api/category/${currentlyEditing.categoryID}`, {
|
const originalContents = ev.target.innerHTML;
|
||||||
method: "DELETE",
|
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(
|
let categoryEl = document.getElementById(
|
||||||
`${currentlyEditing.categoryID}_category`
|
`${currentlyEditing.categoryID}_category`
|
||||||
);
|
);
|
||||||
@@ -754,7 +849,16 @@ async function confirmDeleteCategory() {
|
|||||||
|
|
||||||
closeModal();
|
closeModal();
|
||||||
currentlyEditing = {};
|
currentlyEditing = {};
|
||||||
}
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
document.getElementById(`delete-category-message`).innerText =
|
||||||
|
err.message;
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
deleteButton.disabled = false;
|
||||||
|
cancelButton.disabled = false;
|
||||||
|
deleteButton.innerHTML = originalContents;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function roundToNearestHundredth(num) {
|
function roundToNearestHundredth(num) {
|
||||||
@@ -771,6 +875,7 @@ const stylesToCopy = [
|
|||||||
"letter-spacing",
|
"letter-spacing",
|
||||||
"text-transform",
|
"text-transform",
|
||||||
"text-align",
|
"text-align",
|
||||||
|
"text-wrap-style",
|
||||||
];
|
];
|
||||||
|
|
||||||
let _textMeasuringSpan,
|
let _textMeasuringSpan,
|
||||||
@@ -788,8 +893,6 @@ let _textMeasuringSpan,
|
|||||||
* @returns (() => void) A cleanup function to remove event listeners
|
* @returns (() => void) A cleanup function to remove event listeners
|
||||||
*/
|
*/
|
||||||
function replaceWithResizableTextarea(targetEls) {
|
function replaceWithResizableTextarea(targetEls) {
|
||||||
let startTime = performance.now();
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {Object} TargetInfo
|
* @typedef {Object} TargetInfo
|
||||||
* @property {HTMLElement} targetEl The element to replace.
|
* @property {HTMLElement} targetEl The element to replace.
|
||||||
@@ -824,7 +927,7 @@ function replaceWithResizableTextarea(targetEls) {
|
|||||||
parseFloat(computedStyle.borderTopWidth) +
|
parseFloat(computedStyle.borderTopWidth) +
|
||||||
parseFloat(computedStyle.borderBottomWidth);
|
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
|
// take care of category headers specifically because the parent bounding box contains two other elements
|
||||||
if (targetEl.tagName === "H2") {
|
if (targetEl.tagName === "H2") {
|
||||||
let imageComputedStyle = window.getComputedStyle(
|
let imageComputedStyle = window.getComputedStyle(
|
||||||
@@ -957,9 +1060,6 @@ function replaceWithResizableTextarea(targetEls) {
|
|||||||
|
|
||||||
function resize(inputElement, fill = false) {
|
function resize(inputElement, fill = false) {
|
||||||
const currentInputComputedStyle = window.getComputedStyle(inputElement);
|
const currentInputComputedStyle = window.getComputedStyle(inputElement);
|
||||||
const currentInputBorderWidth =
|
|
||||||
parseFloat(currentInputComputedStyle.borderLeftWidth) +
|
|
||||||
parseFloat(currentInputComputedStyle.borderRightWidth);
|
|
||||||
|
|
||||||
const currentParentElBoundingRectWidth =
|
const currentParentElBoundingRectWidth =
|
||||||
inputElement.parentElement.getBoundingClientRect().width;
|
inputElement.parentElement.getBoundingClientRect().width;
|
||||||
@@ -977,14 +1077,13 @@ function replaceWithResizableTextarea(targetEls) {
|
|||||||
let imageWidth =
|
let imageWidth =
|
||||||
inputElement.previousElementSibling.getBoundingClientRect()
|
inputElement.previousElementSibling.getBoundingClientRect()
|
||||||
.width +
|
.width +
|
||||||
imageComputedStyle.marginLeft +
|
parseFloat(imageComputedStyle.marginLeft) +
|
||||||
imageComputedStyle.marginRight;
|
parseFloat(imageComputedStyle.marginRight);
|
||||||
let actionButtonWidth =
|
let actionButtonWidth =
|
||||||
inputElement.nextElementSibling.getBoundingClientRect().width;
|
inputElement.nextElementSibling.getBoundingClientRect().width;
|
||||||
|
|
||||||
// the brain cells rub together and this vaguely makes sense to me I think but I cant explain it
|
// the brain cells rub together and this vaguely makes sense to me I think but I cant explain it
|
||||||
maxWidth -= imageWidth + actionButtonWidth + caretBuffer;
|
maxWidth -= imageWidth + actionButtonWidth;
|
||||||
maxWidth += currentInputBorderWidth;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let currentContentWidth;
|
let currentContentWidth;
|
||||||
@@ -1017,9 +1116,7 @@ function replaceWithResizableTextarea(targetEls) {
|
|||||||
_textMeasuringSpan.getBoundingClientRect().width;
|
_textMeasuringSpan.getBoundingClientRect().width;
|
||||||
|
|
||||||
currentContentWidth = Math.min(
|
currentContentWidth = Math.min(
|
||||||
roundToNearestHundredth(
|
roundToNearestHundredth(measuredTextWidth) + caretBuffer,
|
||||||
measuredTextWidth + currentInputBorderWidth
|
|
||||||
) + caretBuffer,
|
|
||||||
maxWidth
|
maxWidth
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ type UptimeRobotSite struct {
|
|||||||
FriendlyName string `json:"friendly_name"`
|
FriendlyName string `json:"friendly_name"`
|
||||||
Url string `json:"url"`
|
Url string `json:"url"`
|
||||||
Status int `json:"status"`
|
Status int `json:"status"`
|
||||||
|
Up bool `json:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type UptimeManager struct {
|
type UptimeManager struct {
|
||||||
@@ -104,6 +105,10 @@ func (u *UptimeManager) update() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for i, monitor := range monitors.Monitors {
|
||||||
|
monitors.Monitors[i].Up = monitor.Status == 2
|
||||||
|
}
|
||||||
|
|
||||||
u.mutex.Lock()
|
u.mutex.Lock()
|
||||||
u.sites = monitors.Monitors
|
u.sites = monitors.Monitors
|
||||||
u.lastUpdate = time.Now()
|
u.lastUpdate = time.Now()
|
||||||
|
|||||||
@@ -82,6 +82,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
& > button {
|
& > button {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
column-gap: calc(var(--spacing) * 2);
|
||||||
background-color: var(--color-accent);
|
background-color: var(--color-accent);
|
||||||
color: #fff;
|
color: #fff;
|
||||||
border-radius: calc(var(--spacing) * 1.5);
|
border-radius: calc(var(--spacing) * 1.5);
|
||||||
@@ -104,6 +107,9 @@
|
|||||||
row-gap: calc(var(--spacing) * 2);
|
row-gap: calc(var(--spacing) * 2);
|
||||||
|
|
||||||
& > button {
|
& > button {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
column-gap: calc(var(--spacing) * 2);
|
||||||
padding-inline: calc(var(--spacing) * 4);
|
padding-inline: calc(var(--spacing) * 4);
|
||||||
padding-block: calc(var(--spacing) * 2);
|
padding-block: calc(var(--spacing) * 2);
|
||||||
border-radius: calc(var(--spacing) * 1.5);
|
border-radius: calc(var(--spacing) * 1.5);
|
||||||
@@ -120,6 +126,7 @@
|
|||||||
background-color: #0000;
|
background-color: #0000;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&:not([disabled]) {
|
||||||
&:hover {
|
&:hover {
|
||||||
filter: brightness(125%);
|
filter: brightness(125%);
|
||||||
}
|
}
|
||||||
@@ -130,6 +137,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
input:invalid.invalid {
|
input:invalid.invalid {
|
||||||
border: 1px solid var(--color-error);
|
border: 1px solid var(--color-error);
|
||||||
@@ -192,6 +200,7 @@
|
|||||||
transition: filter 0.15s cubic-bezier(0.45, 0, 0.55, 1);
|
transition: filter 0.15s cubic-bezier(0.45, 0, 0.55, 1);
|
||||||
contain: layout style paint;
|
contain: layout style paint;
|
||||||
|
|
||||||
|
&:not([disabled]) {
|
||||||
&:hover {
|
&:hover {
|
||||||
filter: brightness(125%);
|
filter: brightness(125%);
|
||||||
}
|
}
|
||||||
@@ -200,6 +209,7 @@
|
|||||||
filter: brightness(95%);
|
filter: brightness(95%);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.select-icon-button {
|
.select-icon-button {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -214,6 +224,11 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
button[disabled] {
|
||||||
|
cursor: not-allowed;
|
||||||
|
filter: brightness(75%);
|
||||||
|
}
|
||||||
|
|
||||||
header {
|
header {
|
||||||
display: flex;
|
display: flex;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|||||||
@@ -111,7 +111,6 @@
|
|||||||
|
|
||||||
.category-header h2 {
|
.category-header h2 {
|
||||||
text-transform: capitalize;
|
text-transform: capitalize;
|
||||||
word-break: break-all;
|
|
||||||
border-width: 1px;
|
border-width: 1px;
|
||||||
border-color: #0000;
|
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>
|
<div>
|
||||||
<img width="32" height="32" draggable="false" alt="{{this.Name}}" src="{{this.Icon}}" />
|
<img width="32" height="32" draggable="false" alt="{{this.Name}}" src="{{this.Icon}}" />
|
||||||
</div>
|
</div>
|
||||||
<h2 class="capitalize break-all">{{this.Name}}</h2>
|
<h2>{{this.Name}}</h2>
|
||||||
{{#if IsAdmin}}
|
{{#if IsAdmin}}
|
||||||
<div>
|
<div class="flex-shrink-0 pl-2">
|
||||||
<div class="action-container">
|
<div class="action-container">
|
||||||
<button aria-label="Edit category" onclick="editCategory(this)" class="action-button">
|
<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">
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
|
||||||
|
|||||||
@@ -4,8 +4,9 @@
|
|||||||
undone.
|
undone.
|
||||||
All links associated with this category will also be deleted. Are you sure you want to continue?</p>
|
All links associated with this category will also be deleted. Are you sure you want to continue?</p>
|
||||||
<div>
|
<div>
|
||||||
<button onclick="confirmDeleteCategory()">Delete
|
<button onclick="confirmDeleteCategory(event)">Delete
|
||||||
category</button>
|
category</button>
|
||||||
<button onclick="closeModal()">Cancel</button>
|
<button onclick="closeModal()">Cancel</button>
|
||||||
</div>
|
</div>
|
||||||
|
<span id="delete-category-message"></span>
|
||||||
</div>
|
</div>
|
||||||
@@ -4,8 +4,9 @@
|
|||||||
you sure you
|
you sure you
|
||||||
want to continue?</p>
|
want to continue?</p>
|
||||||
<div>
|
<div>
|
||||||
<button onclick="confirmDeleteLink()">Delete
|
<button onclick="confirmDeleteLink(event)">Delete
|
||||||
link</button>
|
link</button>
|
||||||
<button onclick="closeModal()">Cancel</button>
|
<button onclick="closeModal()">Cancel</button>
|
||||||
</div>
|
</div>
|
||||||
|
<span id="delete-link-message"></span>
|
||||||
</div>
|
</div>
|
||||||
@@ -15,7 +15,8 @@
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label for="linkIcon">Icon</label>
|
<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>
|
</div>
|
||||||
<button type="submit">Add
|
<button type="submit">Add
|
||||||
link</button>
|
link</button>
|
||||||
|
|||||||
@@ -40,8 +40,23 @@
|
|||||||
</div>
|
</div>
|
||||||
</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 -->
|
<!-- 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">
|
<template id="template-link-card">
|
||||||
|
<div data-card>
|
||||||
<div>
|
<div>
|
||||||
<img width="64" height="64" draggable="false" />
|
<img width="64" height="64" draggable="false" />
|
||||||
</div>
|
</div>
|
||||||
@@ -51,8 +66,10 @@
|
|||||||
<p></p>
|
<p></p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
<div id="template-category" class="hidden">
|
<template id="template-category">
|
||||||
|
<div>
|
||||||
<div class="category-header">
|
<div class="category-header">
|
||||||
<div>
|
<div>
|
||||||
<img width="32" height="32" draggable="false" />
|
<img width="32" height="32" draggable="false" />
|
||||||
@@ -71,9 +88,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
<div id="template-edit-actions" class="hidden">
|
<template id="template-edit-actions">
|
||||||
<div class="flex flex-row gap-2">
|
<div>
|
||||||
|
<div class="action-container">
|
||||||
<button class="action-button">
|
<button class="action-button">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24"
|
<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 -->
|
viewBox="0 0 24 24"><!-- Icon from Tabler Icons by Paweł Kuna - https://github.com/tabler/tabler-icons/blob/master/LICENSE -->
|
||||||
@@ -94,11 +113,12 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
<div id="teleport-storage" class="absolute -top-full -left-full hidden">
|
<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 -->
|
<!-- 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">
|
<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"
|
<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 -->
|
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"
|
<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
|
||||||
|
|||||||
@@ -39,13 +39,11 @@
|
|||||||
</span>
|
</span>
|
||||||
<div class="uptime-status">
|
<div class="uptime-status">
|
||||||
<svg viewBox="0 0 10 10">
|
<svg viewBox="0 0 10 10">
|
||||||
<use href="#status-dot"
|
<use href="#status-dot" class="{{#if this.Up}}text-success{{else}}text-error{{/if}}">
|
||||||
class="{{#if (eq this.Status 2)}}text-success{{else}}text-error{{/if}}">
|
|
||||||
</use>
|
</use>
|
||||||
</svg>
|
</svg>
|
||||||
<svg viewBox="0 0 10 10">
|
<svg viewBox="0 0 10 10">
|
||||||
<use href="#status-dot"
|
<use href="#status-dot" class="{{#if this.Up}}text-success{{else}}text-error{{/if}}">
|
||||||
class="{{#if (eq this.Status 2)}}text-success{{else}}text-error{{/if}}">
|
|
||||||
</use>
|
</use>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "passport",
|
"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.",
|
"description": "Passport is a simple, lightweight, and fast dashboard/new tab page for your browser.",
|
||||||
"author": "juls0730",
|
"author": "juls0730",
|
||||||
"license": "BSL-1.0",
|
"license": "BSL-1.0",
|
||||||
@@ -11,13 +11,15 @@
|
|||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"generate": "go generate ./src/",
|
"generate": "go generate ./src/",
|
||||||
|
|
||||||
"dev": "zqdgr generate; PASSPORT_DEV_MODE=true go run src/main.go",
|
"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": "zqdgr generate && go build -tags netgo,prod -ldflags=\"-w -s\" -o passport src/main.go && upx passport",
|
||||||
"build:amd64": "zqdgr generate && GOOS=linux GOARCH=amd64 go build -tags netgo,prod -ldflags=\"-w -s\" -o passport src/main.go",
|
"build:all": "zqdgr build && zqdgr build:arm64 && zqdgr build:amd64",
|
||||||
"build:arm64": "zqdgr generate && GOOS=linux GOARCH=arm64 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-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}",
|
"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"
|
"shutdown_signal": "SIGINT"
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user