Merge branch 'develop' into feat-add-devcontainer

This commit is contained in:
Luis Eduardo
2025-02-02 04:35:38 +00:00
45 changed files with 865 additions and 449 deletions

View File

@@ -1,22 +0,0 @@
# Docs for .air.toml
# https://github.com/cosmtrek/air/blob/master/air_example.toml
root = "."
[build]
cmd = "task build"
full_bin = "task serve"
delay = 100
exclude_dir = ["tmp", "dist", "internal/database/dbgen"]
exclude_regex = [
"_test.go",
"_generated.go",
".sql.go",
".gen.go",
".min.js",
".min.css",
]
include_ext = ["go", "sql", "js", "css", "json"]
[log]
main_only = true

View File

@@ -1,11 +1,14 @@
name: Lint, test, and build
on:
workflow_dispatch:
pull_request:
push:
branches:
- develop
- main
- develop
pull_request:
branches:
- main
- develop
jobs:
lint-test-build:

View File

@@ -46,6 +46,7 @@ jobs:
docker buildx build \
--platform linux/amd64,linux/arm64 \
--build-arg TARGETPLATFORM \
$tag_args \
-f ./docker/Dockerfile \
--push .

1
.gitignore vendored
View File

@@ -13,6 +13,7 @@ dist/
# Temp files/folders
tmp/
temp/
.task/
# Binaries for programs and plugins

48
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,48 @@
## 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.
### 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.
### 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`.
2. **Make your changes** in your feature branch.
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.
### Project Dependencies
This project requires the following dependencies installed on your system:
- [**Taskfile**](https://taskfile.dev/) Used to automate development tasks.
- **Docker** For containerized environments.
- **Docker Compose** To manage multi-container setups.
### How to Use Taskfile Commands
- To see all available commands, run:
```bash
task --list
```
- The **primary command** for development is:
```bash
task on
```
This command should be run from your **host environment** to start a local development server.
### 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.
We appreciate your contributions and are excited to have you on board!

View File

@@ -1,11 +1,22 @@
# Declare the target platform to support multi-arch builds
ARG TARGETPLATFORM
# To make sure we have the node, and golang binaries
FROM node:20.17.0-bookworm AS node
FROM golang:1.23.1-bookworm AS golang
# Set the base image, general environment variables, and move to temp dir
# Set the base image
FROM debian:12.7
ENV DEBIAN_FRONTEND=noninteractive
ENV PATH="$PATH:/usr/local/go/bin"
# Re-declare ARG after FROM to make it available in this build stage
ARG TARGETPLATFORM
RUN echo "Building for ${TARGETPLATFORM}"
# Set the general environment variables, and move to temp dir
ENV DEBIAN_FRONTEND="noninteractive"
ENV GOBIN="/usr/local/go-bin"
ENV PATH="$PATH:/usr/local/go-bin:/usr/local/dl-bin:/usr/local/go/bin"
RUN mkdir -p /app/temp /usr/local/go-bin /usr/local/dl-bin
WORKDIR /app/temp
# Copy node binaries
@@ -24,56 +35,49 @@ RUN apt update && apt install -y postgresql-common && \
apt update && apt install -y \
wget unzip tzdata git \
postgresql-client-13 postgresql-client-14 \
postgresql-client-15 postgresql-client-16 && \
postgresql-client-15 postgresql-client-16 \
postgresql-client-17 && \
rm -rf /var/lib/apt/lists/*
# Install downloadable binaries
RUN set -e && \
if [ "${TARGETPLATFORM}" = "linux/arm64" ]; then \
echo "Downloading arm64 binaries" && \
# Install task
wget --no-verbose https://github.com/go-task/task/releases/download/v3.38.0/task_linux_arm64.tar.gz && \
tar -xzf task_linux_arm64.tar.gz && \
mv ./task /usr/local/bin/task && \
mv ./task /usr/local/dl-bin/task && \
# Install goose
wget --no-verbose https://github.com/pressly/goose/releases/download/v3.22.0/goose_linux_arm64 && \
mv ./goose_linux_arm64 /usr/local/bin/goose && \
mv ./goose_linux_arm64 /usr/local/dl-bin/goose && \
# Install sqlc
wget --no-verbose https://github.com/sqlc-dev/sqlc/releases/download/v1.27.0/sqlc_1.27.0_linux_arm64.tar.gz && \
tar -xzf sqlc_1.27.0_linux_arm64.tar.gz && \
mv ./sqlc /usr/local/bin/sqlc && \
mv ./sqlc /usr/local/dl-bin/sqlc && \
# Install golangci-lint
wget --no-verbose https://github.com/golangci/golangci-lint/releases/download/v1.60.3/golangci-lint-1.60.3-linux-arm64.tar.gz && \
tar -xzf golangci-lint-1.60.3-linux-arm64.tar.gz && \
mv ./golangci-lint-1.60.3-linux-arm64/golangci-lint /usr/local/bin/golangci-lint && \
# Install air
wget --no-verbose https://github.com/air-verse/air/releases/download/v1.52.3/air_1.52.3_linux_arm64 && \
mv ./air_1.52.3_linux_arm64 /usr/local/bin/air; \
mv ./golangci-lint-1.60.3-linux-arm64/golangci-lint /usr/local/dl-bin/golangci-lint; \
else \
echo "Downloading amd64 binaries" && \
# Install task
wget --no-verbose https://github.com/go-task/task/releases/download/v3.38.0/task_linux_amd64.tar.gz && \
tar -xzf task_linux_amd64.tar.gz && \
mv ./task /usr/local/bin/task && \
mv ./task /usr/local/dl-bin/task && \
# Install goose
wget --no-verbose https://github.com/pressly/goose/releases/download/v3.22.0/goose_linux_x86_64 && \
mv ./goose_linux_x86_64 /usr/local/bin/goose && \
mv ./goose_linux_x86_64 /usr/local/dl-bin/goose && \
# Install sqlc
wget --no-verbose https://github.com/sqlc-dev/sqlc/releases/download/v1.27.0/sqlc_1.27.0_linux_amd64.tar.gz && \
tar -xzf sqlc_1.27.0_linux_amd64.tar.gz && \
mv ./sqlc /usr/local/bin/sqlc && \
mv ./sqlc /usr/local/dl-bin/sqlc && \
# Install golangci-lint
wget --no-verbose https://github.com/golangci/golangci-lint/releases/download/v1.60.3/golangci-lint-1.60.3-linux-amd64.tar.gz && \
tar -xzf golangci-lint-1.60.3-linux-amd64.tar.gz && \
mv ./golangci-lint-1.60.3-linux-amd64/golangci-lint /usr/local/bin/golangci-lint && \
# Install air
wget --no-verbose https://github.com/air-verse/air/releases/download/v1.52.3/air_1.52.3_linux_amd64 && \
mv ./air_1.52.3_linux_amd64 /usr/local/bin/air; \
mv ./golangci-lint-1.60.3-linux-amd64/golangci-lint /usr/local/dl-bin/golangci-lint; \
fi && \
# Make binaries executable
chmod +x /usr/local/bin/task && \
chmod +x /usr/local/bin/goose && \
chmod +x /usr/local/bin/sqlc && \
chmod +x /usr/local/bin/golangci-lint && \
chmod +x /usr/local/bin/air
chmod +x /usr/local/dl-bin/*
# Go to the app dir, delete the temporary dir and create backups dir
WORKDIR /app
@@ -97,6 +101,9 @@ RUN go mod download
# Copy the rest of the files
COPY . .
# Fix permissions if needed
RUN task fixperms
# Build the app
RUN task build

View File

@@ -1,11 +1,22 @@
# Declare the target platform to support multi-arch builds
ARG TARGETPLATFORM
# To make sure we have the node, and golang binaries
FROM node:20.17.0-bookworm AS node
FROM golang:1.23.1-bookworm AS golang
# Set the base image, general environment variables, and move to temp dir
# Set the base image
FROM debian:12.7
ENV DEBIAN_FRONTEND=noninteractive
ENV PATH="$PATH:/usr/local/go/bin"
# Re-declare ARG after FROM to make it available in this build stage
ARG TARGETPLATFORM
RUN echo "Building for ${TARGETPLATFORM}"
# Set the general environment variables, and move to temp dir
ENV DEBIAN_FRONTEND="noninteractive"
ENV GOBIN="/usr/local/go-bin"
ENV PATH="$PATH:/usr/local/go-bin:/usr/local/dl-bin:/usr/local/go/bin"
RUN mkdir -p /app/temp /usr/local/go-bin /usr/local/dl-bin
WORKDIR /app/temp
# Copy node binaries
@@ -24,56 +35,49 @@ RUN apt update && apt install -y postgresql-common && \
apt update && apt install -y \
wget unzip tzdata git \
postgresql-client-13 postgresql-client-14 \
postgresql-client-15 postgresql-client-16 && \
postgresql-client-15 postgresql-client-16 \
postgresql-client-17 && \
rm -rf /var/lib/apt/lists/*
# Install downloadable binaries
RUN set -e && \
if [ "${TARGETPLATFORM}" = "linux/arm64" ]; then \
echo "Downloading arm64 binaries" && \
# Install task
wget --no-verbose https://github.com/go-task/task/releases/download/v3.38.0/task_linux_arm64.tar.gz && \
tar -xzf task_linux_arm64.tar.gz && \
mv ./task /usr/local/bin/task && \
mv ./task /usr/local/dl-bin/task && \
# Install goose
wget --no-verbose https://github.com/pressly/goose/releases/download/v3.22.0/goose_linux_arm64 && \
mv ./goose_linux_arm64 /usr/local/bin/goose && \
mv ./goose_linux_arm64 /usr/local/dl-bin/goose && \
# Install sqlc
wget --no-verbose https://github.com/sqlc-dev/sqlc/releases/download/v1.27.0/sqlc_1.27.0_linux_arm64.tar.gz && \
tar -xzf sqlc_1.27.0_linux_arm64.tar.gz && \
mv ./sqlc /usr/local/bin/sqlc && \
mv ./sqlc /usr/local/dl-bin/sqlc && \
# Install golangci-lint
wget --no-verbose https://github.com/golangci/golangci-lint/releases/download/v1.60.3/golangci-lint-1.60.3-linux-arm64.tar.gz && \
tar -xzf golangci-lint-1.60.3-linux-arm64.tar.gz && \
mv ./golangci-lint-1.60.3-linux-arm64/golangci-lint /usr/local/bin/golangci-lint && \
# Install air
wget --no-verbose https://github.com/air-verse/air/releases/download/v1.52.3/air_1.52.3_linux_arm64 && \
mv ./air_1.52.3_linux_arm64 /usr/local/bin/air; \
mv ./golangci-lint-1.60.3-linux-arm64/golangci-lint /usr/local/dl-bin/golangci-lint; \
else \
echo "Downloading amd64 binaries" && \
# Install task
wget --no-verbose https://github.com/go-task/task/releases/download/v3.38.0/task_linux_amd64.tar.gz && \
tar -xzf task_linux_amd64.tar.gz && \
mv ./task /usr/local/bin/task && \
mv ./task /usr/local/dl-bin/task && \
# Install goose
wget --no-verbose https://github.com/pressly/goose/releases/download/v3.22.0/goose_linux_x86_64 && \
mv ./goose_linux_x86_64 /usr/local/bin/goose && \
mv ./goose_linux_x86_64 /usr/local/dl-bin/goose && \
# Install sqlc
wget --no-verbose https://github.com/sqlc-dev/sqlc/releases/download/v1.27.0/sqlc_1.27.0_linux_amd64.tar.gz && \
tar -xzf sqlc_1.27.0_linux_amd64.tar.gz && \
mv ./sqlc /usr/local/bin/sqlc && \
mv ./sqlc /usr/local/dl-bin/sqlc && \
# Install golangci-lint
wget --no-verbose https://github.com/golangci/golangci-lint/releases/download/v1.60.3/golangci-lint-1.60.3-linux-amd64.tar.gz && \
tar -xzf golangci-lint-1.60.3-linux-amd64.tar.gz && \
mv ./golangci-lint-1.60.3-linux-amd64/golangci-lint /usr/local/bin/golangci-lint && \
# Install air
wget --no-verbose https://github.com/air-verse/air/releases/download/v1.52.3/air_1.52.3_linux_amd64 && \
mv ./air_1.52.3_linux_amd64 /usr/local/bin/air; \
mv ./golangci-lint-1.60.3-linux-amd64/golangci-lint /usr/local/dl-bin/golangci-lint; \
fi && \
# Make binaries executable
chmod +x /usr/local/bin/task && \
chmod +x /usr/local/bin/goose && \
chmod +x /usr/local/bin/sqlc && \
chmod +x /usr/local/bin/golangci-lint && \
chmod +x /usr/local/bin/air
chmod +x /usr/local/dl-bin/*
# Go to the app dir, delete the temporary dir and create backups dir
WORKDIR /app

View File

@@ -0,0 +1,15 @@
-- +goose Up
-- +goose StatementBegin
ALTER TABLE databases
DROP CONSTRAINT IF EXISTS databases_pg_version_check,
ADD CONSTRAINT databases_pg_version_check
CHECK (pg_version IN ('13', '14', '15', '16', '17'));
-- +goose StatementEnd
-- +goose Down
-- +goose StatementBegin
ALTER TABLE databases
DROP CONSTRAINT IF EXISTS databases_pg_version_check,
ADD CONSTRAINT databases_pg_version_check
CHECK (pg_version IN ('13', '14', '15', '16'));
-- +goose StatementEnd

View File

@@ -21,34 +21,41 @@ import (
*/
type version struct {
version string
pgDump string
psql string
Version string
PGDump string
PSQL string
}
type PGVersion enum.Member[version]
var (
PG13 = PGVersion{version{
version: "13",
pgDump: "/usr/lib/postgresql/13/bin/pg_dump",
psql: "/usr/lib/postgresql/13/bin/psql",
Version: "13",
PGDump: "/usr/lib/postgresql/13/bin/pg_dump",
PSQL: "/usr/lib/postgresql/13/bin/psql",
}}
PG14 = PGVersion{version{
version: "14",
pgDump: "/usr/lib/postgresql/14/bin/pg_dump",
psql: "/usr/lib/postgresql/14/bin/psql",
Version: "14",
PGDump: "/usr/lib/postgresql/14/bin/pg_dump",
PSQL: "/usr/lib/postgresql/14/bin/psql",
}}
PG15 = PGVersion{version{
version: "15",
pgDump: "/usr/lib/postgresql/15/bin/pg_dump",
psql: "/usr/lib/postgresql/15/bin/psql",
Version: "15",
PGDump: "/usr/lib/postgresql/15/bin/pg_dump",
PSQL: "/usr/lib/postgresql/15/bin/psql",
}}
PG16 = PGVersion{version{
version: "16",
pgDump: "/usr/lib/postgresql/16/bin/pg_dump",
psql: "/usr/lib/postgresql/16/bin/psql",
Version: "16",
PGDump: "/usr/lib/postgresql/16/bin/pg_dump",
PSQL: "/usr/lib/postgresql/16/bin/psql",
}}
PG17 = PGVersion{version{
Version: "17",
PGDump: "/usr/lib/postgresql/17/bin/pg_dump",
PSQL: "/usr/lib/postgresql/17/bin/psql",
}}
PGVersions = []PGVersion{PG13, PG14, PG15, PG16, PG17}
)
type Client struct{}
@@ -69,6 +76,8 @@ func (Client) ParseVersion(version string) (PGVersion, error) {
return PG15, nil
case "16":
return PG16, nil
case "17":
return PG17, nil
default:
return PGVersion{}, fmt.Errorf("pg version not allowed: %s", version)
}
@@ -76,12 +85,12 @@ func (Client) ParseVersion(version string) (PGVersion, error) {
// Test tests the connection to the PostgreSQL database
func (Client) Test(version PGVersion, connString string) error {
cmd := exec.Command(version.Value.psql, connString, "-c", "SELECT 1;")
cmd := exec.Command(version.Value.PSQL, connString, "-c", "SELECT 1;")
output, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf(
"error running psql test v%s: %s",
version.Value.version, output,
version.Value.Version, output,
)
}
@@ -152,7 +161,7 @@ func (Client) Dump(
errorBuffer := &bytes.Buffer{}
reader, writer := io.Pipe()
cmd := exec.Command(version.Value.pgDump, args...)
cmd := exec.Command(version.Value.PGDump, args...)
cmd.Stdout = writer
cmd.Stderr = errorBuffer
@@ -161,7 +170,7 @@ func (Client) Dump(
if err := cmd.Run(); err != nil {
writer.CloseWithError(fmt.Errorf(
"error running pg_dump v%s: %s",
version.Value.version, errorBuffer.String(),
version.Value.Version, errorBuffer.String(),
))
}
}()
@@ -248,12 +257,12 @@ func (Client) RestoreZip(
return fmt.Errorf("dump.sql file not found in ZIP file: %s", zipPath)
}
cmd = exec.Command(version.Value.psql, connString, "-f", dumpPath)
cmd = exec.Command(version.Value.PSQL, connString, "-f", dumpPath)
output, err = cmd.CombinedOutput()
if err != nil {
return fmt.Errorf(
"error running psql v%s command: %s",
version.Value.version, output,
version.Value.Version, output,
)
}

3
internal/view/static/.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
# The build directory stores the JavaScript and CSS files that are generated
# by the build process.
build/

View File

@@ -1 +0,0 @@
style.min.css

View File

@@ -1,4 +1,8 @@
html {
overflow-x: hidden;
overflow-y: auto;
}
.table tbody tr {
@apply hover:bg-base-200;
}

View File

@@ -0,0 +1,9 @@
/*
Fix sweetalert2 scroll issue
https://github.com/sweetalert2/sweetalert2/issues/781#issuecomment-475108658
*/
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

@@ -6,13 +6,4 @@
@import "./partials/slim-select.css";
@import "./partials/notyf.css";
@import "./partials/scrollbar.css";
/*
Fix sweetalert2 scroll issue
https://github.com/sweetalert2/sweetalert2/issues/781#issuecomment-475108658
*/
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;
}
@import "./partials/sweetalert2.css";

View File

@@ -1 +0,0 @@
app.min.js

View File

@@ -1,77 +0,0 @@
export const githubRepoInfo = {
name: 'githubRepoInfo',
fn: () => ({
stars: '',
latestRelease: '',
async init () {
const stars = await this.getStars()
if (stars !== null) {
this.stars = stars
}
const latestRelease = await this.getLatestRelease()
if (latestRelease !== null) {
this.latestRelease = latestRelease
}
},
async getStars () {
const cacheKey = 'pbw_gh_stars'
const cachedData = this.getCachedData(cacheKey)
if (cachedData !== null) {
return cachedData
}
const url = 'https://api.github.com/repos/eduardolat/pgbackweb'
try {
const response = await fetch(url)
if (!response.ok) {
return null
}
const data = await response.json()
this.cacheData(cacheKey, data.stargazers_count)
return data.stargazers_count
} catch {
return null
}
},
async getLatestRelease () {
const cacheKey = 'pbw_gh_last_release'
const cachedData = this.getCachedData(cacheKey)
if (cachedData !== null) {
return cachedData
}
const url = 'https://api.github.com/repos/eduardolat/pgbackweb/releases/latest'
try {
const response = await fetch(url)
if (!response.ok) {
return null
}
const data = await response.json()
this.cacheData(cacheKey, data.name)
return data.name
} catch {
return null
}
},
getCachedData (key) {
const cachedJSON = localStorage.getItem(key)
if (!cachedJSON) {
return null
}
const cached = JSON.parse(cachedJSON)
if (Date.now() - cached.timestamp < 2 * 60 * 1000) {
return cached.value
}
return null
},
cacheData (key, value) {
const data = JSON.stringify({
value,
timestamp: Date.now()
})
localStorage.setItem(key, data)
}
})
}

View File

@@ -2,14 +2,10 @@ 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 { initAlpineComponents } from './init-alpine-components.js'
import { initHelpers } from './init-helpers.js'
import { initDashboardAsideScroll } from './dashboard-aside-scroll.js'
initThemeHelper()
initSweetAlert2()
initNotyf()
initHTMX()
initAlpineComponents()
initHelpers()
initDashboardAsideScroll()

View File

@@ -1,22 +0,0 @@
export function initDashboardAsideScroll () {
document.addEventListener('DOMContentLoaded', function () {
const el = document.getElementById('dashboard-aside')
const key = 'dashboard-aside-scroll-position'
if (!el) return
const saveScrollPosition = window.debounce(
() => {
const scrollPosition = el.scrollTop
localStorage.setItem(key, scrollPosition)
},
200
)
el.addEventListener('scroll', saveScrollPosition)
const scrollPosition = localStorage.getItem(key)
if (scrollPosition) {
el.scrollTop = parseInt(scrollPosition, 10)
}
})
}

View File

@@ -1,15 +0,0 @@
import { changeThemeButton } from './alpine-components/change-theme-button.js'
import { githubRepoInfo } from './alpine-components/github-repo-info.js'
import { dashboardAsideItem } from './alpine-components/dashboard-aside-item.js'
import { genericSlider } from './alpine-components/generic-slider.js'
import { optionsDropdown } from './alpine-components/options-dropdown.js'
export function initAlpineComponents () {
document.addEventListener('alpine:init', () => {
Alpine.data(changeThemeButton.name, changeThemeButton.fn)
Alpine.data(githubRepoInfo.name, githubRepoInfo.fn)
Alpine.data(dashboardAsideItem.name, dashboardAsideItem.fn)
Alpine.data(genericSlider.name, genericSlider.fn)
Alpine.data(optionsDropdown.name, optionsDropdown.fn)
})
}

View File

@@ -16,7 +16,7 @@ type ChangeThemeButtonParams struct {
func ChangeThemeButton(params ChangeThemeButtonParams) gomponents.Node {
return html.Div(
alpine.XData("changeThemeButton"),
alpine.XData("alpineChangeThemeButton()"),
alpine.XCloak(),
components.Classes{

View File

@@ -1,6 +1,5 @@
export const changeThemeButton = {
name: 'changeThemeButton',
fn: () => ({
window.alpineChangeThemeButton = function () {
return {
theme: '',
loadTheme () {
@@ -16,5 +15,5 @@ export const changeThemeButton = {
init () {
this.loadTheme()
}
})
}
}

View File

@@ -11,7 +11,7 @@ import (
func OptionsDropdown(children ...gomponents.Node) gomponents.Node {
return html.Div(
html.Class("inline-block"),
alpine.XData("options_dropdown"),
alpine.XData("alpineOptionsDropdown()"),
alpine.XOn("mouseenter", "open()"),
alpine.XOn("mouseleave", "close()"),
html.Button(

View File

@@ -1,6 +1,5 @@
export const optionsDropdown = {
name: 'options_dropdown',
fn: () => ({
window.alpineOptionsDropdown = function () {
return {
isOpen: false,
buttonEl: null,
contentEl: null,
@@ -43,5 +42,5 @@ export const optionsDropdown = {
this.contentEl.style.top = `${buttonRect.top - contentHeight}px`
}
}
})
}
}

View File

@@ -0,0 +1,25 @@
package component
import (
"database/sql"
"github.com/eduardolat/pgbackweb/internal/integration/postgres"
"github.com/maragudk/gomponents"
"github.com/maragudk/gomponents/html"
)
func PGVersionSelectOptions(selectedVersion sql.NullString) gomponents.Node {
return GMap(
postgres.PGVersions,
func(pgVersion postgres.PGVersion) gomponents.Node {
return html.Option(
html.Value(pgVersion.Value.Version),
gomponents.Textf("PostgreSQL %s", pgVersion.Value.Version),
gomponents.If(
selectedVersion.Valid && selectedVersion.String == pgVersion.Value.Version,
html.Selected(),
),
)
},
)
}

View File

@@ -10,7 +10,7 @@ import (
func StarOnGithub(size size) gomponents.Node {
return html.A(
alpine.XData("githubRepoInfo"),
alpine.XData("alpineStarOnGithub()"),
alpine.XCloak(),
components.Classes{
"btn btn-neutral": true,

View File

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

View File

@@ -1,6 +1,8 @@
package databases
import (
"database/sql"
lucide "github.com/eduardolat/gomponents-lucide"
"github.com/eduardolat/pgbackweb/internal/database/dbgen"
"github.com/eduardolat/pgbackweb/internal/validate"
@@ -77,10 +79,7 @@ func createDatabaseButton() gomponents.Node {
Required: true,
HelpText: "The version of the database",
Children: []gomponents.Node{
html.Option(html.Value("13"), gomponents.Text("PostgreSQL 13")),
html.Option(html.Value("14"), gomponents.Text("PostgreSQL 14")),
html.Option(html.Value("15"), gomponents.Text("PostgreSQL 15")),
html.Option(html.Value("16"), gomponents.Text("PostgreSQL 16")),
component.PGVersionSelectOptions(sql.NullString{}),
},
}),

View File

@@ -90,22 +90,10 @@ func editDatabaseButton(
Required: true,
HelpText: "The version of the database",
Children: []gomponents.Node{
html.Option(
gomponents.If(database.PgVersion == "13", html.Selected()),
html.Value("13"), gomponents.Text("PostgreSQL 13"),
),
html.Option(
gomponents.If(database.PgVersion == "14", html.Selected()),
html.Value("14"), gomponents.Text("PostgreSQL 14"),
),
html.Option(
gomponents.If(database.PgVersion == "15", html.Selected()),
html.Value("15"), gomponents.Text("PostgreSQL 15"),
),
html.Option(
gomponents.If(database.PgVersion == "16", html.Selected()),
html.Value("16"), gomponents.Text("PostgreSQL 16"),
),
component.PGVersionSelectOptions(sql.NullString{
Valid: true,
String: database.PgVersion,
}),
},
}),

View File

@@ -4,11 +4,9 @@ import (
"fmt"
"net/http"
lucide "github.com/eduardolat/gomponents-lucide"
"github.com/eduardolat/pgbackweb/internal/database/dbgen"
"github.com/eduardolat/pgbackweb/internal/util/echoutil"
"github.com/eduardolat/pgbackweb/internal/view/reqctx"
"github.com/eduardolat/pgbackweb/internal/view/web/alpine"
"github.com/eduardolat/pgbackweb/internal/view/web/component"
"github.com/eduardolat/pgbackweb/internal/view/web/layout"
"github.com/google/uuid"
@@ -201,111 +199,8 @@ func indexPage(
BgColors: []string{blueColor, greenColor, redColor},
}),
),
html.Div(
alpine.XData("genericSlider(4)"),
alpine.XCloak(),
html.Class("mt-6 flex flex-col justify-center items-center mx-auto"),
component.H2Text("How to use PG Back Web"),
component.CardBox(component.CardBoxParams{
Class: "mt-4 space-y-4 max-w-[600px]",
Children: []gomponents.Node{
html.Div(
html.Class("flex justify-center"),
html.Ul(
html.Class("steps"),
html.Li(
html.Class("step"),
alpine.XBind("class", "currentSlide >= 1 ? 'step-primary' : ''"),
gomponents.Text("Create database"),
),
html.Li(
html.Class("step"),
alpine.XBind("class", "currentSlide >= 2 ? 'step-primary' : ''"),
gomponents.Text("Create destination"),
),
html.Li(
html.Class("step"),
alpine.XBind("class", "currentSlide >= 3 ? 'step-primary' : ''"),
gomponents.Text("Create backup"),
),
html.Li(
html.Class("step"),
alpine.XBind("class", "currentSlide >= 4 ? 'step-primary' : ''"),
gomponents.Text("Wait for executions"),
),
),
),
html.Div(
alpine.XShow("currentSlide === 1"),
component.H3Text("Create database"),
component.PText(`
To create a database, click on the "Databases" menu item on the
left sidebar. Then click on the "Create database" button. Fill
in the form and click on the "Save" button. You can create as
many databases as you want to backup.
`),
),
html.Div(
alpine.XShow("currentSlide === 2"),
component.H3Text("Create S3 destination (optional)"),
component.PText(`
To create a destination, click on the "Destinations" menu item on
the left sidebar. Then click on the "Create destination" button.
Fill in the form and click on the "Save" button. You can create
as many destinations as you want to store the backups. If you
don't want to use S3 destinations and store the backups locally,
you can skip this step.
`),
),
html.Div(
alpine.XShow("currentSlide === 3"),
component.H3Text("Create backup"),
component.PText(`
To create a backup you need to have at least one database and one
destination. Click on the "Backups" menu item on the left sidebar.
Then click on the "Create backup" button. Fill in the form and
click on the "Save" button. You can create as many backups as you
want including any combination of databases and destinations.
`),
),
html.Div(
alpine.XShow("currentSlide === 4"),
component.H3Text("Wait for executions"),
component.PText(`
When your backup is created and active, the system will start
creating executions based on the schedule you defined. You can
also create executions manually by clicking the "Run backup now"
button on the backups list. You can see the executions on the
"Executions" menu item on the left sidebar and then click on the
"Show details" button to see the details, logs, and download or
delete the backup file.
`),
),
html.Div(
html.Class("mt-4 flex justify-between"),
html.Button(
alpine.XBind("disabled", "!hasPrevSlide"),
alpine.XOn("click", "prevSlide"),
html.Class("btn btn-neutral btn-ghost"),
lucide.ChevronLeft(),
component.SpanText("Previous"),
),
html.Button(
alpine.XBind("disabled", "!hasNextSlide"),
alpine.XOn("click", "nextSlide"),
html.Class("btn btn-neutral btn-ghost"),
component.SpanText("Next"),
lucide.ChevronRight(),
),
),
},
}),
),
indexHowTo(),
}
return layout.Dashboard(reqCtx, layout.DashboardParams{

View File

@@ -0,0 +1,119 @@
package summary
import (
lucide "github.com/eduardolat/gomponents-lucide"
"github.com/eduardolat/pgbackweb/internal/view/web/alpine"
"github.com/eduardolat/pgbackweb/internal/view/web/component"
"github.com/maragudk/gomponents"
"github.com/maragudk/gomponents/html"
)
func indexHowTo() gomponents.Node {
return html.Div(
alpine.XData("alpineSummaryHowToSlider()"),
alpine.XCloak(),
html.Class("mt-6 flex flex-col justify-center items-center mx-auto"),
component.H2Text("How to use PG Back Web"),
component.CardBox(component.CardBoxParams{
Class: "mt-4 space-y-4 max-w-[600px]",
Children: []gomponents.Node{
html.Div(
html.Class("flex justify-center"),
html.Ul(
html.Class("steps"),
html.Li(
html.Class("step"),
alpine.XBind("class", "currentSlide >= 1 ? 'step-primary' : ''"),
gomponents.Text("Create database"),
),
html.Li(
html.Class("step"),
alpine.XBind("class", "currentSlide >= 2 ? 'step-primary' : ''"),
gomponents.Text("Create destination"),
),
html.Li(
html.Class("step"),
alpine.XBind("class", "currentSlide >= 3 ? 'step-primary' : ''"),
gomponents.Text("Create backup"),
),
html.Li(
html.Class("step"),
alpine.XBind("class", "currentSlide >= 4 ? 'step-primary' : ''"),
gomponents.Text("Wait for executions"),
),
),
),
html.Div(
alpine.XShow("currentSlide === 1"),
component.H3Text("Create database"),
component.PText(`
To create a database, click on the "Databases" menu item on the
left sidebar. Then click on the "Create database" button. Fill
in the form and click on the "Save" button. You can create as
many databases as you want to backup.
`),
),
html.Div(
alpine.XShow("currentSlide === 2"),
component.H3Text("Create S3 destination (optional)"),
component.PText(`
To create a destination, click on the "Destinations" menu item on
the left sidebar. Then click on the "Create destination" button.
Fill in the form and click on the "Save" button. You can create
as many destinations as you want to store the backups. If you
don't want to use S3 destinations and store the backups locally,
you can skip this step.
`),
),
html.Div(
alpine.XShow("currentSlide === 3"),
component.H3Text("Create backup"),
component.PText(`
To create a backup you need to have at least one database and one
destination. Click on the "Backups" menu item on the left sidebar.
Then click on the "Create backup" button. Fill in the form and
click on the "Save" button. You can create as many backups as you
want including any combination of databases and destinations.
`),
),
html.Div(
alpine.XShow("currentSlide === 4"),
component.H3Text("Wait for executions"),
component.PText(`
When your backup is created and active, the system will start
creating executions based on the schedule you defined. You can
also create executions manually by clicking the "Run backup now"
button on the backups list. You can see the executions on the
"Executions" menu item on the left sidebar and then click on the
"Show details" button to see the details, logs, and download or
delete the backup file.
`),
),
html.Div(
html.Class("mt-4 flex justify-between"),
html.Button(
alpine.XBind("disabled", "!hasPrevSlide"),
alpine.XOn("click", "prevSlide"),
html.Class("btn btn-neutral btn-ghost"),
lucide.ChevronLeft(),
component.SpanText("Previous"),
),
html.Button(
alpine.XBind("disabled", "!hasNextSlide"),
alpine.XOn("click", "nextSlide"),
html.Class("btn btn-neutral btn-ghost"),
component.SpanText("Next"),
lucide.ChevronRight(),
),
),
},
}),
)
}

View File

@@ -1,18 +1,22 @@
export const genericSlider = {
name: 'genericSlider',
fn: (slidesQty = 0) => ({
currentSlide: slidesQty > 0 ? 1 : 0,
window.alpineSummaryHowToSlider = function () {
return {
slidesQty: 4,
currentSlide: 1,
get hasNextSlide () {
return this.currentSlide < slidesQty
return this.currentSlide < this.slidesQty
},
get hasPrevSlide () {
return this.currentSlide > 1
},
nextSlide () {
if (this.hasNextSlide) this.currentSlide++
},
prevSlide () {
if (this.hasPrevSlide) this.currentSlide--
}
})
}
}

View File

@@ -17,8 +17,8 @@ func head() gomponents.Node {
return gomponents.Group([]gomponents.Node{
html.Link(html.Rel("shortcut icon"), href("/favicon.ico")),
html.Link(html.Rel("stylesheet"), href("/css/style.min.css")),
html.Script(src("/js/app.min.js")),
html.Link(html.Rel("stylesheet"), href("/build/style.min.css")),
html.Script(src("/build/app.min.js")),
html.Script(src("/libs/htmx/htmx-2.0.1.min.js"), html.Defer()),
html.Script(src("/libs/alpinejs/alpinejs-3.14.1.min.js"), html.Defer()),

View File

@@ -118,7 +118,7 @@ func dashboardAsideItem(
text, link string, strict bool,
) gomponents.Node {
return html.A(
alpine.XData(fmt.Sprintf("dashboardAsideItem('%s', %t)", link, strict)),
alpine.XData(fmt.Sprintf("alpineDashboardAsideItem('%s', %t)", link, strict)),
html.Class("block flex flex-col items-center justify-center group"),
html.Href(link),
html.Button(

View File

@@ -1,6 +1,26 @@
export const dashboardAsideItem = {
name: 'dashboardAsideItem',
fn: (link = '', strict = false) => ({
document.addEventListener('DOMContentLoaded', function () {
const el = document.getElementById('dashboard-aside')
const key = 'dashboard-aside-scroll-position'
if (!el) return
const saveScrollPosition = window.debounce(
() => {
const scrollPosition = el.scrollTop
localStorage.setItem(key, scrollPosition)
},
200
)
el.addEventListener('scroll', saveScrollPosition)
const scrollPosition = localStorage.getItem(key)
if (scrollPosition) {
el.scrollTop = parseInt(scrollPosition, 10)
}
})
window.alpineDashboardAsideItem = function (link = '', strict = false) {
return {
link,
strict,
is_active: false,
@@ -29,5 +49,5 @@ export const dashboardAsideItem = {
this.checkActive()
}
}
})
}
}

View File

@@ -1,11 +1,7 @@
package layout
import (
"fmt"
lucide "github.com/eduardolat/gomponents-lucide"
"github.com/eduardolat/pgbackweb/internal/config"
"github.com/eduardolat/pgbackweb/internal/view/web/alpine"
"github.com/eduardolat/pgbackweb/internal/view/web/component"
"github.com/eduardolat/pgbackweb/internal/view/web/htmx"
"github.com/maragudk/gomponents"
@@ -28,7 +24,7 @@ func dashboardHeader() gomponents.Node {
Size: component.SizeMd,
}),
component.StarOnGithub(component.SizeMd),
dashboardHeaderCheckForUpdates(),
dashboardHeaderUpdates(),
component.HxLoadingMd("header-indicator"),
),
html.Div(
@@ -55,26 +51,3 @@ func dashboardHeader() gomponents.Node {
),
)
}
func dashboardHeaderCheckForUpdates() gomponents.Node {
return html.A(
alpine.XData("githubRepoInfo"),
alpine.XCloak(),
alpine.XShow(fmt.Sprintf(
"latestRelease !== '' && latestRelease !== '%s'",
config.Version,
)),
components.Classes{
"btn btn-warning": true,
},
html.Href("https://github.com/eduardolat/pgbackweb/releases"),
html.Target("_blank"),
lucide.ExternalLink(),
component.SpanText("Update available"),
html.Span(
alpine.XShow("stars"),
alpine.XText("'( ' + latestRelease + ' )'"),
),
)
}

View File

@@ -0,0 +1,32 @@
package layout
import (
"fmt"
lucide "github.com/eduardolat/gomponents-lucide"
"github.com/eduardolat/pgbackweb/internal/config"
"github.com/eduardolat/pgbackweb/internal/view/web/alpine"
"github.com/eduardolat/pgbackweb/internal/view/web/component"
"github.com/maragudk/gomponents"
"github.com/maragudk/gomponents/html"
)
func dashboardHeaderUpdates() gomponents.Node {
return html.A(
alpine.XData("alpineDashboardHeaderUpdates()"),
alpine.XCloak(),
alpine.XShow(fmt.Sprintf(
"latestRelease !== null && latestRelease !== '%s'",
config.Version,
)),
html.Class("btn btn-warning"),
html.Href("https://github.com/eduardolat/pgbackweb/releases"),
html.Target("_blank"),
lucide.ExternalLink(),
component.SpanText("Update available"),
html.Span(
alpine.XText("'( ' + latestRelease + ' )'"),
),
)
}

View File

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

218
package-lock.json generated
View File

@@ -11,6 +11,8 @@
"devDependencies": {
"daisyui": "4.12.10",
"esbuild": "0.23.1",
"fs-extra": "11.2.0",
"glob": "11.0.0",
"standard": "17.1.0",
"tailwindcss": "3.4.6"
}
@@ -575,6 +577,7 @@
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
"integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==",
"dev": true,
"license": "ISC",
"dependencies": {
"string-width": "^5.1.2",
"string-width-cjs": "npm:string-width@^4.2.0",
@@ -675,6 +678,7 @@
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
"integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==",
"dev": true,
"license": "MIT",
"optional": true,
"engines": {
"node": ">=14"
@@ -730,10 +734,11 @@
}
},
"node_modules/ansi-regex": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz",
"integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==",
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz",
"integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
},
@@ -746,6 +751,7 @@
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz",
"integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
},
@@ -972,6 +978,7 @@
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
"dev": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0"
}
@@ -1346,13 +1353,15 @@
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
"integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
"dev": true
"dev": true,
"license": "MIT"
},
"node_modules/emoji-regex": {
"version": "9.2.2",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
"integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
"dev": true
"dev": true,
"license": "MIT"
},
"node_modules/error-ex": {
"version": "1.3.2",
@@ -2304,6 +2313,21 @@
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/fs-extra": {
"version": "11.2.0",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz",
"integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==",
"dev": true,
"license": "MIT",
"dependencies": {
"graceful-fs": "^4.2.0",
"jsonfile": "^6.0.1",
"universalify": "^2.0.0"
},
"engines": {
"node": ">=14.14"
}
},
"node_modules/fs.realpath": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
@@ -2409,21 +2433,25 @@
}
},
"node_modules/glob": {
"version": "10.4.5",
"resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz",
"integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==",
"version": "11.0.0",
"resolved": "https://registry.npmjs.org/glob/-/glob-11.0.0.tgz",
"integrity": "sha512-9UiX/Bl6J2yaBbxKoEBRm4Cipxgok8kQYcOPEhScPwebu2I0HoQOuYdIO6S3hLuWoZgpDpwQZMzTFxgpkyT76g==",
"dev": true,
"license": "ISC",
"dependencies": {
"foreground-child": "^3.1.0",
"jackspeak": "^3.1.2",
"minimatch": "^9.0.4",
"jackspeak": "^4.0.1",
"minimatch": "^10.0.0",
"minipass": "^7.1.2",
"package-json-from-dist": "^1.0.0",
"path-scurry": "^1.11.1"
"path-scurry": "^2.0.0"
},
"bin": {
"glob": "dist/esm/bin.mjs"
},
"engines": {
"node": "20 || >=22"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
@@ -2801,6 +2829,7 @@
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
@@ -3043,18 +3072,19 @@
}
},
"node_modules/jackspeak": {
"version": "3.4.3",
"resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz",
"integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==",
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.0.2.tgz",
"integrity": "sha512-bZsjR/iRjl1Nk1UkjGpAzLNfQtzuijhn2g+pbZb98HQ1Gk8vM9hfbxeMBP+M2/UUdwj0RqGG3mlvk2MsAqwvEw==",
"dev": true,
"license": "BlueOak-1.0.0",
"dependencies": {
"@isaacs/cliui": "^8.0.2"
},
"engines": {
"node": "20 || >=22"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
},
"optionalDependencies": {
"@pkgjs/parseargs": "^0.11.0"
}
},
"node_modules/jiti": {
@@ -3120,6 +3150,19 @@
"json5": "lib/cli.js"
}
},
"node_modules/jsonfile": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz",
"integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"universalify": "^2.0.0"
},
"optionalDependencies": {
"graceful-fs": "^4.1.6"
}
},
"node_modules/jsx-ast-utils": {
"version": "3.3.5",
"resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz",
@@ -3240,10 +3283,14 @@
}
},
"node_modules/lru-cache": {
"version": "10.4.3",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
"integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
"dev": true
"version": "11.0.1",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.0.1.tgz",
"integrity": "sha512-CgeuL5uom6j/ZVrg7G/+1IXqRY8JXX4Hghfy5YE0EhoYQWvndP1kufu58cmZLNIDKnRhZrXfdS9urVWx98AipQ==",
"dev": true,
"license": "ISC",
"engines": {
"node": "20 || >=22"
}
},
"node_modules/merge2": {
"version": "1.4.1",
@@ -3268,15 +3315,16 @@
}
},
"node_modules/minimatch": {
"version": "9.0.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
"integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
"version": "10.0.1",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.1.tgz",
"integrity": "sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==",
"dev": true,
"license": "ISC",
"dependencies": {
"brace-expansion": "^2.0.1"
},
"engines": {
"node": ">=16 || 14 >=14.17"
"node": "20 || >=22"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
@@ -3296,6 +3344,7 @@
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
"integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
"dev": true,
"license": "ISC",
"engines": {
"node": ">=16 || 14 >=14.17"
}
@@ -3600,16 +3649,17 @@
"dev": true
},
"node_modules/path-scurry": {
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz",
"integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==",
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.0.tgz",
"integrity": "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==",
"dev": true,
"license": "BlueOak-1.0.0",
"dependencies": {
"lru-cache": "^10.2.0",
"minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
"lru-cache": "^11.0.0",
"minipass": "^7.1.2"
},
"engines": {
"node": ">=16 || 14 >=14.18"
"node": "20 || >=22"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
@@ -4341,6 +4391,7 @@
"resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
"integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
"dev": true,
"license": "MIT",
"dependencies": {
"eastasianwidth": "^0.2.0",
"emoji-regex": "^9.2.2",
@@ -4359,6 +4410,7 @@
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
"dev": true,
"license": "MIT",
"dependencies": {
"emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0",
@@ -4373,6 +4425,7 @@
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
@@ -4381,13 +4434,15 @@
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
"dev": true
"dev": true,
"license": "MIT"
},
"node_modules/string-width-cjs/node_modules/strip-ansi": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-regex": "^5.0.1"
},
@@ -4485,6 +4540,7 @@
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
"integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-regex": "^6.0.1"
},
@@ -4501,6 +4557,7 @@
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-regex": "^5.0.1"
},
@@ -4513,6 +4570,7 @@
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
@@ -4560,6 +4618,83 @@
"node": ">=16 || 14 >=14.17"
}
},
"node_modules/sucrase/node_modules/glob": {
"version": "10.4.5",
"resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz",
"integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==",
"dev": true,
"license": "ISC",
"dependencies": {
"foreground-child": "^3.1.0",
"jackspeak": "^3.1.2",
"minimatch": "^9.0.4",
"minipass": "^7.1.2",
"package-json-from-dist": "^1.0.0",
"path-scurry": "^1.11.1"
},
"bin": {
"glob": "dist/esm/bin.mjs"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/sucrase/node_modules/jackspeak": {
"version": "3.4.3",
"resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz",
"integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==",
"dev": true,
"license": "BlueOak-1.0.0",
"dependencies": {
"@isaacs/cliui": "^8.0.2"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
},
"optionalDependencies": {
"@pkgjs/parseargs": "^0.11.0"
}
},
"node_modules/sucrase/node_modules/lru-cache": {
"version": "10.4.3",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
"integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
"dev": true,
"license": "ISC"
},
"node_modules/sucrase/node_modules/minimatch": {
"version": "9.0.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
"integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
"dev": true,
"license": "ISC",
"dependencies": {
"brace-expansion": "^2.0.1"
},
"engines": {
"node": ">=16 || 14 >=14.17"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/sucrase/node_modules/path-scurry": {
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz",
"integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==",
"dev": true,
"license": "BlueOak-1.0.0",
"dependencies": {
"lru-cache": "^10.2.0",
"minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
},
"engines": {
"node": ">=16 || 14 >=14.18"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
@@ -4790,6 +4925,16 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/universalify": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
"integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 10.0.0"
}
},
"node_modules/uri-js": {
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
@@ -4922,6 +5067,7 @@
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
"integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-styles": "^6.1.0",
"string-width": "^5.0.1",
@@ -4940,6 +5086,7 @@
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-styles": "^4.0.0",
"string-width": "^4.1.0",
@@ -4957,6 +5104,7 @@
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
@@ -4966,6 +5114,7 @@
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"dev": true,
"license": "MIT",
"dependencies": {
"color-convert": "^2.0.1"
},
@@ -4980,13 +5129,15 @@
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
"dev": true
"dev": true,
"license": "MIT"
},
"node_modules/wrap-ansi-cjs/node_modules/string-width": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
"dev": true,
"license": "MIT",
"dependencies": {
"emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0",
@@ -5001,6 +5152,7 @@
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-regex": "^5.0.1"
},

View File

@@ -10,6 +10,8 @@
"devDependencies": {
"daisyui": "4.12.10",
"esbuild": "0.23.1",
"fs-extra": "11.2.0",
"glob": "11.0.0",
"standard": "17.1.0",
"tailwindcss": "3.4.6"
},
@@ -25,4 +27,4 @@
"Swal"
]
}
}
}

107
scripts/build-js.mjs Normal file
View File

@@ -0,0 +1,107 @@
#!/usr/bin/env node
/**
* This script is responsible for creating the final build of all the project's
* JavaScript.
*
* 1. Creates a prebuild of internal/view/static/js/app.js
* 2. Creates a prebuild of all *.inc.js files in internal/view/web
* 3. Combines the two prebuilds into a final build, minifies it, and stores
* it in internal/view/static/build/app.min.js
*
* This script generates temporal files in the tmp directory.
*/
import path from 'path'
import fse from 'fs-extra'
import * as esbuild from 'esbuild'
import { fileURLToPath } from 'url'
import { glob } from 'glob'
// Obtains the project's root directory
const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
const rootDir = path.join(__dirname, '..')
async function prebuildApp () {
const entryFile = path.join(rootDir, './internal/view/static/js/app.js')
const outFile = path.join(rootDir, './tmp/prebuild-app.js')
try {
await esbuild.build({
entryPoints: [entryFile],
bundle: true,
minify: false,
outfile: outFile,
format: 'iife'
})
} catch (error) {
console.error('Error prebuilding app.js:', error)
}
}
async function prebuildIncludedFiles () {
const alpineGlob = path.join(rootDir, './internal/view/web/**/*.inc.js')
const tempFile = path.join(rootDir, './tmp/prebuild-incs.js')
try {
const alpineFiles = await glob(alpineGlob)
let outFileContent = `// This file is auto-generated by ${__filename}. DO NOT EDIT.\n\n`
for (const file of alpineFiles) {
const content = await fse.readFile(file, 'utf-8')
outFileContent += content + '\n'
}
await fse.outputFile(tempFile, outFileContent)
} catch (error) {
console.error('Error prebuilding *.inc.js files:', error)
}
}
async function mergePrebuilds () {
const prebuilds = [
path.join(rootDir, './tmp/prebuild-app.js'),
path.join(rootDir, './tmp/prebuild-incs.js')
]
const outFile = path.join(rootDir, './tmp/prebuild.js')
try {
let outFileContent = ''
for (const file of prebuilds) {
const content = await fse.readFile(file, 'utf-8')
outFileContent += content + '\n\n'
}
await fse.outputFile(outFile, outFileContent)
} catch (error) {
console.error('Error combining prebuilds:', error)
}
}
async function build () {
const entryFile = path.join(rootDir, './tmp/prebuild.js')
const outFile = path.join(rootDir, './internal/view/static/build/app.min.js')
try {
await esbuild.build({
entryPoints: [entryFile],
bundle: true,
minify: true,
outfile: outFile,
format: 'iife'
})
console.log('JavaScript builded successfully')
} catch (error) {
console.error('Error creating JavaScript build:', error)
}
}
await prebuildApp()
await prebuildIncludedFiles()
await mergePrebuilds()
await build()

View File

@@ -33,12 +33,13 @@ check_command "/usr/lib/postgresql/15/bin/psql --version" "PostgreSQL 15 psql"
check_command "/usr/lib/postgresql/15/bin/pg_dump --version" "PostgreSQL 15 pg_dump"
check_command "/usr/lib/postgresql/16/bin/psql --version" "PostgreSQL 16 psql"
check_command "/usr/lib/postgresql/16/bin/pg_dump --version" "PostgreSQL 16 pg_dump"
check_command "/usr/lib/postgresql/17/bin/psql --version" "PostgreSQL 17 psql"
check_command "/usr/lib/postgresql/17/bin/pg_dump --version" "PostgreSQL 17 pg_dump"
# Check software installed by downloading binaries
check_command "task --version" "task"
check_command "goose --version" "goose"
check_command "sqlc version" "sqlc"
check_command "golangci-lint --version" "golangci-lint"
check_command "air -v" "air"
echo "All dependencies are working correctly!"

View File

@@ -1,4 +0,0 @@
#!/usr/bin/bash
# Fix permissions of all files in the current directory and subdirectories
chmod -R 777 ./

39
scripts/sqlc-prebuild.mjs Normal file
View File

@@ -0,0 +1,39 @@
#!/usr/bin/env node
import path from 'path'
import fse from 'fs-extra'
import { fileURLToPath } from 'url'
import { glob } from 'glob'
const sourceGlobs = [
'./internal/service/**/*.sql'
]
const outputFilePath = './internal/database/dbgen/queries.gen.sql'
// Obtains the project's root directory
const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
const rootDir = path.join(__dirname, '..')
const absoluteOutputFilePath = path.join(rootDir, outputFilePath)
try {
// Find all files that match the source globs
const files = []
for (const sourceGlob of sourceGlobs) {
const foundFiles = await glob(path.join(rootDir, sourceGlob))
files.push(...foundFiles)
}
// Read each file and concatenate them into a single string
let outFileContent = `-- This file is auto-generated by ${__filename}. DO NOT EDIT.\n\n`
for (const file of files) {
const content = await fse.readFile(file, 'utf-8')
outFileContent += `-- file: ${file}\n\n${content}\n\n`
}
await fse.ensureDir(path.dirname(absoluteOutputFilePath))
await fse.outputFile(absoluteOutputFilePath, outFileContent)
} catch (error) {
console.error('Error creating SQLC prebuild:', error)
}

View File

@@ -3,8 +3,7 @@ version: "2"
sql:
- engine: "postgresql"
schema: "./internal/database/migrations/"
queries:
- "./internal/service/*/"
queries: "./internal/database/dbgen/queries.gen.sql"
gen:
go:
package: "dbgen"

View File

@@ -2,6 +2,8 @@ version: "3"
dotenv: [".env"]
interval: 500ms
tasks:
on:
desc: Start development environment, should be run from the host machine
@@ -15,7 +17,31 @@ tasks:
dev:
desc: Build and serve the project with hot reloading
cmd: air -c .air.toml
watch: true
sources:
- "**/*.go"
- "**/*.sql"
- "**/*.js"
- "**/*.css"
- "**/*.json"
- exclude: "./.git/**"
- exclude: "./node_modules/**"
- exclude: "./internal/database/dbgen/**"
- exclude: "./internal/view/static/build/**"
- exclude: "./temp/**"
- exclude: "./tmp/**"
- exclude: "./dist/**"
- exclude: "**/*.test.go"
- exclude: "**/*.generated.go"
- exclude: "**/*.sql.go"
- exclude: "**/*.gen.go"
- exclude: "**/*.gen.sql"
- exclude: "**/*.min.css"
- exclude: "**/*.min.js"
deps:
- build
cmds:
- ./dist/app
build:
desc: Build the project
@@ -54,9 +80,14 @@ tasks:
gen-db:
desc: Generate sqlc files
silent: true
cmd: sqlc generate
cmds:
- ./scripts/sqlc-prebuild.mjs
- sqlc generate
sources:
- ./internal/**/*.sql
- ./internal/service/**/*.sql
generates:
- ./internal/database/queries.gen.sql
- ./internal/database/dbgen/*.go
reset-db:
desc: Reset the database
@@ -68,12 +99,11 @@ tasks:
cmds:
- >
npm run tailwindcss --
--minify
--config ./tailwind.config.js
--input ./internal/view/static/css/style.css
--output ./internal/view/static/css/style.min.css --minify
- >
npm run esbuild -- ./internal/view/static/js/app.js
--bundle --minify --outfile=./internal/view/static/js/app.min.js
--output ./internal/view/static/build/style.min.css
- ./scripts/build-js.mjs
tidy:
desc: Tidy the go.mod file
@@ -118,6 +148,6 @@ tasks:
- rm -rf ./tmp
- rm -rf ./dist
fixperms: # Fixes the permissions of the files in the project
fixperms:
desc: Fixes the permissions of the files in the project
cmd: ./scripts/fixperms.sh
cmd: chmod -R 777 ./