Run deno fmt

This commit is contained in:
Luis Eduardo
2025-02-04 18:56:15 +00:00
parent c99d234040
commit f0a60bd482
31 changed files with 433 additions and 356 deletions

View File

@@ -1,45 +1,59 @@
## PG Back Web - Contribution Guidelines
Thank you for your interest in contributing to the PG Back Web project! Please follow these guidelines to ensure smooth collaboration and consistent code quality.
Thank you for your interest in contributing to the PG Back Web project! Please
follow these guidelines to ensure smooth collaboration and consistent code
quality.
## Open Source
This project is completely open source, so before making a contribution make sure you agree with the license in the [LICENSE](LICENSE) file and that all your contributions are under the same license.
This project is completely open source, so before making a contribution make
sure you agree with the license in the [LICENSE](LICENSE) file and that all your
contributions are under the same license.
This means that any contribution made to this project will be assumed to be made under the same license.
This means that any contribution made to this project will be assumed to be made
under the same license.
### Main Branch Policy
- The **main branch** reflects the latest **stable release** of the project.
- **No direct commits** should ever be made to the main branch.
- All development work should happen in feature branches and merged via **Pull Requests (PRs)** into the **develop** branch.
- The **develop branch** contains the latest code under active development. Once a new release is ready, the main branch will be updated by merging from the development branch.
- The **main branch** reflects the latest **stable release** of the project.
- **No direct commits** should ever be made to the main branch.
- All development work should happen in feature branches and merged via **Pull
Requests (PRs)** into the **develop** branch.
- The **develop branch** contains the latest code under active development. Once
a new release is ready, the main branch will be updated by merging from the
development branch.
### Development Workflow
1. **Fork the repository** and create a feature branch from the `develop` branch.
- Use descriptive names for your branches, e.g., `feature/add-new-endpoint` or `bugfix/fix-connection-issue`.
1. **Fork the repository** and create a feature branch from the `develop`
branch.
- Use descriptive names for your branches, e.g., `feature/add-new-endpoint`
or `bugfix/fix-connection-issue`.
2. **Make your changes** in your feature branch.
3. **Ensure all tests pass** and the code follows the projects style guidelines.
3. **Ensure all tests pass** and the code follows the projects style
guidelines.
4. **Open a Pull Request (PR)** against the `develop` branch.
5. Your PR will be reviewed by maintainers. Please be responsive to feedback.
6. Once approved, the changes will be merged into the `develop` branch. A merge into the `main` branch will only occur when releasing a new version.
6. Once approved, the changes will be merged into the `develop` branch. A merge
into the `main` branch will only occur when releasing a new version.
### Project Dependencies
This project requires the following dependencies installed on your system:
- **VSCode** - To enter into the devcontainer
- **Docker** For containerized environments.
- **Docker** For containerized environments.
- **Docker Compose** To manage multi-container setups.
### Development process
This project uses devcontainers to simplify the development, please refer to the following resources to learn more about devcontainers:
This project uses devcontainers to simplify the development, please refer to the
following resources to learn more about devcontainers:
- https://containers.dev
- https://code.visualstudio.com/docs/devcontainers/containers
@@ -54,8 +68,11 @@ This project uses devcontainers to simplify the development, please refer to the
### Additional Notes
- Always **write clear commit messages** that explain the purpose of your changes.
- **Keep your fork up to date** with the latest changes from the `develop` branch.
- Be respectful and follow the projects code of conduct when interacting with other contributors.
- Always **write clear commit messages** that explain the purpose of your
changes.
- **Keep your fork up to date** with the latest changes from the `develop`
branch.
- Be respectful and follow the projects code of conduct when interacting with
other contributors.
We appreciate your contributions and are excited to have you on board!

View File

@@ -30,7 +30,8 @@
## Why PG Back Web?
PG Back Web isn't just another backup tool. It's your trusted ally in ensuring the security and availability of your PostgreSQL data:
PG Back Web isn't just another backup tool. It's your trusted ally in ensuring
the security and availability of your PostgreSQL data:
- 🎯 **Designed for everyone**: From individual developers to teams.
- ⏱️ **Save time**: Automate your backups and forget about manual tasks.
@@ -38,23 +39,33 @@ PG Back Web isn't just another backup tool. It's your trusted ally in ensuring t
## Features
- 📦 **Intuitive web interface**: Manage your backups with ease, no database expertise required.
- 📅 **Scheduled backups**: Set it and forget it. PG Back Web takes care of the rest.
- 📈 **Backup monitoring**: Visualize the status of your backups with execution logs.
- 📤 **Instant download & restore**: Restore and download your backups when you need them, directly from the web interface.
- 📦 **Intuitive web interface**: Manage your backups with ease, no database
expertise required.
- 📅 **Scheduled backups**: Set it and forget it. PG Back Web takes care of the
rest.
- 📈 **Backup monitoring**: Visualize the status of your backups with execution
logs.
- 📤 **Instant download & restore**: Restore and download your backups when you
need them, directly from the web interface.
- 🖥 **Multi-version support**: Compatible with PostgreSQL 13, 14, 15, and 16.
- 📁 **Local & S3 storage**: Store backups locally or add as many S3 buckets as you want for greater flexibility.
- ❤️‍🩹 **Health checks**: Automatically check the health of your databases and destinations.
- 🔔 **Webhooks**: Get notified when a backup finishes, failed, health check fails, or other events.
- 📁 **Local & S3 storage**: Store backups locally or add as many S3 buckets as
you want for greater flexibility.
- ❤️‍🩹 **Health checks**: Automatically check the health of your databases and
destinations.
- 🔔 **Webhooks**: Get notified when a backup finishes, failed, health check
fails, or other events.
- 🔒 **Security first**: PGP encryption to protect your sensitive information.
- 🛡️ **Open-source trust**: Open-source code under MIT license, backed by the robust pg_dump tool.
- 🛡️ **Open-source trust**: Open-source code under MIT license, backed by the
robust pg_dump tool.
- 🌚 **Dark mode**: Because we all love dark mode.
## Installation
PG Back Web is available as a Docker image. You just need to set 3 environment variables and you're good to go!
PG Back Web is available as a Docker image. You just need to set 3 environment
variables and you're good to go!
Here's an example of how you can run PG Back Web with Docker Compose, feel free to adapt it to your needs:
Here's an example of how you can run PG Back Web with Docker Compose, feel free
to adapt it to your needs:
```yaml
services:
@@ -89,17 +100,23 @@ services:
retries: 5
```
You can watch [this youtube video](https://www.youtube.com/watch?v=vf7SLrSO8sw) to see how easy it is to set up PG Back Web.
You can watch [this youtube video](https://www.youtube.com/watch?v=vf7SLrSO8sw)
to see how easy it is to set up PG Back Web.
## Configuration
You only need to configure the following environment variables:
- `PBW_ENCRYPTION_KEY`: Your encryption key. Generate a strong one and store it in a safe place, as PG Back Web uses it to encrypt sensitive data.
- `PBW_ENCRYPTION_KEY`: Your encryption key. Generate a strong one and store it
in a safe place, as PG Back Web uses it to encrypt sensitive data.
- `PBW_POSTGRES_CONN_STRING`: The connection string for the PostgreSQL database that will store PG Back Web data.
- `PBW_POSTGRES_CONN_STRING`: The connection string for the PostgreSQL database
that will store PG Back Web data.
- `TZ`: Your [timezone](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones#List) (optional). Default is `UTC`. This impacts logging, backup filenames and default timezone in the web interface.
- `TZ`: Your
[timezone](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones#List)
(optional). Default is `UTC`. This impacts logging, backup filenames and
default timezone in the web interface.
## Screenshot
@@ -107,17 +124,20 @@ You only need to configure the following environment variables:
## Reset password
You can reset your PG Back Web password by running the following command in the server where PG Back Web is running:
You can reset your PG Back Web password by running the following command in the
server where PG Back Web is running:
```bash
docker exec -it <container_name_or_id> sh -c change-password
```
You should replace `<container_name_or_id>` with the name or ID of the PG Back Web container, then just follow the instructions.
You should replace `<container_name_or_id>` with the name or ID of the PG Back
Web container, then just follow the instructions.
## Next steps
In this link you can see a list of features that have been confirmed for future updates:
In this link you can see a list of features that have been confirmed for future
updates:
<a href="https://github.com/eduardolat/pgbackweb/issues?q=is%3Aissue+is%3Aopen+label%3A%22confirmed+next+step%22">
Next steps ⏭️
@@ -125,12 +145,16 @@ In this link you can see a list of features that have been confirmed for future
## Join the Community
Got ideas to improve PG Back Web? Contribute to the project! Every suggestion and pull request is welcome.
Got ideas to improve PG Back Web? Contribute to the project! Every suggestion
and pull request is welcome.
## License
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file
for details.
---
💖 **Love PG Back Web?** Give us a ⭐ on GitHub and share the project with your colleagues. Together, we can make PostgreSQL backups more accessible to everyone!
💖 **Love PG Back Web?** Give us a ⭐ on GitHub and share the project with your
colleagues. Together, we can make PostgreSQL backups more accessible to
everyone!

18
deno.json Normal file
View File

@@ -0,0 +1,18 @@
{
"fmt": {
"exclude": [
"./internal/view/static/libs/**/*"
]
},
"lint": {
"exclude": [
"./internal/view/static/libs/**/*"
],
"rules": {
"exclude": [
"no-window",
"no-window-prefix"
]
}
}
}

View File

@@ -2,10 +2,10 @@
This package includes a custom logger for the project.
Functions are exposed for logging messages easily; it is imperative that
these functions are used throughout the project to maintain a standard in
log messages.
Functions are exposed for logging messages easily; it is imperative that these
functions are used throughout the project to maintain a standard in log
messages.
If necessary, the `logWriter` can be edited so that in addition to writing to
stdout, it can also write to a file or send the logs to a server like
Better Stack or New Relic.
stdout, it can also write to a file or send the logs to a server like Better
Stack or New Relic.

View File

@@ -1,19 +1,24 @@
# paginateutil
This package provides a utility for paginating data. It should be used in conjunction with database queries to paginate results.
This package provides a utility for paginating data. It should be used in
conjunction with database queries to paginate results.
Use these utilities to paginate results and, above all, to return a common structure in the different places where pagination is performed, maintaining consistency throughout the project.
Use these utilities to paginate results and, above all, to return a common
structure in the different places where pagination is performed, maintaining
consistency throughout the project.
## Usage
### 1. Define your queries:
- **PaginateCount:** This function should return the total number of records that match the query.
- **PaginateCount:** This function should return the total number of records
that match the query.
- **Paginate:** This function should return the paginated records.
### 2. Create the offset from the request parameters:
Use the `CreateOffsetFromParams` function to create the offset from the request parameters needed to paginate the results.
Use the `CreateOffsetFromParams` function to create the offset from the request
parameters needed to paginate the results.
### 3. Define the signature of your pagination function:
@@ -38,9 +43,13 @@ func PaginateXYZ(
### Example
Refer to `internal/service/backups/paginate_backups.go` for an example of how to use the `paginateutil` package.
Refer to `internal/service/backups/paginate_backups.go` for an example of how to
use the `paginateutil` package.
## Notes
- **Default Values:** Ensure to set reasonable default values for pagination parameters (`page` and `limit`) if they are not provided.
- **Common Response Structure:** Use `CreatePaginateResponse` to generate a common response structure that includes information about pagination, such as the total number of items, the current page, and the number of items per page.
- **Default Values:** Ensure to set reasonable default values for pagination
parameters (`page` and `limit`) if they are not provided.
- **Common Response Structure:** Use `CreatePaginateResponse` to generate a
common response structure that includes information about pagination, such as
the total number of items, the current page, and the number of items per page.

View File

@@ -1,5 +1,6 @@
# Validate functions
All the functions in this directory are used to validate data, all of them _MUST BE PURE FUNCTIONS_.
All the functions in this directory are used to validate data, all of them _MUST
BE PURE FUNCTIONS_.
https://en.wikipedia.org/wiki/Pure_function

View File

@@ -1,9 +1,15 @@
# reqctx
`reqctx` is a utility package designed to manage request-specific context values in this project. It helps to encapsulate authentication status and user information within the Echo request context in a type-safe manner.
`reqctx` is a utility package designed to manage request-specific context values
in this project. It helps to encapsulate authentication status and user
information within the Echo request context in a type-safe manner.
## Purpose
When developing web applications, it is common to pass values such as authentication status and user information through the request lifecycle. Using Echo's built-in context (`echo.Context`) can lead to potential issues such as typographical errors, lack of type safety, and reduced code readability.
When developing web applications, it is common to pass values such as
authentication status and user information through the request lifecycle. Using
Echo's built-in context (`echo.Context`) can lead to potential issues such as
typographical errors, lack of type safety, and reduced code readability.
`reqctx` addresses these issues by providing a structured way to manage these context values.
`reqctx` addresses these issues by providing a structured way to manage these
context values.

View File

@@ -1,3 +1,3 @@
[x-cloak] {
display: none !important;
}
}

View File

@@ -5,4 +5,4 @@ html {
.table tbody tr {
@apply hover:bg-base-200;
}
}

View File

@@ -8,4 +8,4 @@
.htmx-request.htmx-indicator {
display: block;
}
}

View File

@@ -1,3 +1,3 @@
svg[data-nodxgo="lucide"]:not([class*="size-"]) {
@apply size-4;
}
}

View File

@@ -1,3 +1,3 @@
.notyf__toast {
@apply rounded-btn break-all !important;
}
}

View File

@@ -20,4 +20,4 @@
* {
scrollbar-width: thin;
}
}
}

View File

@@ -58,4 +58,4 @@
.ss-value-delete svg path {
@apply stroke-primary-content !important;
}
}

View File

@@ -6,4 +6,4 @@ body.swal2-shown:not(.swal2-no-backdrop):not(.swal2-toast-shown),
html.swal2-shown:not(.swal2-no-backdrop):not(.swal2-toast-shown) {
height: 100% !important;
overflow-y: visible !important;
}
}

View File

@@ -1,4 +1,4 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@tailwind variants;
@tailwind variants;

View File

@@ -6,4 +6,4 @@
@import "./partials/slim-select.css";
@import "./partials/notyf.css";
@import "./partials/scrollbar.css";
@import "./partials/sweetalert2.css";
@import "./partials/sweetalert2.css";

View File

@@ -1,11 +1,11 @@
import { initThemeHelper } from './init-theme-helper.js'
import { initSweetAlert2 } from './init-sweetalert2.js'
import { initNotyf } from './init-notyf.js'
import { initHTMX } from './init-htmx.js'
import { initHelpers } from './init-helpers.js'
import { initThemeHelper } from "./init-theme-helper.js";
import { initSweetAlert2 } from "./init-sweetalert2.js";
import { initNotyf } from "./init-notyf.js";
import { initHTMX } from "./init-htmx.js";
import { initHelpers } from "./init-helpers.js";
initThemeHelper()
initSweetAlert2()
initNotyf()
initHTMX()
initHelpers()
initThemeHelper();
initSweetAlert2();
initNotyf();
initHTMX();
initHelpers();

View File

@@ -1,75 +1,75 @@
export function initHelpers () {
function debounce (fn, delayMilliseconds) {
let timeoutInstance
export function initHelpers() {
function debounce(fn, delayMilliseconds) {
let timeoutInstance;
return function (...args) {
clearTimeout(timeoutInstance)
clearTimeout(timeoutInstance);
timeoutInstance = setTimeout(() => {
fn.apply(this, args)
}, delayMilliseconds)
}
fn.apply(this, args);
}, delayMilliseconds);
};
}
function copyToClipboard (textToCopy) {
const successMessage = 'Text copied'
const errorMessage = 'Error copying text'
function copyToClipboard(textToCopy) {
const successMessage = "Text copied";
const errorMessage = "Error copying text";
/* First, try the modern approach */
if (navigator.clipboard && window.isSecureContext) {
return navigator.clipboard
.writeText(textToCopy)
.then(() => {
toaster.success(successMessage)
toaster.success(successMessage);
})
.catch((err) => {
console.error(errorMessage, err)
toaster.error(errorMessage)
})
console.error(errorMessage, err);
toaster.error(errorMessage);
});
}
/* Fallback: use execCommand("copy") method */
const textArea = document.createElement('textarea')
textArea.value = textToCopy
const textArea = document.createElement("textarea");
textArea.value = textToCopy;
textArea.style.position = 'fixed'
textArea.style.left = '-9999px'
textArea.style.top = '-9999px'
document.body.appendChild(textArea)
textArea.focus()
textArea.select()
textArea.style.position = "fixed";
textArea.style.left = "-9999px";
textArea.style.top = "-9999px";
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
try {
const successful = document.execCommand('copy')
const successful = document.execCommand("copy");
if (successful) {
toaster.success(successMessage)
toaster.success(successMessage);
} else {
console.error('Fallback', errorMessage)
toaster.error(errorMessage)
console.error("Fallback", errorMessage);
toaster.error(errorMessage);
}
} catch (err) {
console.error('Fallback', errorMessage, err)
toaster.error(errorMessage)
console.error("Fallback", errorMessage, err);
toaster.error(errorMessage);
}
document.body.removeChild(textArea)
document.body.removeChild(textArea);
}
function textareaAutoGrow (element) {
if (!element?.style) return
element.style.height = '1px'
element.style.height = (element.scrollHeight + 2) + 'px'
function textareaAutoGrow(element) {
if (!element?.style) return;
element.style.height = "1px";
element.style.height = (element.scrollHeight + 2) + "px";
}
function formatJson (inJSON) {
if (typeof inJSON !== 'string' || inJSON === '') return inJSON
function formatJson(inJSON) {
if (typeof inJSON !== "string" || inJSON === "") return inJSON;
try {
return JSON.stringify(JSON.parse(inJSON), null, 2)
} catch (e) {
return inJSON
return JSON.stringify(JSON.parse(inJSON), null, 2);
} catch {
return inJSON;
}
}
window.debounce = debounce
window.copyToClipboard = copyToClipboard
window.textareaAutoGrow = textareaAutoGrow
window.formatJson = formatJson
window.debounce = debounce;
window.copyToClipboard = copyToClipboard;
window.textareaAutoGrow = textareaAutoGrow;
window.formatJson = formatJson;
}

View File

@@ -1,72 +1,72 @@
export function initHTMX () {
export function initHTMX() {
const triggers = {
ctm_alert: function (evt) {
const message = decodeURIComponent(evt.detail.value)
window.swalAlert(message)
const message = decodeURIComponent(evt.detail.value);
window.swalAlert(message);
},
ctm_alert_with_refresh: function (evt) {
const message = decodeURIComponent(evt.detail.value)
const message = decodeURIComponent(evt.detail.value);
window.swalAlert(message).then(() => {
location.reload()
})
location.reload();
});
},
ctm_alert_with_redirect: function (evt) {
const payload = decodeURIComponent(evt.detail.value)
const parts = payload.split('-::-::-')
const payload = decodeURIComponent(evt.detail.value);
const parts = payload.split("-::-::-");
if (parts.length !== 2) {
return
return;
}
const message = parts[0]
const url = parts[1]
const message = parts[0];
const url = parts[1];
window.swalAlert(message).then(() => {
location.href = url
})
location.href = url;
});
},
ctm_toast_success: function (evt) {
const message = decodeURIComponent(evt.detail.value)
toaster.success(message)
const message = decodeURIComponent(evt.detail.value);
toaster.success(message);
},
ctm_toast_error: function (evt) {
const message = decodeURIComponent(evt.detail.value)
toaster.error(message)
const message = decodeURIComponent(evt.detail.value);
toaster.error(message);
},
ctm_toast_success_infinite: function (evt) {
const message = decodeURIComponent(evt.detail.value)
toaster.successInfinite(message)
const message = decodeURIComponent(evt.detail.value);
toaster.successInfinite(message);
},
ctm_toast_error_infinite: function (evt) {
const message = decodeURIComponent(evt.detail.value)
toaster.errorInfinite(message)
}
}
const message = decodeURIComponent(evt.detail.value);
toaster.errorInfinite(message);
},
};
for (const key in triggers) {
document.addEventListener(key, triggers[key])
document.addEventListener(key, triggers[key]);
}
// Add trigger to use sweetalert2 for confirms
document.addEventListener('htmx:confirm', function (e) {
if (!e.detail.target.hasAttribute('hx-confirm')) return
document.addEventListener("htmx:confirm", function (e) {
if (!e.detail.target.hasAttribute("hx-confirm")) return;
e.preventDefault()
e.preventDefault();
window.swalConfirm(e.detail.question).then(function (result) {
if (result.isConfirmed) e.detail.issueRequest(true)
})
})
if (result.isConfirmed) e.detail.issueRequest(true);
});
});
// This fixes this issue:
// https://stackoverflow.com/questions/73658449/htmx-request-not-firing-when-hx-attributes-are-added-dynamically-from-javascrip
document.addEventListener('DOMContentLoaded', function () {
document.addEventListener("DOMContentLoaded", function () {
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
mutation.addedNodes.forEach((node) => {
if (node.nodeType === 1 && !node['htmx-internal-data']) {
htmx.process(node)
if (node.nodeType === 1 && !node["htmx-internal-data"]) {
htmx.process(node);
}
})
})
})
observer.observe(document, { childList: true, subtree: true })
})
});
});
});
observer.observe(document, { childList: true, subtree: true });
});
}

View File

@@ -1,53 +1,53 @@
export function initNotyf () {
let toastQueue = []
export function initNotyf() {
let toastQueue = [];
const toastCfg = {
duration: 5000,
ripple: false,
position: { x: 'right', y: 'bottom' },
dismissible: true
}
position: { x: "right", y: "bottom" },
dismissible: true,
};
const infiniteToastCfg = {
...toastCfg,
duration: 0
}
duration: 0,
};
window.toaster = {
success: (message) => {
toastQueue.push({ type: 'success', message, config: toastCfg })
toastQueue.push({ type: "success", message, config: toastCfg });
},
error: (message) => {
toastQueue.push({ type: 'error', message, config: toastCfg })
toastQueue.push({ type: "error", message, config: toastCfg });
},
successInfinite: (message) => {
toastQueue.push({ type: 'success', message, config: infiniteToastCfg })
toastQueue.push({ type: "success", message, config: infiniteToastCfg });
},
errorInfinite: (message) => {
toastQueue.push({ type: 'error', message, config: infiniteToastCfg })
}
}
toastQueue.push({ type: "error", message, config: infiniteToastCfg });
},
};
document.addEventListener('DOMContentLoaded', function () {
const notyf = new Notyf()
document.addEventListener("DOMContentLoaded", function () {
const notyf = new Notyf();
toastQueue.forEach(item => {
notyf.open({ type: item.type, message: item.message, ...item.config })
})
toastQueue.forEach((item) => {
notyf.open({ type: item.type, message: item.message, ...item.config });
});
toastQueue = []
toastQueue = [];
window.toaster.success = (message) => {
notyf.open({ type: 'success', message, ...toastCfg })
}
notyf.open({ type: "success", message, ...toastCfg });
};
window.toaster.error = (message) => {
notyf.open({ type: 'error', message, ...toastCfg })
}
notyf.open({ type: "error", message, ...toastCfg });
};
window.toaster.successInfinite = (message) => {
notyf.open({ type: 'success', message, ...infiniteToastCfg })
}
notyf.open({ type: "success", message, ...infiniteToastCfg });
};
window.toaster.errorInfinite = (message) => {
notyf.open({ type: 'error', message, ...infiniteToastCfg })
}
})
notyf.open({ type: "error", message, ...infiniteToastCfg });
};
});
}

View File

@@ -1,34 +1,34 @@
export function initSweetAlert2 () {
export function initSweetAlert2() {
// Docs at https://sweetalert2.github.io/#configuration
const defaultConfig = {
icon: 'info',
confirmButtonText: 'Okay',
cancelButtonText: 'Cancel',
icon: "info",
confirmButtonText: "Okay",
cancelButtonText: "Cancel",
customClass: {
popup: 'rounded-box bg-base-100 text-base-content',
confirmButton: 'btn btn-primary',
denyButton: 'btn btn-warning',
cancelButton: 'btn btn-error'
}
}
popup: "rounded-box bg-base-100 text-base-content",
confirmButton: "btn btn-primary",
denyButton: "btn btn-warning",
cancelButton: "btn btn-error",
},
};
async function swalAlert (text) {
return Swal.fire({
async function swalAlert(text) {
return await Swal.fire({
...defaultConfig,
title: text
})
}
async function swalConfirm (text) {
return Swal.fire({
...defaultConfig,
icon: 'question',
title: text,
confirmButtonText: 'Confirm',
showCancelButton: true
})
});
}
window.swalAlert = swalAlert
window.swalConfirm = swalConfirm
async function swalConfirm(text) {
return await Swal.fire({
...defaultConfig,
icon: "question",
title: text,
confirmButtonText: "Confirm",
showCancelButton: true,
});
}
window.swalAlert = swalAlert;
window.swalConfirm = swalConfirm;
}

View File

@@ -1,17 +1,17 @@
export function initThemeHelper () {
function getTheme () {
const theme = localStorage.getItem('theme')
return theme || ''
export function initThemeHelper() {
function getTheme() {
const theme = localStorage.getItem("theme");
return theme || "";
}
function setTheme (theme) {
localStorage.setItem('theme', theme)
document.documentElement.setAttribute('data-theme', theme)
function setTheme(theme) {
localStorage.setItem("theme", theme);
document.documentElement.setAttribute("data-theme", theme);
}
window.getTheme = getTheme
window.setTheme = setTheme
window.getTheme = getTheme;
window.setTheme = setTheme;
const theme = getTheme()
setTheme(theme)
const theme = getTheme();
setTheme(theme);
}

View File

@@ -1,19 +1,19 @@
window.alpineChangeThemeButton = function () {
return {
theme: '',
theme: "",
loadTheme () {
const theme = window.getTheme()
this.theme = theme || 'system'
loadTheme() {
const theme = window.getTheme();
this.theme = theme || "system";
},
setTheme (theme) {
window.setTheme(theme)
this.theme = theme || 'system'
setTheme(theme) {
window.setTheme(theme);
this.theme = theme || "system";
},
init () {
this.loadTheme()
}
}
}
init() {
this.loadTheme();
},
};
};

View File

@@ -5,42 +5,43 @@ window.alpineOptionsDropdown = function () {
contentEl: null,
closeTimeout: null,
init () {
this.buttonEl = this.$refs.button
this.contentEl = this.$refs.content
init() {
this.buttonEl = this.$refs.button;
this.contentEl = this.$refs.content;
},
open () {
this.isOpen = true
this.contentEl.classList.remove('hidden')
this.positionContent()
open() {
this.isOpen = true;
this.contentEl.classList.remove("hidden");
this.positionContent();
if (this.closeTimeout) {
clearTimeout(this.closeTimeout)
this.closeTimeout = null
clearTimeout(this.closeTimeout);
this.closeTimeout = null;
}
},
close () {
close() {
this.closeTimeout = setTimeout(() => {
this.isOpen = false
this.contentEl.classList.add('hidden')
}, 200)
this.isOpen = false;
this.contentEl.classList.add("hidden");
}, 200);
},
positionContent () {
const buttonRect = this.buttonEl.getBoundingClientRect()
const contentHeight = this.contentEl.offsetHeight
const windowHeight = window.innerHeight
const moreSpaceBelow = (windowHeight - buttonRect.bottom) > buttonRect.top
positionContent() {
const buttonRect = this.buttonEl.getBoundingClientRect();
const contentHeight = this.contentEl.offsetHeight;
const windowHeight = window.innerHeight;
const moreSpaceBelow =
(windowHeight - buttonRect.bottom) > buttonRect.top;
this.contentEl.style.left = `${buttonRect.left}px`
this.contentEl.style.left = `${buttonRect.left}px`;
if (moreSpaceBelow) {
this.contentEl.style.top = `${buttonRect.bottom}px`
this.contentEl.style.top = `${buttonRect.bottom}px`;
} else {
this.contentEl.style.top = `${buttonRect.top - contentHeight}px`
this.contentEl.style.top = `${buttonRect.top - contentHeight}px`;
}
}
}
}
},
};
};

View File

@@ -2,41 +2,41 @@ window.alpineStarOnGithub = function () {
return {
stars: null,
async init () {
const stars = await this.getStars()
async init() {
const stars = await this.getStars();
if (stars !== null) {
this.stars = stars
this.stars = stars;
}
},
async getStars () {
const cacheKey = 'pbw-gh-stars'
async getStars() {
const cacheKey = "pbw-gh-stars";
const cachedJSON = localStorage.getItem(cacheKey)
const cachedJSON = localStorage.getItem(cacheKey);
if (cachedJSON) {
const cached = JSON.parse(cachedJSON)
const cached = JSON.parse(cachedJSON);
if (Date.now() - cached.timestamp < 2 * 60 * 1000) {
return cached.value
return cached.value;
}
}
const url = 'https://api.github.com/repos/eduardolat/pgbackweb'
const url = "https://api.github.com/repos/eduardolat/pgbackweb";
try {
const response = await fetch(url)
const response = await fetch(url);
if (!response.ok) {
return null
return null;
}
const data = await response.json()
const value = data.stargazers_count
const data = await response.json();
const value = data.stargazers_count;
const dataToCache = JSON.stringify({
value,
timestamp: Date.now()
})
localStorage.setItem(cacheKey, dataToCache)
return value
timestamp: Date.now(),
});
localStorage.setItem(cacheKey, dataToCache);
return value;
} catch {
return null
return null;
}
}
}
}
},
};
};

View File

@@ -3,20 +3,20 @@ window.alpineSummaryHowToSlider = function () {
slidesQty: 4,
currentSlide: 1,
get hasNextSlide () {
return this.currentSlide < this.slidesQty
get hasNextSlide() {
return this.currentSlide < this.slidesQty;
},
get hasPrevSlide () {
return this.currentSlide > 1
get hasPrevSlide() {
return this.currentSlide > 1;
},
nextSlide () {
if (this.hasNextSlide) this.currentSlide++
nextSlide() {
if (this.hasNextSlide) this.currentSlide++;
},
prevSlide () {
if (this.hasPrevSlide) this.currentSlide--
}
}
}
prevSlide() {
if (this.hasPrevSlide) this.currentSlide--;
},
};
};

View File

@@ -1,53 +1,53 @@
document.addEventListener('DOMContentLoaded', function () {
const el = document.getElementById('dashboard-aside')
const key = 'dashboard-aside-scroll-position'
document.addEventListener("DOMContentLoaded", function () {
const el = document.getElementById("dashboard-aside");
const key = "dashboard-aside-scroll-position";
if (!el) return
if (!el) return;
const saveScrollPosition = window.debounce(
() => {
const scrollPosition = el.scrollTop
localStorage.setItem(key, scrollPosition)
const scrollPosition = el.scrollTop;
localStorage.setItem(key, scrollPosition);
},
200
)
el.addEventListener('scroll', saveScrollPosition)
200,
);
el.addEventListener("scroll", saveScrollPosition);
const scrollPosition = localStorage.getItem(key)
const scrollPosition = localStorage.getItem(key);
if (scrollPosition) {
el.scrollTop = parseInt(scrollPosition, 10)
el.scrollTop = parseInt(scrollPosition, 10);
}
})
});
window.alpineDashboardAsideItem = function (link = '', strict = false) {
window.alpineDashboardAsideItem = function (link = "", strict = false) {
return {
link,
strict,
is_active: false,
checkActive () {
checkActive() {
if (this.strict) {
this.is_active = window.location.pathname === this.link
return
this.is_active = window.location.pathname === this.link;
return;
}
this.is_active = window.location.pathname.startsWith(this.link)
this.is_active = window.location.pathname.startsWith(this.link);
},
init () {
this.checkActive()
init() {
this.checkActive();
const originalPushState = window.history.pushState
const originalPushState = window.history.pushState;
window.history.pushState = (...args) => {
originalPushState.apply(window.history, args)
this.checkActive()
}
originalPushState.apply(window.history, args);
this.checkActive();
};
const originalReplaceState = window.history.replaceState
const originalReplaceState = window.history.replaceState;
window.history.replaceState = (...args) => {
originalReplaceState.apply(window.history, args)
this.checkActive()
}
}
}
}
originalReplaceState.apply(window.history, args);
this.checkActive();
};
},
};
};

View File

@@ -2,41 +2,42 @@ window.alpineDashboardHeaderUpdates = function () {
return {
latestRelease: null,
async init () {
const latestRelease = await this.getLatestRelease()
async init() {
const latestRelease = await this.getLatestRelease();
if (latestRelease !== null) {
this.latestRelease = latestRelease
this.latestRelease = latestRelease;
}
},
async getLatestRelease () {
const cacheKey = 'pbw-gh-last-release'
async getLatestRelease() {
const cacheKey = "pbw-gh-last-release";
const cachedJSON = localStorage.getItem(cacheKey)
const cachedJSON = localStorage.getItem(cacheKey);
if (cachedJSON) {
const cached = JSON.parse(cachedJSON)
const cached = JSON.parse(cachedJSON);
if (Date.now() - cached.timestamp < 2 * 60 * 1000) {
return cached.value
return cached.value;
}
}
const url = 'https://api.github.com/repos/eduardolat/pgbackweb/releases/latest'
const url =
"https://api.github.com/repos/eduardolat/pgbackweb/releases/latest";
try {
const response = await fetch(url)
const response = await fetch(url);
if (!response.ok) {
return null
return null;
}
const data = await response.json()
const value = data.name
const data = await response.json();
const value = data.name;
const dataToCache = JSON.stringify({
value,
timestamp: Date.now()
})
localStorage.setItem(cacheKey, dataToCache)
return value
timestamp: Date.now(),
});
localStorage.setItem(cacheKey, dataToCache);
return value;
} catch {
return null
return null;
}
}
}
}
},
};
};

View File

@@ -1,29 +1,29 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ['./internal/view/web/**/*.go'],
plugins: [require('daisyui')],
content: ["./internal/view/web/**/*.go"],
plugins: [require("daisyui")],
daisyui: {
logs: false,
themes: [
{
light: {
...require('daisyui/src/theming/themes').light,
primary: '#2be7c8',
'success-content': '#ffffff',
'error-content': '#ffffff'
...require("daisyui/src/theming/themes").light,
primary: "#2be7c8",
"success-content": "#ffffff",
"error-content": "#ffffff",
},
dark: {
...require('daisyui/src/theming/themes').dracula,
primary: '#2be7c8'
}
}
...require("daisyui/src/theming/themes").dracula,
primary: "#2be7c8",
},
},
],
darkTheme: 'dark'
darkTheme: "dark",
},
theme: {
screens: {
desk: '768px' // only one breakpoint to keep it simple
desk: "768px", // only one breakpoint to keep it simple
},
extend: {}
}
}
extend: {},
},
};

View File

@@ -88,11 +88,11 @@ tasks:
silent: true
cmds:
- >
npm run tailwindcss --
--minify
--config ./tailwind.config.js
--input ./internal/view/static/css/style.css
--output ./internal/view/static/build/style.min.css
npm run tailwindcss --
--minify
--config ./tailwind.config.js
--input ./internal/view/static/css/style.css
--output ./internal/view/static/build/style.min.css
- ./scripts/build-js.mjs
tidy: