Compare commits
4 Commits
d31f24c101
...
a1e5346fdf
| Author | SHA1 | Date | |
|---|---|---|---|
|
a1e5346fdf
|
|||
|
5b8177bd12
|
|||
|
8c18e81358
|
|||
|
0558c719d6
|
39
.github/workflows/docker-publish.yml
vendored
Normal file
39
.github/workflows/docker-publish.yml
vendored
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
name: Build and Push Docker Image to GHCR
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: ["main"]
|
||||||
|
tags: ["v*.*.*"]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-and-push:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
packages: write
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Log in to GHCR
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ghcr.io
|
||||||
|
username: ${{ github.actor }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Extract Docker metadata
|
||||||
|
id: meta
|
||||||
|
uses: docker/metadata-action@v5
|
||||||
|
with:
|
||||||
|
images: ghcr.io/${{ github.repository }}
|
||||||
|
|
||||||
|
- name: Build and push Docker image
|
||||||
|
uses: docker/build-push-action@v6
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
push: true
|
||||||
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -1,6 +1,8 @@
|
|||||||
passport
|
passport
|
||||||
.env
|
.env
|
||||||
passport.db*
|
passport.db*
|
||||||
public/uploads/
|
public
|
||||||
zqdgr
|
zqdgr
|
||||||
|
|
||||||
|
# compiled via go prepare
|
||||||
assets/tailwind.css
|
assets/tailwind.css
|
||||||
27
Dockerfile
Normal file
27
Dockerfile
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
FROM golang:1.25 AS builder
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
ENV CGO_ENABLED=1 GOOS=linux GOARCH=amd64
|
||||||
|
RUN apt-get update && apt-get install -y gcc libc6-dev sqlite3 ca-certificates
|
||||||
|
|
||||||
|
COPY go.mod go.sum ./
|
||||||
|
RUN go mod download
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# tailwindcss needed for go generate
|
||||||
|
RUN curl -sLO https://github.com/tailwindlabs/tailwindcss/releases/download/v4.1.13/tailwindcss-linux-x64
|
||||||
|
RUN chmod +x tailwindcss-linux-x64
|
||||||
|
RUN mv tailwindcss-linux-x64 /usr/local/bin/tailwindcss
|
||||||
|
|
||||||
|
RUN go generate
|
||||||
|
RUN go build -ldflags="-w -s" -o passport
|
||||||
|
|
||||||
|
# ---- Runtime Stage ----
|
||||||
|
FROM gcr.io/distroless/cc-debian12
|
||||||
|
|
||||||
|
WORKDIR /data
|
||||||
|
COPY --from=builder /app/passport /usr/local/bin/passport
|
||||||
|
EXPOSE 3000
|
||||||
|
|
||||||
|
CMD ["/usr/local/bin/passport"]
|
||||||
75
README.md
75
README.md
@@ -17,47 +17,68 @@ Passport is a simple, fast, and lightweight web dashboard/new tab replacement.
|
|||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
1. Clone the repository
|
### Docker
|
||||||
2. Configure the `.env` file, an example is provided in the `.env.example` file, see below for every available environment variable
|
|
||||||
4. Deploy `passport` to your web server
|
Passport is available as a Docker image via this repository. This is the recommended way to run Passport.
|
||||||
5. profit
|
|
||||||
|
```bash
|
||||||
|
docker run -d --name passport -p 3000:3000 -e PASSPORT_ADMIN_USERNAME=admin -e PASSPORT_ADMIN_PASSWORD=password ghcr.io/juls0730/passport:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
### Building from source
|
||||||
|
|
||||||
|
If you want to build from source, you will need to install the dependencies first.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go install github.com/juls0730/zqdgr@latest
|
||||||
|
go install github.com/tailwindlabs/tailwindcss-cli@latest
|
||||||
|
```
|
||||||
|
|
||||||
|
Then you can build the binary.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go build -o passport
|
||||||
|
```
|
||||||
|
|
||||||
|
You can then run the binary.
|
||||||
|
|
||||||
### Configuration
|
### Configuration
|
||||||
|
|
||||||
#### Passport configuration
|
#### Passport configuration
|
||||||
|
|
||||||
| Environment Variable | Description | Required | Default |
|
| Environment Variable | Description | Required | Default |
|
||||||
| --- | --- | --- | --- |
|
| -------------------------------------- | ------------------------------------------------------------------------------- | -------- | ------- |
|
||||||
| `PASSPORT_DEV_MODE` | Enables dev mode | false | false |
|
| `PASSPORT_DEV_MODE` | Enables dev mode | false | false |
|
||||||
| `PASSPORT_ENABLE_PREFORK` | Enables preforking | false | false |
|
| `PASSPORT_ENABLE_PREFORK` | Enables preforking | false | false |
|
||||||
| `PASSPORT_ENABLE_WEATHER` | Enables weather data, see [Weather configuration](#weather-configuration) | false | false |
|
| `PASSPORT_ENABLE_WEATHER` | Enables weather data, see [Weather configuration](#weather-configuration) | false | false |
|
||||||
| `PASSPORT_ENABLE_UPTIME` | Enables uptime data, see [Uptime configuration](#uptime-configuration) | false | false |
|
| `PASSPORT_ENABLE_UPTIME` | Enables uptime data, see [Uptime configuration](#uptime-configuration) | false | false |
|
||||||
| `PASSPORT_ADMIN_USERNAME` | The username for the admin dashboard | true |
|
| `PASSPORT_ADMIN_USERNAME` | The username for the admin dashboard | true |
|
||||||
| `PASSPORT_ADMIN_PASSWORD` | The password for the admin dashboard | true |
|
| `PASSPORT_ADMIN_PASSWORD` | The password for the admin dashboard | true |
|
||||||
| `PASSPORT_SEARCH_PROVIDER` | The search provider to use for the search bar, without any query parameters | true |
|
| `PASSPORT_SEARCH_PROVIDER` | The search provider to use for the search bar, without any query parameters | true |
|
||||||
| `PASSPORT_SEARCH_PROVIDER_QUERY_PARAM` | The query parameter to use for the search provider, e.g. `q` for most providers |false | q |
|
| `PASSPORT_SEARCH_PROVIDER_QUERY_PARAM` | The query parameter to use for the search provider, e.g. `q` for most providers | false | q |
|
||||||
|
|
||||||
#### Weather configuration
|
#### Weather configuration
|
||||||
|
|
||||||
| Environment Variable | Description | Required | Default |
|
| Environment Variable | Description | Required | Default |
|
||||||
| --- | --- | --- | --- |
|
| ----------------------------- | ------------------------------------------------------------------------- | ---------- | -------------- |
|
||||||
| `OPENWEATHER_PROVIDER` | The weather provider to use, currently only `openweathermap` is supported | true | openweathermap |
|
| `OPENWEATHER_PROVIDER` | The weather provider to use, currently only `openweathermap` is supported | true | openweathermap |
|
||||||
| `OPENWEATHER_API_KEY` | The OpenWeather API key | if enabled | |
|
| `OPENWEATHER_API_KEY` | The OpenWeather API key | if enabled | |
|
||||||
| `OPENWEATHER_TEMP_UNITS` | The temperature units to use, either `metric` or `imperial` | false | metric |
|
| `OPENWEATHER_TEMP_UNITS` | The temperature units to use, either `metric` or `imperial` | false | metric |
|
||||||
| `OPENWEATHER_LAT` | The latitude of your location | if enabled | |
|
| `OPENWEATHER_LAT` | The latitude of your location | if enabled | |
|
||||||
| `OPENWEATHER_LON` | The longitude of your location | if enabled | |
|
| `OPENWEATHER_LON` | The longitude of your location | if enabled | |
|
||||||
| `OPENWEATHER_UPDATE_INTERVAL` | The interval in minutes to update the weather data | false | 15 |
|
| `OPENWEATHER_UPDATE_INTERVAL` | The interval in minutes to update the weather data | false | 15 |
|
||||||
|
|
||||||
#### Uptime configuration
|
#### Uptime configuration
|
||||||
|
|
||||||
| Environment Variable | Description | Required | Default |
|
| Environment Variable | Description | Required | Default |
|
||||||
| --- | --- | --- | --- |
|
| ----------------------------- | ------------------------------------------------- | ---------- | ------- |
|
||||||
| `UPTIMEROBOT_API_KEY` | The UptimeRobot API key | if enabled | |
|
| `UPTIMEROBOT_API_KEY` | The UptimeRobot API key | if enabled | |
|
||||||
| `UPTIMEROBOT_UPDATE_INTERVAL` | The interval in seconds to update the uptime data | false | 300 |
|
| `UPTIMEROBOT_UPDATE_INTERVAL` | The interval in seconds to update the uptime data | false | 300 |
|
||||||
|
|
||||||
### Adding links and categories
|
### Adding links and categories
|
||||||
|
|
||||||
The admin dashboard can be accessed at `/admin`, you will be redirected to the login page if you are not logged in, use the credentials you configured in the `.env` file to login. Once logged in you can add links and categories.
|
The admin dashboard can be accessed at `/admin`, you will be redirected to the login page if you are not logged in, use
|
||||||
|
the credentials you configured via the environment variables to login. Once logged in you can add links and categories.
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB |
30
go.mod
30
go.mod
@@ -1,42 +1,42 @@
|
|||||||
module github.com/juls0730/passport
|
module github.com/juls0730/passport
|
||||||
|
|
||||||
go 1.23.2
|
go 1.25.0
|
||||||
|
|
||||||
require github.com/caarlos0/env/v11 v11.3.1
|
require github.com/caarlos0/env/v11 v11.3.1
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/fxamacker/cbor/v2 v2.7.0 // indirect
|
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
|
||||||
github.com/gofiber/schema v1.2.0 // indirect
|
github.com/gofiber/schema v1.6.0 // indirect
|
||||||
github.com/mailgun/raymond/v2 v2.0.48 // indirect
|
github.com/mailgun/raymond/v2 v2.0.48 // indirect
|
||||||
github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c // indirect
|
github.com/philhofer/fwd v1.2.0 // indirect
|
||||||
github.com/sirupsen/logrus v1.8.1 // indirect
|
github.com/sirupsen/logrus v1.8.1 // indirect
|
||||||
github.com/tinylib/msgp v1.2.5 // indirect
|
github.com/tinylib/msgp v1.4.0 // indirect
|
||||||
github.com/x448/float16 v0.8.4 // indirect
|
github.com/x448/float16 v0.8.4 // indirect
|
||||||
golang.org/x/crypto v0.31.0 // indirect
|
golang.org/x/crypto v0.42.0 // indirect
|
||||||
golang.org/x/net v0.31.0 // indirect
|
golang.org/x/net v0.44.0 // indirect
|
||||||
golang.org/x/text v0.21.0 // indirect
|
golang.org/x/text v0.29.0 // indirect
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/andybalholm/brotli v1.1.1 // indirect
|
github.com/andybalholm/brotli v1.2.0 // indirect
|
||||||
github.com/chai2010/webp v1.1.1
|
github.com/chai2010/webp v1.1.1
|
||||||
github.com/gofiber/fiber/v2 v2.52.5 // indirect
|
github.com/gofiber/fiber/v2 v2.52.5 // indirect
|
||||||
github.com/gofiber/fiber/v3 v3.0.0-beta.4
|
github.com/gofiber/fiber/v3 v3.0.0-rc.1
|
||||||
github.com/gofiber/template v1.8.3 // indirect
|
github.com/gofiber/template v1.8.3 // indirect
|
||||||
github.com/gofiber/template/handlebars/v2 v2.1.10
|
github.com/gofiber/template/handlebars/v2 v2.1.10
|
||||||
github.com/gofiber/utils v1.1.0 // indirect
|
github.com/gofiber/utils v1.1.0 // indirect
|
||||||
github.com/gofiber/utils/v2 v2.0.0-beta.7 // indirect
|
github.com/gofiber/utils/v2 v2.0.0-rc.1 // indirect
|
||||||
github.com/google/uuid v1.6.0
|
github.com/google/uuid v1.6.0
|
||||||
github.com/joho/godotenv v1.5.1
|
github.com/joho/godotenv v1.5.1
|
||||||
github.com/klauspost/compress v1.17.11 // indirect
|
github.com/klauspost/compress v1.18.0 // indirect
|
||||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/mattn/go-runewidth v0.0.15 // indirect
|
github.com/mattn/go-runewidth v0.0.15 // indirect
|
||||||
github.com/mattn/go-sqlite3 v1.14.24
|
github.com/mattn/go-sqlite3 v1.14.24
|
||||||
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646
|
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646
|
||||||
github.com/rivo/uniseg v0.2.0 // indirect
|
github.com/rivo/uniseg v0.2.0 // indirect
|
||||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||||
github.com/valyala/fasthttp v1.58.0 // indirect
|
github.com/valyala/fasthttp v1.66.0 // indirect
|
||||||
github.com/valyala/tcplisten v1.0.0 // indirect
|
github.com/valyala/tcplisten v1.0.0 // indirect
|
||||||
golang.org/x/sys v0.28.0 // indirect
|
golang.org/x/sys v0.36.0 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
29
go.sum
29
go.sum
@@ -1,5 +1,7 @@
|
|||||||
github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
|
github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
|
||||||
github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
|
github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
|
||||||
|
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
|
||||||
|
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
|
||||||
github.com/caarlos0/env/v11 v11.3.1 h1:cArPWC15hWmEt+gWk7YBi7lEXTXCvpaSdCiZE2X5mCA=
|
github.com/caarlos0/env/v11 v11.3.1 h1:cArPWC15hWmEt+gWk7YBi7lEXTXCvpaSdCiZE2X5mCA=
|
||||||
github.com/caarlos0/env/v11 v11.3.1/go.mod h1:qupehSf/Y0TUTsxKywqRt/vJjN5nz6vauiYEUUr8P4U=
|
github.com/caarlos0/env/v11 v11.3.1/go.mod h1:qupehSf/Y0TUTsxKywqRt/vJjN5nz6vauiYEUUr8P4U=
|
||||||
github.com/chai2010/webp v1.1.1 h1:jTRmEccAJ4MGrhFOrPMpNGIJ/eybIgwKpcACsrTEapk=
|
github.com/chai2010/webp v1.1.1 h1:jTRmEccAJ4MGrhFOrPMpNGIJ/eybIgwKpcACsrTEapk=
|
||||||
@@ -9,12 +11,18 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
|
|||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E=
|
github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E=
|
||||||
github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ=
|
github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ=
|
||||||
|
github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=
|
||||||
|
github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
|
||||||
github.com/gofiber/fiber/v2 v2.52.5 h1:tWoP1MJQjGEe4GB5TUGOi7P2E0ZMMRx5ZTG4rT+yGMo=
|
github.com/gofiber/fiber/v2 v2.52.5 h1:tWoP1MJQjGEe4GB5TUGOi7P2E0ZMMRx5ZTG4rT+yGMo=
|
||||||
github.com/gofiber/fiber/v2 v2.52.5/go.mod h1:KEOE+cXMhXG0zHc9d8+E38hoX+ZN7bhOtgeF2oT6jrQ=
|
github.com/gofiber/fiber/v2 v2.52.5/go.mod h1:KEOE+cXMhXG0zHc9d8+E38hoX+ZN7bhOtgeF2oT6jrQ=
|
||||||
github.com/gofiber/fiber/v3 v3.0.0-beta.4 h1:KzDSavvhG7m81NIsmnu5l3ZDbVS4feCidl4xlIfu6V0=
|
github.com/gofiber/fiber/v3 v3.0.0-beta.4 h1:KzDSavvhG7m81NIsmnu5l3ZDbVS4feCidl4xlIfu6V0=
|
||||||
github.com/gofiber/fiber/v3 v3.0.0-beta.4/go.mod h1:/WFUoHRkZEsGHyy2+fYcdqi109IVOFbVwxv1n1RU+kk=
|
github.com/gofiber/fiber/v3 v3.0.0-beta.4/go.mod h1:/WFUoHRkZEsGHyy2+fYcdqi109IVOFbVwxv1n1RU+kk=
|
||||||
|
github.com/gofiber/fiber/v3 v3.0.0-rc.1 h1:034MxesK6bqGkidP+QR+Ysc1ukOacBWOHCarCKC1xfg=
|
||||||
|
github.com/gofiber/fiber/v3 v3.0.0-rc.1/go.mod h1:hFdT00oT0XVuQH1/z2i5n1pl/msExHDUie1SsLOkCuM=
|
||||||
github.com/gofiber/schema v1.2.0 h1:j+ZRrNnUa/0ZuWrn/6kAtAufEr4jCJ+JuTURAMxNSZg=
|
github.com/gofiber/schema v1.2.0 h1:j+ZRrNnUa/0ZuWrn/6kAtAufEr4jCJ+JuTURAMxNSZg=
|
||||||
github.com/gofiber/schema v1.2.0/go.mod h1:YYwj01w3hVfaNjhtJzaqetymL56VW642YS3qZPhuE6c=
|
github.com/gofiber/schema v1.2.0/go.mod h1:YYwj01w3hVfaNjhtJzaqetymL56VW642YS3qZPhuE6c=
|
||||||
|
github.com/gofiber/schema v1.6.0 h1:rAgVDFwhndtC+hgV7Vu5ItQCn7eC2mBA4Eu1/ZTiEYY=
|
||||||
|
github.com/gofiber/schema v1.6.0/go.mod h1:WNZWpQx8LlPSK7ZaX0OqOh+nQo/eW2OevsXs1VZfs/s=
|
||||||
github.com/gofiber/template v1.8.3 h1:hzHdvMwMo/T2kouz2pPCA0zGiLCeMnoGsQZBTSYgZxc=
|
github.com/gofiber/template v1.8.3 h1:hzHdvMwMo/T2kouz2pPCA0zGiLCeMnoGsQZBTSYgZxc=
|
||||||
github.com/gofiber/template v1.8.3/go.mod h1:bs/2n0pSNPOkRa5VJ8zTIvedcI/lEYxzV3+YPXdBvq8=
|
github.com/gofiber/template v1.8.3/go.mod h1:bs/2n0pSNPOkRa5VJ8zTIvedcI/lEYxzV3+YPXdBvq8=
|
||||||
github.com/gofiber/template/handlebars/v2 v2.1.10 h1:Qc+uUMULCqW60LF4VKO1REpiyDAUy3vqW7xq66FPJGM=
|
github.com/gofiber/template/handlebars/v2 v2.1.10 h1:Qc+uUMULCqW60LF4VKO1REpiyDAUy3vqW7xq66FPJGM=
|
||||||
@@ -23,16 +31,22 @@ github.com/gofiber/utils v1.1.0 h1:vdEBpn7AzIUJRhe+CiTOJdUcTg4Q9RK+pEa0KPbLdrM=
|
|||||||
github.com/gofiber/utils v1.1.0/go.mod h1:poZpsnhBykfnY1Mc0KeEa6mSHrS3dV0+oBWyeQmb2e0=
|
github.com/gofiber/utils v1.1.0/go.mod h1:poZpsnhBykfnY1Mc0KeEa6mSHrS3dV0+oBWyeQmb2e0=
|
||||||
github.com/gofiber/utils/v2 v2.0.0-beta.7 h1:NnHFrRHvhrufPABdWajcKZejz9HnCWmT/asoxRsiEbQ=
|
github.com/gofiber/utils/v2 v2.0.0-beta.7 h1:NnHFrRHvhrufPABdWajcKZejz9HnCWmT/asoxRsiEbQ=
|
||||||
github.com/gofiber/utils/v2 v2.0.0-beta.7/go.mod h1:J/M03s+HMdZdvhAeyh76xT72IfVqBzuz/OJkrMa7cwU=
|
github.com/gofiber/utils/v2 v2.0.0-beta.7/go.mod h1:J/M03s+HMdZdvhAeyh76xT72IfVqBzuz/OJkrMa7cwU=
|
||||||
|
github.com/gofiber/utils/v2 v2.0.0-rc.1 h1:b77K5Rk9+Pjdxz4HlwEBnS7u5nikhx7armQB8xPds4s=
|
||||||
|
github.com/gofiber/utils/v2 v2.0.0-rc.1/go.mod h1:Y1g08g7gvST49bbjHJ1AVqcsmg93912R/tbKWhn6V3E=
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||||
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||||
github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
|
github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
|
||||||
github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
|
github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
|
||||||
|
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
||||||
|
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
||||||
github.com/mailgun/raymond/v2 v2.0.48 h1:5dmlB680ZkFG2RN/0lvTAghrSxIESeu9/2aeDqACtjw=
|
github.com/mailgun/raymond/v2 v2.0.48 h1:5dmlB680ZkFG2RN/0lvTAghrSxIESeu9/2aeDqACtjw=
|
||||||
github.com/mailgun/raymond/v2 v2.0.48/go.mod h1:lsgvL50kgt1ylcFJYZiULi5fjPBkkhNfj4KA0W54Z18=
|
github.com/mailgun/raymond/v2 v2.0.48/go.mod h1:lsgvL50kgt1ylcFJYZiULi5fjPBkkhNfj4KA0W54Z18=
|
||||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||||
|
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||||
|
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
@@ -44,6 +58,8 @@ github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6
|
|||||||
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
|
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
|
||||||
github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c h1:dAMKvw0MlJT1GshSTtih8C2gDs04w8dReiOGXrGLNoY=
|
github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c h1:dAMKvw0MlJT1GshSTtih8C2gDs04w8dReiOGXrGLNoY=
|
||||||
github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM=
|
github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM=
|
||||||
|
github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM=
|
||||||
|
github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
|
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
|
||||||
@@ -55,12 +71,17 @@ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf
|
|||||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
|
github.com/stretchr/testify v1.11.0 h1:ib4sjIrwZKxE5u/Japgo/7SJV3PvgjGiRNAvTVGqQl8=
|
||||||
github.com/tinylib/msgp v1.2.5 h1:WeQg1whrXRFiZusidTQqzETkRpGjFjcIhW6uqWH09po=
|
github.com/tinylib/msgp v1.2.5 h1:WeQg1whrXRFiZusidTQqzETkRpGjFjcIhW6uqWH09po=
|
||||||
github.com/tinylib/msgp v1.2.5/go.mod h1:ykjzy2wzgrlvpDCRc4LA8UXy6D8bzMSuAF3WD57Gok0=
|
github.com/tinylib/msgp v1.2.5/go.mod h1:ykjzy2wzgrlvpDCRc4LA8UXy6D8bzMSuAF3WD57Gok0=
|
||||||
|
github.com/tinylib/msgp v1.4.0 h1:SYOeDRiydzOw9kSiwdYp9UcBgPFtLU2WDHaJXyHruf8=
|
||||||
|
github.com/tinylib/msgp v1.4.0/go.mod h1:cvjFkb4RiC8qSBOPMGPSzSAx47nAsfhLVTCZZNuHv5o=
|
||||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||||
github.com/valyala/fasthttp v1.58.0 h1:GGB2dWxSbEprU9j0iMJHgdKYJVDyjrOwF9RE59PbRuE=
|
github.com/valyala/fasthttp v1.58.0 h1:GGB2dWxSbEprU9j0iMJHgdKYJVDyjrOwF9RE59PbRuE=
|
||||||
github.com/valyala/fasthttp v1.58.0/go.mod h1:SYXvHHaFp7QZHGKSHmoMipInhrI5StHrhDTYVEjK/Kw=
|
github.com/valyala/fasthttp v1.58.0/go.mod h1:SYXvHHaFp7QZHGKSHmoMipInhrI5StHrhDTYVEjK/Kw=
|
||||||
|
github.com/valyala/fasthttp v1.66.0 h1:M87A0Z7EayeyNaV6pfO3tUTUiYO0dZfEJnRGXTVNuyU=
|
||||||
|
github.com/valyala/fasthttp v1.66.0/go.mod h1:Y4eC+zwoocmXSVCB1JmhNbYtS7tZPRI2ztPB72EVObs=
|
||||||
github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8=
|
github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8=
|
||||||
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
|
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
|
||||||
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
||||||
@@ -69,15 +90,23 @@ github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZ
|
|||||||
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
||||||
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
|
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
|
||||||
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||||
|
golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI=
|
||||||
|
golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8=
|
||||||
golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo=
|
golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo=
|
||||||
golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM=
|
golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM=
|
||||||
|
golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I=
|
||||||
|
golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
|
||||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
|
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
|
||||||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
|
||||||
|
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||||
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
||||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||||
|
golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
|
||||||
|
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||||
|
|||||||
288
main.go
288
main.go
@@ -19,9 +19,12 @@ import (
|
|||||||
"mime/multipart"
|
"mime/multipart"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
"os/signal"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/caarlos0/env/v11"
|
"github.com/caarlos0/env/v11"
|
||||||
@@ -143,13 +146,40 @@ type App struct {
|
|||||||
db *sql.DB
|
db *sql.DB
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewApp(dbPath string) (*App, error) {
|
func (app *App) Close() error {
|
||||||
|
return app.db.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewApp(dbPath string, options map[string]any) (*App, error) {
|
||||||
config, err := ParseConfig()
|
config, err := ParseConfig()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
db, err := sql.Open("sqlite3", dbPath)
|
file, err := os.OpenFile(dbPath, os.O_RDWR|os.O_CREATE, 0644)
|
||||||
|
if err != nil {
|
||||||
|
if os.IsPermission(err) {
|
||||||
|
return nil, fmt.Errorf("file %s is not readable and writable: %v", dbPath, err)
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("failed to open file %s for read/write: %v", dbPath, err)
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
var connectionOpts string
|
||||||
|
for k, v := range options {
|
||||||
|
if connectionOpts != "" {
|
||||||
|
connectionOpts += "&"
|
||||||
|
}
|
||||||
|
|
||||||
|
connectionOpts += fmt.Sprintf("%s=%v", k, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
db, err := sql.Open("sqlite3", fmt.Sprintf("%s?%s", dbPath, connectionOpts))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = db.Ping()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -481,72 +511,64 @@ type Link struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type CategoryManager struct {
|
type CategoryManager struct {
|
||||||
db *sql.DB
|
db *sql.DB
|
||||||
Categories []Category
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewCategoryManager(db *sql.DB) (*CategoryManager, error) {
|
func NewCategoryManager(db *sql.DB) (*CategoryManager, error) {
|
||||||
rows, err := db.Query(`
|
return &CategoryManager{
|
||||||
|
db: db,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (manager *CategoryManager) GetCategories() []Category {
|
||||||
|
rows, err := manager.db.Query(`
|
||||||
SELECT id, name, icon
|
SELECT id, name, icon
|
||||||
FROM categories
|
FROM categories
|
||||||
ORDER BY id ASC
|
ORDER BY id ASC
|
||||||
`)
|
`)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil
|
||||||
}
|
}
|
||||||
defer rows.Close()
|
defer rows.Close()
|
||||||
|
|
||||||
var categories []Category
|
var categories []Category
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var cat Category
|
var cat Category
|
||||||
|
|
||||||
if err := rows.Scan(&cat.ID, &cat.Name, &cat.Icon); err != nil {
|
if err := rows.Scan(&cat.ID, &cat.Name, &cat.Icon); err != nil {
|
||||||
return nil, err
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
rows, err := db.Query(`
|
|
||||||
SELECT id, category_id, name, description, icon, url
|
|
||||||
FROM links
|
|
||||||
WHERE category_id = ?
|
|
||||||
ORDER BY id ASC
|
|
||||||
`, cat.ID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer rows.Close()
|
|
||||||
|
|
||||||
var links []Link
|
|
||||||
for rows.Next() {
|
|
||||||
var link Link
|
|
||||||
if err := rows.Scan(&link.ID, &link.CategoryID, &link.Name, &link.Description,
|
|
||||||
&link.Icon, &link.URL); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
links = append(links, link)
|
|
||||||
}
|
|
||||||
|
|
||||||
cat.Links = links
|
|
||||||
categories = append(categories, cat)
|
categories = append(categories, cat)
|
||||||
}
|
}
|
||||||
|
|
||||||
return &CategoryManager{
|
for i, cat := range categories {
|
||||||
db: db,
|
categories[i].Links = manager.GetLinks(cat.ID)
|
||||||
Categories: categories,
|
}
|
||||||
}, nil
|
|
||||||
|
return categories
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get Category by ID, returns nil if not found
|
// Get Category by ID, returns nil if not found
|
||||||
func (manager *CategoryManager) GetCategory(id int64) *Category {
|
func (manager *CategoryManager) GetCategory(id int64) *Category {
|
||||||
var category *Category
|
rows, err := manager.db.Query(`
|
||||||
|
SELECT id, name, icon
|
||||||
|
FROM categories
|
||||||
|
WHERE id = ?
|
||||||
|
`, id)
|
||||||
|
|
||||||
// probably potentially bad
|
if err != nil {
|
||||||
for _, cat := range manager.Categories {
|
return nil
|
||||||
if cat.ID == id {
|
}
|
||||||
category = &cat
|
defer rows.Close()
|
||||||
break
|
|
||||||
}
|
var cat Category
|
||||||
|
if err := rows.Scan(&cat.ID, &cat.Name, &cat.Icon); err != nil {
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return category
|
return &cat
|
||||||
}
|
}
|
||||||
|
|
||||||
func (manager *CategoryManager) CreateCategory(category Category) (*Category, error) {
|
func (manager *CategoryManager) CreateCategory(category Category) (*Category, error) {
|
||||||
@@ -569,11 +591,91 @@ func (manager *CategoryManager) CreateCategory(category Category) (*Category, er
|
|||||||
}
|
}
|
||||||
|
|
||||||
category.ID = categoryID
|
category.ID = categoryID
|
||||||
manager.Categories = append(manager.Categories, category)
|
|
||||||
|
|
||||||
return &category, nil
|
return &category, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (manager *CategoryManager) DeleteCategory(id int64) error {
|
||||||
|
rows, err := manager.db.Query(`
|
||||||
|
SELECT icon FROM categories WHERE id = ?
|
||||||
|
UNION
|
||||||
|
SELECT icon FROM links WHERE category_id = ?
|
||||||
|
`, id, id)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
var icons []string
|
||||||
|
for rows.Next() {
|
||||||
|
var icon string
|
||||||
|
if err := rows.Scan(&icon); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
icons = append(icons, icon)
|
||||||
|
}
|
||||||
|
|
||||||
|
tx, err := manager.db.Begin()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer tx.Rollback()
|
||||||
|
|
||||||
|
_, err = tx.Exec("DELETE FROM categories WHERE id = ?", id)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = tx.Exec("DELETE FROM links WHERE category_id = ?", id)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := tx.Commit(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, icon := range icons {
|
||||||
|
if icon == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.Remove(filepath.Join("public/", icon)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (manager *CategoryManager) GetLinks(categoryID int64) []Link {
|
||||||
|
rows, err := manager.db.Query(`
|
||||||
|
SELECT id, category_id, name, description, icon, url
|
||||||
|
FROM links
|
||||||
|
WHERE category_id = ?
|
||||||
|
ORDER BY id ASC
|
||||||
|
`, categoryID)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
var links []Link
|
||||||
|
for rows.Next() {
|
||||||
|
var link Link
|
||||||
|
if err := rows.Scan(&link.ID, &link.CategoryID, &link.Name, &link.Description,
|
||||||
|
&link.Icon, &link.URL); err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
links = append(links, link)
|
||||||
|
}
|
||||||
|
|
||||||
|
return links
|
||||||
|
}
|
||||||
|
|
||||||
func (manager *CategoryManager) CreateLink(db *sql.DB, link Link) (*Link, error) {
|
func (manager *CategoryManager) CreateLink(db *sql.DB, link Link) (*Link, error) {
|
||||||
var err error
|
var err error
|
||||||
insertLinkStmt, err = db.Prepare(`
|
insertLinkStmt, err = db.Prepare(`
|
||||||
@@ -592,20 +694,6 @@ func (manager *CategoryManager) CreateLink(db *sql.DB, link Link) (*Link, error)
|
|||||||
|
|
||||||
link.ID = linkID
|
link.ID = linkID
|
||||||
|
|
||||||
var cat *Category
|
|
||||||
for i, c := range manager.Categories {
|
|
||||||
if c.ID == link.CategoryID {
|
|
||||||
cat = &manager.Categories[i]
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if cat == nil {
|
|
||||||
return nil, fmt.Errorf("category not found")
|
|
||||||
}
|
|
||||||
|
|
||||||
cat.Links = append(cat.Links, link)
|
|
||||||
|
|
||||||
return &link, nil
|
return &link, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -673,9 +761,7 @@ func getWeatherIcon(iconId string) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
if err := godotenv.Load(); err != nil {
|
godotenv.Load()
|
||||||
fmt.Println("No .env file found, using default values")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
@@ -683,11 +769,29 @@ func main() {
|
|||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
app, err := NewApp("passport.db?cache=shared&mode=rwc&_journal_mode=WAL")
|
dbPath, err := filepath.Abs("passport.db")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
app, err := NewApp(dbPath, map[string]any{
|
||||||
|
"cache": "shared",
|
||||||
|
"mode": "rwc",
|
||||||
|
"_journal_mode": "WAL",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
defer app.Close()
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
c := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(c, syscall.SIGINT, syscall.SIGTERM)
|
||||||
|
<-c
|
||||||
|
app.Close()
|
||||||
|
os.Exit(0)
|
||||||
|
}()
|
||||||
|
|
||||||
templatesDir, err := fs.Sub(embeddedAssets, "templates")
|
templatesDir, err := fs.Sub(embeddedAssets, "templates")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
@@ -727,6 +831,11 @@ func main() {
|
|||||||
|
|
||||||
router.Use(helmet.New(helmet.ConfigDefault))
|
router.Use(helmet.New(helmet.ConfigDefault))
|
||||||
|
|
||||||
|
// redirect /favicon.ico to /assets/favicon.ico
|
||||||
|
router.Get("/favicon.ico", func(c fiber.Ctx) error {
|
||||||
|
return c.Redirect().To("/assets/favicon.ico")
|
||||||
|
})
|
||||||
|
|
||||||
router.Use("/", static.New("./public", static.Config{
|
router.Use("/", static.New("./public", static.Config{
|
||||||
Browse: false,
|
Browse: false,
|
||||||
MaxAge: 31536000,
|
MaxAge: 31536000,
|
||||||
@@ -741,7 +850,7 @@ func main() {
|
|||||||
renderData := fiber.Map{
|
renderData := fiber.Map{
|
||||||
"SearchProviderURL": app.Config.SearchProvider.URL,
|
"SearchProviderURL": app.Config.SearchProvider.URL,
|
||||||
"SearchParam": app.Config.SearchProvider.Query,
|
"SearchParam": app.Config.SearchProvider.Query,
|
||||||
"Categories": app.CategoryManager.Categories,
|
"Categories": app.CategoryManager.GetCategories(),
|
||||||
}
|
}
|
||||||
|
|
||||||
if app.Config.WeatherEnabled {
|
if app.Config.WeatherEnabled {
|
||||||
@@ -816,12 +925,13 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return c.Render("views/admin/index", fiber.Map{
|
return c.Render("views/admin/index", fiber.Map{
|
||||||
"Categories": app.CategoryManager.Categories,
|
"Categories": app.CategoryManager.GetCategories(),
|
||||||
}, "layouts/main")
|
}, "layouts/main")
|
||||||
})
|
})
|
||||||
|
|
||||||
api := router.Group("/api")
|
api := router.Group("/api")
|
||||||
{
|
{
|
||||||
|
// all API routes require admin auth. No user needs to make api requests since the site is SSR
|
||||||
api.Use(func(c fiber.Ctx) error {
|
api.Use(func(c fiber.Ctx) error {
|
||||||
if c.Locals("IsAdmin") == nil {
|
if c.Locals("IsAdmin") == nil {
|
||||||
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"message": "Unauthorized"})
|
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"message": "Unauthorized"})
|
||||||
@@ -962,63 +1072,25 @@ func main() {
|
|||||||
api.Delete("/links/:id", func(c fiber.Ctx) error {
|
api.Delete("/links/:id", func(c fiber.Ctx) error {
|
||||||
id := c.Params("id")
|
id := c.Params("id")
|
||||||
|
|
||||||
app.CategoryManager.DeleteLink(id)
|
err = app.CategoryManager.DeleteLink(id)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
return c.SendStatus(fiber.StatusOK)
|
return c.SendStatus(fiber.StatusOK)
|
||||||
})
|
})
|
||||||
|
|
||||||
api.Delete("/categories/:id", func(c fiber.Ctx) error {
|
api.Delete("/categories/:id", func(c fiber.Ctx) error {
|
||||||
id := c.Params("id")
|
// id = parseInt(c.Params("id"))
|
||||||
|
id, err := strconv.ParseInt(c.Params("id"), 10, 64)
|
||||||
rows, err := app.db.Query(`
|
|
||||||
SELECT icon FROM categories WHERE id = ?
|
|
||||||
UNION
|
|
||||||
SELECT icon FROM links WHERE category_id = ?
|
|
||||||
`, id, id)
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
defer rows.Close()
|
err = app.CategoryManager.DeleteCategory(id)
|
||||||
|
|
||||||
var icons []string
|
|
||||||
for rows.Next() {
|
|
||||||
var icon string
|
|
||||||
if err := rows.Scan(&icon); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
icons = append(icons, icon)
|
|
||||||
}
|
|
||||||
|
|
||||||
tx, err := app.db.Begin()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer tx.Rollback()
|
|
||||||
|
|
||||||
_, err = tx.Exec("DELETE FROM categories WHERE id = ?", id)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = tx.Exec("DELETE FROM links WHERE category_id = ?", id)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := tx.Commit(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, icon := range icons {
|
|
||||||
if icon == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := os.Remove(filepath.Join("public/", icon)); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.SendStatus(fiber.StatusOK)
|
return c.SendStatus(fiber.StatusOK)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<section class="flex justify-center w-full">
|
<section class="flex justify-center w-full transition-[filter] duration-300 ease-[cubic-bezier(0.45,_0,_0.55,_1)]">
|
||||||
<div class="w-full sm:w-4/5 p-2.5">
|
<div class="w-full sm:w-4/5 p-2.5">
|
||||||
{{#each Categories}}
|
{{#each Categories}}
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
@@ -31,7 +31,7 @@
|
|||||||
</svg></button>
|
</svg></button>
|
||||||
</div>
|
</div>
|
||||||
{{/each}}
|
{{/each}}
|
||||||
<div onclick="addLink({{this.ID}})"
|
<div onclick="openLinkModal({{this.ID}})"
|
||||||
class="rounded-2xl border border-dashed border-[#656565] p-2.5 flex flex-row items-center shadow-md hover:shadow-xl transition-[shadow,transform] ease-[cubic-bezier(0.16,1,0.3,1)] pointer-cursor select-none cursor-pointer">
|
class="rounded-2xl border border-dashed border-[#656565] p-2.5 flex flex-row items-center shadow-md hover:shadow-xl transition-[shadow,transform] ease-[cubic-bezier(0.16,1,0.3,1)] pointer-cursor select-none cursor-pointer">
|
||||||
<svg class="mr-2" xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 24 24">
|
<svg class="mr-2" xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 24 24">
|
||||||
<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
|
<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
|
||||||
@@ -48,14 +48,15 @@
|
|||||||
<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||||
d="M12 5v14m-7-7h14" />
|
d="M12 5v14m-7-7h14" />
|
||||||
</svg>
|
</svg>
|
||||||
<h2 onclick="addCategory()" class="text-[#656565] underline decoration-dashed cursor-pointer">
|
<h2 onclick="openCategoryModal()" class="text-[#656565] underline decoration-dashed cursor-pointer">
|
||||||
Add a new category
|
Add a new category
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<div id="linkModal" class="hidden absolute top-0 left-0 bottom-0 right-0 bg-[#00000070] justify-center items-center">
|
<div id="linkModal"
|
||||||
<div class="bg-[#151316] rounded-xl overflow-hidden w-fit p-4">
|
class="flex modal-bg fixed top-0 left-0 bottom-0 right-0 bg-[#00000070] justify-center items-center">
|
||||||
|
<div class="bg-[#151316] rounded-xl overflow-hidden w-fit p-4 modal">
|
||||||
<h3>Add A link</h3>
|
<h3>Add A link</h3>
|
||||||
<form id="link-form" action="/api/links" method="post" class="flex flex-col gap-y-3 my-2">
|
<form id="link-form" action="/api/links" method="post" class="flex flex-col gap-y-3 my-2">
|
||||||
<div>
|
<div>
|
||||||
@@ -78,7 +79,8 @@
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label for="linkIcon">Icon</label>
|
<label for="linkIcon">Icon</label>
|
||||||
<input class="w-full text-white" type="file" name="icon" id="linkIcon" accept="image/*" />
|
<input class="w-full text-white py-2 px-4 rounded bg-[#1C1C21] border border-[#56565b]/30" type="file"
|
||||||
|
name="icon" id="linkIcon" accept="image/*" />
|
||||||
</div>
|
</div>
|
||||||
<button class="px-4 py-2 rounded-md w-full bg-[#8A42FF] text-white border-0" type="submit">Add</button>
|
<button class="px-4 py-2 rounded-md w-full bg-[#8A42FF] text-white border-0" type="submit">Add</button>
|
||||||
</form>
|
</form>
|
||||||
@@ -86,10 +88,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="categoryModal"
|
<div id="categoryModal"
|
||||||
class="hidden absolute top-0 left-0 bottom-0 right-0 bg-[#00000070] justify-center items-center">
|
class="flex modal-bg fixed top-0 left-0 bottom-0 right-0 bg-[#00000070] justify-center items-center">
|
||||||
<div class="bg-[#151316] rounded-xl overflow-hidden w-fit p-4">
|
<div class="bg-[#151316] rounded-xl overflow-hidden w-fit p-4 modal">
|
||||||
<h3>Create A category</h3>
|
<h3>Create A category</h3>
|
||||||
<form id="category-form" action="/api/categories" method="post" class="flex flex-col gap-y-3 my-2">
|
<form id="category-form" action="/api/categories" method="post"
|
||||||
|
class="flex flex-col gap-y-3 my-2 [&>div]:flex [&>div]:flex-col [&>div]:gap-1">
|
||||||
<div>
|
<div>
|
||||||
<label for="categoryName">Name</label>
|
<label for="categoryName">Name</label>
|
||||||
<input
|
<input
|
||||||
@@ -98,7 +101,8 @@
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label for="linkIcon">Icon</label>
|
<label for="linkIcon">Icon</label>
|
||||||
<input class="w-full text-white" type="file" name="icon" id="linkIcon" accept=".svg" />
|
<input class="w-full text-white py-2 px-4 rounded bg-[#1C1C21] border border-[#56565b]/30" type="file"
|
||||||
|
name="icon" id="linkIcon" accept=".svg" />
|
||||||
</div>
|
</div>
|
||||||
<button class="px-4 py-2 rounded-md w-full bg-[#8A42FF] text-white border-0" type="submit">Create</button>
|
<button class="px-4 py-2 rounded-md w-full bg-[#8A42FF] text-white border-0" type="submit">Create</button>
|
||||||
</form>
|
</form>
|
||||||
@@ -108,19 +112,41 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
// idfk what this variable capitalization is, it's a mess
|
// idfk what this variable capitalization is, it's a mess
|
||||||
let linkModal = document.getElementById("linkModal");
|
let linkModalBg = document.getElementById("linkModal");
|
||||||
let categoryModal = document.getElementById("categoryModal");
|
let linkModal = linkModalBg.querySelector("div");
|
||||||
|
let categoryModalBg = document.getElementById("categoryModal");
|
||||||
|
let categoryModal = categoryModalBg.querySelector("div");
|
||||||
|
let pageElement = document.querySelector("section");
|
||||||
let targetCategoryID = null;
|
let targetCategoryID = null;
|
||||||
|
|
||||||
function addCategory() {
|
function openCategoryModal() {
|
||||||
categoryModal.classList.remove("hidden");
|
pageElement.style.filter = "blur(20px)";
|
||||||
categoryModal.classList.add("flex");
|
|
||||||
|
categoryModalBg.classList.add("is-visible");
|
||||||
|
categoryModal.classList.add("is-visible");
|
||||||
}
|
}
|
||||||
|
|
||||||
function addLink(categoryID) {
|
function closeCategoryModal() {
|
||||||
|
pageElement.style.filter = "";
|
||||||
|
|
||||||
|
categoryModalBg.classList.remove("is-visible");
|
||||||
|
categoryModal.classList.remove("is-visible");
|
||||||
|
}
|
||||||
|
|
||||||
|
function openLinkModal(categoryID) {
|
||||||
targetCategoryID = categoryID;
|
targetCategoryID = categoryID;
|
||||||
linkModal.classList.remove("hidden");
|
|
||||||
linkModal.classList.add("flex");
|
pageElement.style.filter = "blur(20px)";
|
||||||
|
|
||||||
|
linkModalBg.classList.add("is-visible");
|
||||||
|
linkModal.classList.add("is-visible");
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeLinkModal() {
|
||||||
|
pageElement.style.filter = "";
|
||||||
|
|
||||||
|
linkModalBg.classList.remove("is-visible");
|
||||||
|
linkModal.classList.remove("is-visible");
|
||||||
}
|
}
|
||||||
|
|
||||||
async function deleteLink(linkID) {
|
async function deleteLink(linkID) {
|
||||||
@@ -155,8 +181,7 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (res.status === 201) {
|
if (res.status === 201) {
|
||||||
linkModal.classList.add("hidden");
|
closeLinkModal();
|
||||||
linkModal.classList.remove("flex");
|
|
||||||
document.getElementById("link-form").reset();
|
document.getElementById("link-form").reset();
|
||||||
location.reload();
|
location.reload();
|
||||||
} else {
|
} else {
|
||||||
@@ -175,8 +200,7 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (res.status === 201) {
|
if (res.status === 201) {
|
||||||
categoryModal.classList.add("hidden");
|
closeCategoryModal()
|
||||||
categoryModal.classList.remove("flex");
|
|
||||||
document.getElementById("category-form").reset();
|
document.getElementById("category-form").reset();
|
||||||
location.reload();
|
location.reload();
|
||||||
} else {
|
} else {
|
||||||
@@ -185,19 +209,47 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
linkModal.addEventListener("click", (event) => {
|
linkModalBg.addEventListener("click", (event) => {
|
||||||
if (event.target === linkModal) {
|
if (event.target === linkModalBg) {
|
||||||
targetCategoryID = null;
|
targetCategoryID = null;
|
||||||
linkModal.classList.add("hidden");
|
closeLinkModal();
|
||||||
linkModal.classList.remove("flex");
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
categoryModal.addEventListener("click", (event) => {
|
categoryModalBg.addEventListener("click", (event) => {
|
||||||
if (event.target === categoryModal) {
|
if (event.target === categoryModalBg) {
|
||||||
targetCategoryID = null;
|
targetCategoryID = null;
|
||||||
categoryModal.classList.add("hidden");
|
closeCategoryModal();
|
||||||
categoryModal.classList.remove("flex");
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.modal-bg {
|
||||||
|
visibility: hidden;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.3s ease, visibility 0s 0.3s;
|
||||||
|
transition-timing-function: cubic-bezier(0.45, 0, 0.55, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-bg.is-visible {
|
||||||
|
visibility: visible;
|
||||||
|
opacity: 1;
|
||||||
|
transition-delay: 0s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(20px) scale(0.95);
|
||||||
|
|
||||||
|
transition: opacity 0.3s ease, transform 0.3s ease;
|
||||||
|
transition-timing-function: cubic-bezier(0.45, 0, 0.55, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal.is-visible {
|
||||||
|
opacity: 1;
|
||||||
|
visibility: visible;
|
||||||
|
transform: translateY(0) scale(1);
|
||||||
|
transition-delay: 0s;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -71,7 +71,7 @@
|
|||||||
<div class="w-full sm:w-4/5 p-2.5">
|
<div class="w-full sm:w-4/5 p-2.5">
|
||||||
{{#each Categories}}
|
{{#each Categories}}
|
||||||
<div class="flex items-center w-fit">
|
<div class="flex items-center w-fit">
|
||||||
<img class="object-contain mr-2 select-none" width="32" height="32" draggable="false" alt="{{this.Name}}"
|
<img class="object-contain mr-2 select-none text-white" width="32" height="32" draggable="false" alt="{{this.Name}}"
|
||||||
src="{{this.Icon}}" />
|
src="{{this.Icon}}" />
|
||||||
<h2 class="capitalize w-fit">{{this.Name}}</h2>
|
<h2 class="capitalize w-fit">{{this.Name}}</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -11,7 +11,8 @@
|
|||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "go generate; PASSPORT_DEV_MODE=true go run main.go",
|
"dev": "go generate; PASSPORT_DEV_MODE=true go run main.go",
|
||||||
"build": "go generate && go build -tags netgo,prod -o passport"
|
"build": "go generate && go build -tags netgo,prod -ldflags=\"-w -s\" -o passport"
|
||||||
},
|
},
|
||||||
"pattern": "**/*.go,templates/views/**/*.hbs,styles/**/*.css,assets/**/*.{svg,png,jpg,jpeg,webp,woff2,ttf,otf,eot,ico,gif,webp}"
|
"pattern": "**/*.go,templates/views/**/*.hbs,styles/**/*.css,assets/**/*.{svg,png,jpg,jpeg,webp,woff2,ttf,otf,eot,ico,gif,webp}",
|
||||||
|
"shutdown_signal": "SIGINT"
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user