Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
7620577fa0
|
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,5 +1,4 @@
|
|||||||
passport
|
passport
|
||||||
passport-*
|
|
||||||
.env
|
.env
|
||||||
passport.db*
|
passport.db*
|
||||||
public
|
public
|
||||||
|
|||||||
16
Dockerfile
16
Dockerfile
@@ -5,6 +5,21 @@ 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
|
||||||
|
|
||||||
@@ -20,6 +35,7 @@ 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
|
||||||
|
|||||||
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.
|
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 | |
|
||||||
| `WEATHER_UPDATE_INTERVAL` | The interval in minutes to update the weather data | false | 15 |
|
| `WEATHER_UPDATE_INTERVAL` | The interval in minutes to update the weather data | false | 15 |
|
||||||
|
|
||||||
#### Uptime configuration
|
#### 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.
|
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 |
|
||||||
| ------------------------- | ------------------------------------------------- | -------- | ------- |
|
| ------------------------ | ------------------------------------------------- | -------- | ------- |
|
||||||
| `PASSPORT_UPTIME_API_KEY` | The UptimeRobot API key | true | |
|
| `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,7 +5,6 @@
|
|||||||
"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",
|
||||||
@@ -123,7 +122,7 @@
|
|||||||
|
|
||||||
"autoprefixer": ["autoprefixer@10.4.21", "", { "dependencies": { "browserslist": "^4.24.4", "caniuse-lite": "^1.0.30001702", "fraction.js": "^4.3.7", "normalize-range": "^0.1.2", "picocolors": "^1.1.1", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.1.0" }, "bin": { "autoprefixer": "bin/autoprefixer" } }, "sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ=="],
|
"autoprefixer": ["autoprefixer@10.4.21", "", { "dependencies": { "browserslist": "^4.24.4", "caniuse-lite": "^1.0.30001702", "fraction.js": "^4.3.7", "normalize-range": "^0.1.2", "picocolors": "^1.1.1", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.1.0" }, "bin": { "autoprefixer": "bin/autoprefixer" } }, "sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ=="],
|
||||||
|
|
||||||
"baseline-browser-mapping": ["baseline-browser-mapping@2.9.2", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-PxSsosKQjI38iXkmb3d0Y32efqyA0uW4s41u4IVBsLlWLhCiYNpH/AfNOVWRqCQBlD8TFJTz6OUWNd4DFJCnmw=="],
|
"baseline-browser-mapping": ["baseline-browser-mapping@2.8.10", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-uLfgBi+7IBNay8ECBO2mVMGZAc1VgZWEChxm4lv+TobGdG82LnXMjuNGo/BSSZZL4UmkWhxEHP2f5ziLNwGWMA=="],
|
||||||
|
|
||||||
"binary-extensions": ["binary-extensions@2.3.0", "", {}, "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw=="],
|
"binary-extensions": ["binary-extensions@2.3.0", "", {}, "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw=="],
|
||||||
|
|
||||||
@@ -479,8 +478,6 @@
|
|||||||
|
|
||||||
"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,7 +9,6 @@
|
|||||||
},
|
},
|
||||||
"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,15 +80,7 @@ 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
|
||||||
@@ -725,6 +717,10 @@ 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 {
|
||||||
@@ -1019,13 +1015,8 @@ 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)
|
||||||
status := fiber.StatusInternalServerError
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
||||||
if strings.Contains(err.Error(), "unsupported file type") {
|
"message": "Failed to upload file, please try again!",
|
||||||
status = fiber.StatusBadRequest
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.Status(status).JSON(fiber.Map{
|
|
||||||
"message": "Failed to upload file: " + err.Error(),
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1116,13 +1107,8 @@ 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)
|
||||||
status := fiber.StatusInternalServerError
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
||||||
if strings.Contains(err.Error(), "unsupported file type") {
|
"message": "Failed to upload file, please try again!",
|
||||||
status = fiber.StatusBadRequest
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.Status(status).JSON(fiber.Map{
|
|
||||||
"message": "Failed to upload file: " + err.Error(),
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1239,13 +1225,8 @@ 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)
|
||||||
status := fiber.StatusInternalServerError
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
||||||
if strings.Contains(err.Error(), "unsupported file type") {
|
"message": "Failed to upload file, please try again!",
|
||||||
status = fiber.StatusBadRequest
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.Status(status).JSON(fiber.Map{
|
|
||||||
"message": "Failed to upload file: " + err.Error(),
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,13 +11,38 @@ 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
|
||||||
@@ -51,7 +76,9 @@ function addErrorListener(form) {
|
|||||||
function cloneEditActions(primaryActions) {
|
function cloneEditActions(primaryActions) {
|
||||||
let editActions = document
|
let editActions = document
|
||||||
.getElementById("template-edit-actions")
|
.getElementById("template-edit-actions")
|
||||||
.content.firstElementChild.cloneNode(true);
|
.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++) {
|
||||||
@@ -67,46 +94,6 @@ 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")
|
||||||
@@ -114,77 +101,66 @@ document
|
|||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
let data = new FormData(event.target);
|
let data = new FormData(event.target);
|
||||||
|
|
||||||
const submitButton = event.target.querySelector("button");
|
let res = await fetch(`/api/category/${targetCategoryID}/link`, {
|
||||||
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) => {
|
|
||||||
let json = await res.json();
|
|
||||||
|
|
||||||
if (!res.ok) {
|
if (res.status === 201) {
|
||||||
throw new Error(json.message);
|
let json = await res.json();
|
||||||
}
|
|
||||||
|
|
||||||
let category = document.getElementById(
|
let category = document.getElementById(
|
||||||
`${targetCategoryID}_category`
|
`${targetCategoryID}_category`
|
||||||
);
|
);
|
||||||
let linkGrid = category.nextElementSibling;
|
let linkGrid = category.nextElementSibling;
|
||||||
|
|
||||||
let newLinkCard = document
|
let newLinkCard = document
|
||||||
.getElementById("template-link-card")
|
.getElementById("template-link-card")
|
||||||
.content.firstElementChild.cloneNode(true);
|
.cloneNode(true);
|
||||||
|
|
||||||
let newLinkImgElement = newLinkCard.querySelector(
|
newLinkCard.classList.remove("hidden");
|
||||||
"div:first-child img"
|
newLinkCard.classList.add("link-card", "admin", "relative");
|
||||||
);
|
|
||||||
|
|
||||||
newLinkImgElement.src = await processFile(data.get("icon"));
|
let newLinkImgElement = newLinkCard.querySelector(
|
||||||
newLinkImgElement.alt = data.get("name");
|
"div:first-child img"
|
||||||
|
);
|
||||||
|
|
||||||
newLinkCard.querySelector("h3").textContent = data.get("name");
|
newLinkImgElement.src = await processFile(data.get("icon"));
|
||||||
newLinkCard.querySelector("p").textContent =
|
newLinkImgElement.alt = data.get("name");
|
||||||
data.get("description");
|
|
||||||
|
|
||||||
newLinkCard.setAttribute("id", `${json.link.id}_link`);
|
newLinkCard.querySelector("h3").textContent = data.get("name");
|
||||||
|
newLinkCard.querySelector("p").textContent =
|
||||||
|
data.get("description");
|
||||||
|
|
||||||
let editActions = cloneEditActions([
|
newLinkCard.setAttribute("id", `${json.link.id}_link`);
|
||||||
{
|
|
||||||
clickAction: "editLink(this)",
|
|
||||||
label: "Edit link",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
clickAction: "deleteLink(this)",
|
|
||||||
label: "Delete link",
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
editActions.classList.add("absolute", "right-1", "top-1");
|
let editActions = cloneEditActions([
|
||||||
|
{
|
||||||
|
clickAction: "editLink(this)",
|
||||||
|
label: "Edit link",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
clickAction: "deleteLink(this)",
|
||||||
|
label: "Delete link",
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
newLinkCard.appendChild(editActions);
|
editActions.classList.add("absolute", "right-1", "top-1");
|
||||||
|
|
||||||
// append the card as the second to last element
|
newLinkCard.appendChild(editActions);
|
||||||
linkGrid.insertBefore(newLinkCard, linkGrid.lastElementChild);
|
|
||||||
closeModal("link");
|
|
||||||
|
|
||||||
// after the close animation plays
|
// append the card as the second to last element
|
||||||
setTimeout(() => {
|
linkGrid.insertBefore(newLinkCard, linkGrid.lastElementChild);
|
||||||
document.getElementById(`link-form`).reset();
|
closeModal("link");
|
||||||
document.getElementById(`link-message`).innerText = "";
|
|
||||||
}, 300);
|
// after the close animation plays
|
||||||
})
|
setTimeout(() => {
|
||||||
.catch((err) => {
|
document.getElementById(`link-form`).reset();
|
||||||
document.getElementById(`link-message`).innerText = err.message;
|
}, 300);
|
||||||
})
|
} else {
|
||||||
.finally(() => {
|
let json = await res.json();
|
||||||
submitButton.disabled = false;
|
document.getElementById(`link-message`).innerText = json.message;
|
||||||
submitButton.innerHTML = originalContents;
|
}
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
addErrorListener("category");
|
addErrorListener("category");
|
||||||
@@ -194,94 +170,74 @@ document
|
|||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
let data = new FormData(event.target);
|
let data = new FormData(event.target);
|
||||||
|
|
||||||
const submitButton = event.target.querySelector("button");
|
let res = await fetch(`/api/category`, {
|
||||||
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) => {
|
|
||||||
let json = await res.json();
|
|
||||||
|
|
||||||
if (!res.ok) {
|
if (res.status === 201) {
|
||||||
throw new Error(json.message);
|
let json = await res.json();
|
||||||
}
|
|
||||||
|
|
||||||
let newCategory = document
|
let newCategory = document
|
||||||
.getElementById("template-category")
|
.getElementById("template-category")
|
||||||
.content.firstElementChild.cloneNode(true);
|
.cloneNode(true);
|
||||||
|
|
||||||
let linkGrid = newCategory.querySelector("div:nth-child(2)");
|
let linkGrid = newCategory.querySelector("div:nth-child(2)");
|
||||||
let categoryHeader =
|
let categoryHeader = newCategory.querySelector(".category-header");
|
||||||
newCategory.querySelector(".category-header");
|
categoryHeader.setAttribute("id", `${json.category.id}_category`);
|
||||||
categoryHeader.setAttribute(
|
categoryHeader.querySelector("h2").textContent = json.category.name;
|
||||||
"id",
|
|
||||||
`${json.category.id}_category`
|
|
||||||
);
|
|
||||||
categoryHeader.querySelector("h2").textContent =
|
|
||||||
json.category.name;
|
|
||||||
|
|
||||||
let editActions = cloneEditActions([
|
let editActions = cloneEditActions([
|
||||||
{
|
{
|
||||||
clickAction: "editCategory(this)",
|
clickAction: "editCategory(this)",
|
||||||
label: "Edit category",
|
label: "Edit category",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
clickAction: "deleteCategory(this)",
|
clickAction: "deleteCategory(this)",
|
||||||
label: "Delete category",
|
label: "Delete category",
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
editActions.classList.add("pl-2", "flex-shrink-0");
|
editActions.classList.add("pl-2");
|
||||||
|
|
||||||
categoryHeader.appendChild(editActions);
|
categoryHeader.appendChild(editActions);
|
||||||
|
|
||||||
let categoryImg =
|
let categoryImg = categoryHeader.querySelector("div:first-child");
|
||||||
categoryHeader.querySelector("div:first-child");
|
|
||||||
|
|
||||||
categoryImg.querySelector("img").src = await processFile(
|
categoryImg.querySelector("img").src = await processFile(
|
||||||
data.get("icon")
|
data.get("icon")
|
||||||
|
);
|
||||||
|
|
||||||
|
linkGrid
|
||||||
|
.querySelector("div")
|
||||||
|
.setAttribute(
|
||||||
|
"onclick",
|
||||||
|
`openModal('link', ${json.category.id})`
|
||||||
);
|
);
|
||||||
|
|
||||||
linkGrid
|
let addCategoryButton = document.getElementById(
|
||||||
.querySelector("div")
|
"add-category-button"
|
||||||
.setAttribute(
|
);
|
||||||
"onclick",
|
addCategoryButton.parentElement.insertBefore(
|
||||||
`openModal('link', ${json.category.id})`
|
categoryHeader,
|
||||||
);
|
addCategoryButton
|
||||||
|
);
|
||||||
|
addCategoryButton.parentElement.insertBefore(
|
||||||
|
linkGrid,
|
||||||
|
addCategoryButton
|
||||||
|
);
|
||||||
|
|
||||||
let addCategoryButton = document.getElementById(
|
closeModal("category");
|
||||||
"add-category-button"
|
|
||||||
);
|
|
||||||
addCategoryButton.parentElement.insertBefore(
|
|
||||||
categoryHeader,
|
|
||||||
addCategoryButton
|
|
||||||
);
|
|
||||||
addCategoryButton.parentElement.insertBefore(
|
|
||||||
linkGrid,
|
|
||||||
addCategoryButton
|
|
||||||
);
|
|
||||||
|
|
||||||
closeModal("category");
|
// after the close animation plays
|
||||||
|
setTimeout(() => {
|
||||||
// after the close animation plays
|
document.getElementById(`category-form`).reset();
|
||||||
setTimeout(() => {
|
}, 300);
|
||||||
document.getElementById(`category-form`).reset();
|
} else {
|
||||||
document.getElementById(`category-message`).innerText = "";
|
let json = await res.json();
|
||||||
}, 300);
|
document.getElementById(`category-message`).innerText =
|
||||||
})
|
json.message;
|
||||||
.catch((err) => {
|
}
|
||||||
document.getElementById(`category-message`).innerText =
|
|
||||||
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
|
||||||
@@ -374,9 +330,6 @@ 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);
|
||||||
|
|
||||||
@@ -421,37 +374,22 @@ 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");
|
switch (currentlyEditing.type) {
|
||||||
|
case "link":
|
||||||
try {
|
confirmLinkEdit();
|
||||||
setConfirmLoading(confirmButton);
|
break;
|
||||||
|
case "category":
|
||||||
switch (currentlyEditing.type) {
|
confirmCategoryEdit();
|
||||||
case "link":
|
break;
|
||||||
await confirmLinkEdit();
|
default:
|
||||||
break;
|
console.error("Unknown currentlyEditing type");
|
||||||
case "category":
|
break;
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -512,7 +450,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/jpeg,image/png,image/webp,image/svg+xml";
|
iconUploadInput.accept = "image/*";
|
||||||
targetedImageElement = linkImg;
|
targetedImageElement = linkImg;
|
||||||
|
|
||||||
teleportElement(selectIconButton, linkImg.parentElement);
|
teleportElement(selectIconButton, linkImg.parentElement);
|
||||||
@@ -532,10 +470,6 @@ function editLink(target) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Confirms the edit of the link
|
|
||||||
* @param {Event} ev The event that triggered the function
|
|
||||||
*/
|
|
||||||
async function confirmLinkEdit() {
|
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");
|
||||||
@@ -570,19 +504,23 @@ async function confirmLinkEdit() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await fetch(
|
let res = 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(
|
||||||
@@ -618,7 +556,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("[data-card]");
|
let linkEl = target.closest(".link-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);
|
||||||
|
|
||||||
@@ -639,47 +577,21 @@ function deleteLink(target) {
|
|||||||
openModal("link-delete");
|
openModal("link-delete");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
async function confirmDeleteLink() {
|
||||||
* Confirms the deletion of the link
|
let res = await fetch(
|
||||||
* @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);
|
|
||||||
}
|
|
||||||
|
|
||||||
let linkEl = document.getElementById(
|
if (res.status === 200) {
|
||||||
`${currentlyEditing.linkID}_link`
|
let linkEl = document.getElementById(`${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;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -759,10 +671,12 @@ async function confirmCategoryEdit() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await fetch(`/api/category/${currentlyEditing.categoryID}`, {
|
let res = 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;
|
||||||
@@ -770,7 +684,9 @@ 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) {
|
||||||
@@ -823,42 +739,22 @@ function deleteCategory(target) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function confirmDeleteCategory() {
|
async function confirmDeleteCategory() {
|
||||||
const originalContents = ev.target.innerHTML;
|
let res = await fetch(`/api/category/${currentlyEditing.categoryID}`, {
|
||||||
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",
|
method: "DELETE",
|
||||||
})
|
});
|
||||||
.then(async (res) => {
|
|
||||||
if (!res.ok) {
|
|
||||||
let json = await res.json();
|
|
||||||
throw new Error(json.message);
|
|
||||||
}
|
|
||||||
|
|
||||||
let categoryEl = document.getElementById(
|
if (res.status === 200) {
|
||||||
`${currentlyEditing.categoryID}_category`
|
let categoryEl = document.getElementById(
|
||||||
);
|
`${currentlyEditing.categoryID}_category`
|
||||||
// get the next element and remove it (its the link grid)
|
);
|
||||||
let nextEl = categoryEl.nextElementSibling;
|
// get the next element and remove it (its the link grid)
|
||||||
nextEl.remove();
|
let nextEl = categoryEl.nextElementSibling;
|
||||||
categoryEl.remove();
|
nextEl.remove();
|
||||||
|
categoryEl.remove();
|
||||||
|
|
||||||
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) {
|
||||||
@@ -875,7 +771,6 @@ const stylesToCopy = [
|
|||||||
"letter-spacing",
|
"letter-spacing",
|
||||||
"text-transform",
|
"text-transform",
|
||||||
"text-align",
|
"text-align",
|
||||||
"text-wrap-style",
|
|
||||||
];
|
];
|
||||||
|
|
||||||
let _textMeasuringSpan,
|
let _textMeasuringSpan,
|
||||||
@@ -893,6 +788,8 @@ 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.
|
||||||
@@ -927,7 +824,7 @@ function replaceWithResizableTextarea(targetEls) {
|
|||||||
parseFloat(computedStyle.borderTopWidth) +
|
parseFloat(computedStyle.borderTopWidth) +
|
||||||
parseFloat(computedStyle.borderBottomWidth);
|
parseFloat(computedStyle.borderBottomWidth);
|
||||||
|
|
||||||
let maxWidth = parentBoundingRect.width;
|
let maxWidth = parentBoundingRect.width - borderWidth;
|
||||||
// take care of category headers specifically because the parent bounding box contains two other elements
|
// 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(
|
||||||
@@ -1060,6 +957,9 @@ 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;
|
||||||
@@ -1077,13 +977,14 @@ function replaceWithResizableTextarea(targetEls) {
|
|||||||
let imageWidth =
|
let imageWidth =
|
||||||
inputElement.previousElementSibling.getBoundingClientRect()
|
inputElement.previousElementSibling.getBoundingClientRect()
|
||||||
.width +
|
.width +
|
||||||
parseFloat(imageComputedStyle.marginLeft) +
|
imageComputedStyle.marginLeft +
|
||||||
parseFloat(imageComputedStyle.marginRight);
|
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;
|
maxWidth -= imageWidth + actionButtonWidth + caretBuffer;
|
||||||
|
maxWidth += currentInputBorderWidth;
|
||||||
}
|
}
|
||||||
|
|
||||||
let currentContentWidth;
|
let currentContentWidth;
|
||||||
@@ -1116,7 +1017,9 @@ function replaceWithResizableTextarea(targetEls) {
|
|||||||
_textMeasuringSpan.getBoundingClientRect().width;
|
_textMeasuringSpan.getBoundingClientRect().width;
|
||||||
|
|
||||||
currentContentWidth = Math.min(
|
currentContentWidth = Math.min(
|
||||||
roundToNearestHundredth(measuredTextWidth) + caretBuffer,
|
roundToNearestHundredth(
|
||||||
|
measuredTextWidth + currentInputBorderWidth
|
||||||
|
) + caretBuffer,
|
||||||
maxWidth
|
maxWidth
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ 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 {
|
||||||
@@ -105,10 +104,6 @@ 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,9 +82,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
& > 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);
|
||||||
@@ -107,9 +104,6 @@
|
|||||||
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);
|
||||||
@@ -126,14 +120,12 @@
|
|||||||
background-color: #0000;
|
background-color: #0000;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:not([disabled]) {
|
&:hover {
|
||||||
&:hover {
|
filter: brightness(125%);
|
||||||
filter: brightness(125%);
|
}
|
||||||
}
|
|
||||||
|
|
||||||
&:active {
|
&:active {
|
||||||
filter: brightness(95%);
|
filter: brightness(95%);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -200,14 +192,12 @@
|
|||||||
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%);
|
}
|
||||||
}
|
|
||||||
|
|
||||||
&:active {
|
&:active {
|
||||||
filter: brightness(95%);
|
filter: brightness(95%);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -224,11 +214,6 @@
|
|||||||
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,6 +111,7 @@
|
|||||||
|
|
||||||
.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;
|
||||||
}
|
}
|
||||||
@@ -145,13 +146,3 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@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>{{this.Name}}</h2>
|
<h2 class="capitalize break-all">{{this.Name}}</h2>
|
||||||
{{#if IsAdmin}}
|
{{#if IsAdmin}}
|
||||||
<div class="flex-shrink-0 pl-2">
|
<div>
|
||||||
<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,9 +4,8 @@
|
|||||||
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(event)">Delete
|
<button onclick="confirmDeleteCategory()">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,9 +4,8 @@
|
|||||||
you sure you
|
you sure you
|
||||||
want to continue?</p>
|
want to continue?</p>
|
||||||
<div>
|
<div>
|
||||||
<button onclick="confirmDeleteLink(event)">Delete
|
<button onclick="confirmDeleteLink()">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,8 +15,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label for="linkIcon">Icon</label>
|
<label for="linkIcon">Icon</label>
|
||||||
<input required type="file" name="icon" id="linkIcon"
|
<input required type="file" name="icon" id="linkIcon" accept="image/*" />
|
||||||
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,85 +40,65 @@
|
|||||||
</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 -->
|
||||||
<template id="template-link-card">
|
<div id="template-link-card" class="hidden">
|
||||||
<div data-card>
|
|
||||||
<div>
|
|
||||||
<img width="64" height="64" draggable="false" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h3></h3>
|
|
||||||
<!-- add 2 to the height to account for the border -->
|
|
||||||
<p></p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template id="template-category">
|
|
||||||
<div>
|
<div>
|
||||||
<div class="category-header">
|
<img width="64" height="64" draggable="false" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3></h3>
|
||||||
|
<!-- add 2 to the height to account for the border -->
|
||||||
|
<p></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="template-category" class="hidden">
|
||||||
|
<div class="category-header">
|
||||||
|
<div>
|
||||||
|
<img width="32" height="32" draggable="false" />
|
||||||
|
</div>
|
||||||
|
<h2></h2>
|
||||||
|
</div>
|
||||||
|
<div class="link-grid">
|
||||||
|
<div class="new-link-card link-card admin">
|
||||||
|
<svg class="mr-2" xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 24 24">
|
||||||
|
<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
|
||||||
|
stroke-width="2" d="M12 5v14m-7-7h14" />
|
||||||
|
</svg>
|
||||||
<div>
|
<div>
|
||||||
<img width="32" height="32" draggable="false" />
|
<h3>Add a link</h3>
|
||||||
</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>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</div>
|
||||||
|
|
||||||
<template id="template-edit-actions">
|
<div id="template-edit-actions" class="hidden">
|
||||||
<div>
|
<div class="flex flex-row gap-2">
|
||||||
<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 -->
|
<g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
|
||||||
<g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
|
stroke-width="2">
|
||||||
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="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" />
|
||||||
<path d="M20.385 6.585a2.1 2.1 0 0 0-2.97-2.97L9 12v3h3zM16 5l3 3" />
|
</g>
|
||||||
</g>
|
</svg>
|
||||||
</svg>
|
</button>
|
||||||
</button>
|
<button class="text-error action-button">
|
||||||
<button class="text-error 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 -->
|
<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
|
||||||
<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
|
stroke-width="2"
|
||||||
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" />
|
||||||
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>
|
||||||
</svg>
|
</button>
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</div>
|
||||||
|
|
||||||
<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(event)">
|
<button class="action-button text-success" onclick="confirmEdit()">
|
||||||
<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,11 +39,13 @@
|
|||||||
</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" class="{{#if this.Up}}text-success{{else}}text-error{{/if}}">
|
<use href="#status-dot"
|
||||||
|
class="{{#if (eq this.Status 2)}}text-success{{else}}text-error{{/if}}">
|
||||||
</use>
|
</use>
|
||||||
</svg>
|
</svg>
|
||||||
<svg viewBox="0 0 10 10">
|
<svg viewBox="0 0 10 10">
|
||||||
<use href="#status-dot" class="{{#if this.Up}}text-success{{else}}text-error{{/if}}">
|
<use href="#status-dot"
|
||||||
|
class="{{#if (eq this.Status 2)}}text-success{{else}}text-error{{/if}}">
|
||||||
</use>
|
</use>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "passport",
|
"name": "passport",
|
||||||
"version": "0.3.4",
|
"version": "0.3.3",
|
||||||
"description": "Passport is a simple, lightweight, and fast dashboard/new tab page for your browser.",
|
"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,15 +11,13 @@
|
|||||||
},
|
},
|
||||||
"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 && upx passport",
|
"build": "zqdgr generate && go build -tags netgo,prod -ldflags=\"-w -s\" -o passport src/main.go",
|
||||||
"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 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 src/main.go"
|
||||||
"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": [
|
"excluded_files": ["src/assets/styles"],
|
||||||
"src/assets/styles"
|
|
||||||
],
|
|
||||||
"shutdown_signal": "SIGINT"
|
"shutdown_signal": "SIGINT"
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user