mirror of
https://github.com/eduardolat/pgbackweb.git
synced 2026-02-18 11:28:27 -06:00
Run deno fmt
This commit is contained in:
@@ -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 project’s style guidelines.
|
||||
3. **Ensure all tests pass** and the code follows the project’s 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 project’s 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 project’s code of conduct when interacting with
|
||||
other contributors.
|
||||
|
||||
We appreciate your contributions and are excited to have you on board!
|
||||
|
||||
66
README.md
66
README.md
@@ -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
18
deno.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"fmt": {
|
||||
"exclude": [
|
||||
"./internal/view/static/libs/**/*"
|
||||
]
|
||||
},
|
||||
"lint": {
|
||||
"exclude": [
|
||||
"./internal/view/static/libs/**/*"
|
||||
],
|
||||
"rules": {
|
||||
"exclude": [
|
||||
"no-window",
|
||||
"no-window-prefix"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
[x-cloak] {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,4 +5,4 @@ html {
|
||||
|
||||
.table tbody tr {
|
||||
@apply hover:bg-base-200;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,4 +8,4 @@
|
||||
|
||||
.htmx-request.htmx-indicator {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
svg[data-nodxgo="lucide"]:not([class*="size-"]) {
|
||||
@apply size-4;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
.notyf__toast {
|
||||
@apply rounded-btn break-all !important;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,4 +20,4 @@
|
||||
* {
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,4 +58,4 @@
|
||||
|
||||
.ss-value-delete svg path {
|
||||
@apply stroke-primary-content !important;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
@tailwind variants;
|
||||
@tailwind variants;
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 });
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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 });
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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`;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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--;
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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();
|
||||
};
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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: {},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user