├── .github ├── dependabot.yml └── workflows │ └── release.yaml ├── .gitignore ├── .releaserc.json ├── LICENSE ├── README.md ├── api ├── Dockerfile ├── docs │ ├── docs.go │ ├── swagger.json │ └── swagger.yaml ├── go.mod ├── go.sum └── main.go ├── doc ├── default-icon.svg ├── screenshot_1.png ├── screenshot_2.png ├── screenshot_3.png └── screenshot_4.png ├── docker-compose-build.yaml ├── docker-compose.yaml └── ui ├── .gitignore ├── .npmrc ├── 01-sub.sh ├── Dockerfile ├── package.json ├── pnpm-lock.yaml ├── src ├── app.css ├── app.d.ts ├── app.html ├── icons │ └── fox.svg ├── lib │ ├── stores.ts │ └── types.ts └── routes │ ├── +layout.svelte │ ├── +layout.ts │ ├── +page.svelte │ └── layout.svelte.ts ├── static ├── favicon.png ├── icons │ ├── android-chrome-192x192.png │ ├── android-chrome-512x512.png │ └── apple-touch-icon.png └── manifest.webmanifest ├── svelte.config.js ├── tsconfig.json └── vite.config.ts /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "gomod" 4 | directory: "/api" 5 | commit-message: 6 | prefix: "build: " 7 | schedule: 8 | interval: "weekly" 9 | - package-ecosystem: "docker" 10 | directory: "/api" 11 | commit-message: 12 | prefix: "build: " 13 | schedule: 14 | interval: "weekly" 15 | - package-ecosystem: "npm" 16 | directory: "/ui" 17 | commit-message: 18 | prefix: "build: " 19 | schedule: 20 | interval: "weekly" 21 | - package-ecosystem: "docker" 22 | directory: "/ui" 23 | commit-message: 24 | prefix: "build: " 25 | schedule: 26 | interval: "weekly" 27 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | push: 4 | branches: 5 | - main 6 | 7 | jobs: 8 | release: 9 | runs-on: ubuntu-latest 10 | permissions: 11 | contents: write 12 | issues: write 13 | pull-requests: write 14 | id-token: write 15 | outputs: 16 | VERSION_NUMBER: ${{ steps.output.outputs.VERSION_NUMBER }} 17 | CI_CREATE_IMAGE: ${{ steps.output.outputs.CI_CREATE_IMAGE }} 18 | steps: 19 | - uses: actions/checkout@v4 20 | - name: Install pnpm 21 | uses: pnpm/action-setup@v4 22 | with: 23 | version: 10 24 | - name: Use Node.js 20 25 | uses: actions/setup-node@v4 26 | with: 27 | node-version: 20 28 | - name: Run semantic-release 29 | env: 30 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 31 | run: pnpm --package semantic-release@23 --package @semantic-release/exec --package @semantic-release/changelog --package conventional-changelog-conventionalcommits@7.0.2 dlx semantic-release 32 | - name: Generate output 33 | id: output 34 | run: test -f "release.env" && cat release.env >> "$GITHUB_OUTPUT" || exit 0 35 | docker: 36 | runs-on: ubuntu-latest 37 | needs: release 38 | if: ${{ needs.release.outputs.CI_CREATE_IMAGE == 'true' }} 39 | steps: 40 | - name: Set QEMU 41 | uses: docker/setup-qemu-action@v3 42 | - name: Set Buildx 43 | uses: docker/setup-buildx-action@v3 44 | - name: Login to Docker Hub 45 | uses: docker/login-action@v3 46 | with: 47 | username: ${{ vars.DOCKERHUB_USERNAME }} 48 | password: ${{ secrets.DOCKERHUB_TOKEN }} 49 | - uses: actions/checkout@v4 50 | - name: Build and push UI 51 | uses: docker/build-push-action@v6 52 | with: 53 | context: ./ui 54 | push: true 55 | platforms: linux/amd64,linux/arm64 56 | tags: "${{ vars.DOCKERHUB_USERNAME }}/thrifty-ui:latest, ${{ vars.DOCKERHUB_USERNAME }}/thrifty-ui:${{ needs.release.outputs.VERSION_NUMBER }}" 57 | - name: Build and push API 58 | uses: docker/build-push-action@v6 59 | with: 60 | context: ./api 61 | push: true 62 | platforms: linux/amd64,linux/arm64 63 | tags: "${{ vars.DOCKERHUB_USERNAME }}/thrifty-api:latest, ${{ vars.DOCKERHUB_USERNAME }}/thrifty-api:${{ needs.release.outputs.VERSION_NUMBER }}" 64 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .pnpm-store 3 | .scannerwork 4 | sonar-project.properties 5 | data 6 | **/.dockerignore 7 | **.sqlite 8 | **/.DS_Store 9 | -------------------------------------------------------------------------------- /.releaserc.json: -------------------------------------------------------------------------------- 1 | { 2 | "branches": [ 3 | "main" 4 | ], 5 | "plugins": [ 6 | [ 7 | "@semantic-release/commit-analyzer", 8 | { 9 | "preset": "conventionalcommits" 10 | } 11 | ], 12 | [ 13 | "@semantic-release/release-notes-generator", 14 | { 15 | "preset": "conventionalcommits", 16 | "presetConfig": { 17 | "types": [ 18 | { 19 | "type": "feat", 20 | "section": "✨ Features ✨", 21 | "hidden": false 22 | }, 23 | { 24 | "type": "fix", 25 | "section": "🐞 Bug fixes 🐞", 26 | "hidden": false 27 | }, 28 | { 29 | "type": "perf", 30 | "section": "\uD83D\uDE80 Performance improvements \uD83D\uDE80", 31 | "hidden": false 32 | }, 33 | { 34 | "type": "ci", 35 | "section": "\uD83D\uDEA7 CI changes \uD83D\uDEA7", 36 | "hidden": false 37 | }, 38 | { 39 | "type": "build", 40 | "section": "\uD83C\uDFD7️ Build tool changes \uD83C\uDFD7️", 41 | "hidden": false 42 | }, 43 | { 44 | "type": "docs", 45 | "section": "\uD83D\uDDCE Documentation \uD83D\uDDCE", 46 | "hidden": false 47 | }, 48 | { 49 | "type": "refactor", 50 | "section": "\uD83D\uDD27 Refactoring \uD83D\uDD27", 51 | "hidden": false 52 | }, 53 | { 54 | "type": "test", 55 | "section": "\uD83D\uDC4C Test cases \uD83D\uDC4C", 56 | "hidden": false 57 | }, 58 | { 59 | "type": "chore", 60 | "section": "ℹ\uFE0F Uncategorized changes ℹ\uFE0F", 61 | "hidden": false 62 | }, 63 | { 64 | "type": "revert", 65 | "section": "\uD83D\uDCA3 Reverts \uD83D\uDCA3", 66 | "hidden": false 67 | } 68 | ] 69 | } 70 | } 71 | ], 72 | "@semantic-release/changelog", 73 | [ 74 | "@semantic-release/github", 75 | { 76 | "assets": [ 77 | { 78 | "path": "CHANGELOG.md", 79 | "label": "Changes" 80 | } 81 | ] 82 | } 83 | ], 84 | [ 85 | "@semantic-release/exec", 86 | { 87 | "analyzeCommitsCmd": "echo \"VERSION_NUMBER=${lastRelease.version}\" > release.env; echo \"CI_CREATE_IMAGE=false\" >> release.env", 88 | "generateNotesCmd": "echo \"VERSION_NUMBER=${nextRelease.version}\" > release.env; echo \"CI_CREATE_IMAGE=true\" >> release.env" 89 | } 90 | ] 91 | ] 92 | } 93 | 94 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 2-Clause License 2 | 3 | Copyright (c) 2025, tiehfood 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | 1. Redistributions of source code must retain the above copyright notice, this 9 | list of conditions and the following disclaimer. 10 | 11 | 2. Redistributions in binary form must reproduce the above copyright notice, 12 | this list of conditions and the following disclaimer in the documentation 13 | and/or other materials provided with the distribution. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 16 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 17 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 19 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 20 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 21 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 22 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 23 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 24 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Icon 3 |

4 |

5 | 6 | Release Version 7 | 8 | 9 | Docker Pulls 10 | 11 | 12 | License 13 | 14 | 15 | Stars 16 | 17 |

18 | 19 | # Thrifty 20 | 21 | Please be gracious with me, this is my first ever app 🙈. Thrifty is a simple web application that helps you manage your income and expenses. 22 | It's focused on simplicity and is not aimed to track every single penny you spend. 23 | The idea is to get a rough overview of your monthly cash flow and what's left to spend. 24 | 25 | Features: 26 | - Add income and expenses 27 | - Edit existing entries 28 | - Delete entries 29 | - Support for SVG icons (default: dollar) 30 | - Two rows for income and expenses (collapse into single one on smaller devices) 31 | - API documentation at `/swagger/index.html` 32 | - Currency configurable via environment variable 33 | 34 | Frameworks used: 35 | 36 | - __UI__: SvelteKit and Flowbite 37 | - __API__: golang 38 | 39 | ## Screenshots 40 | | ![Screenshot1](doc/screenshot_1.png) | ![Screenshot2](doc/screenshot_2.png) | 41 | |:------------------------------------:|:------------------------------------:| 42 | | ![Screenshot3](doc/screenshot_3.png) | ![Screenshot4](doc/screenshot_4.png) | 43 | 44 | ## Running the app 45 | 46 | Every release since 1.2.0 should create a docker image on DockerHub. 47 | Use [docker-compose.yaml](docker-compose.yaml) to run the app with the latest version. 48 | 49 | ```bash 50 | # run the app 51 | docker compose -f docker-compose.yaml up -d 52 | ``` 53 | The app should now be available at `http://localhost:9090`. 54 | 55 | If you want to build the image yourself, you can use the following commands. 56 | ```bash 57 | # run the app 58 | docker compose -f docker-compose-build.yaml build 59 | 60 | # run the app 61 | docker compose -f docker-compose-build.yaml up -d 62 | ``` 63 | 64 | ### Tips 65 | #### Want to use `$` as the currency symbol? 66 | >Set `CURRENCY_ISO=USD` for the UI in the docker compose file and restart the UI container. 67 | #### How about other currencies? 68 | > The step above should apply for any other currency ISO-code in this list: [ISO 4217](https://de.wikipedia.org/wiki/ISO_4217) 69 | #### Where is the data stored? 70 | > The data is stored in a SQLite database in a docker volume. You can also use a custom path in the [docker-compose.yaml](docker-compose.yaml) to expose the database. 71 | #### I need a docker image for a different architecture. 72 | > Currently amd64 and arm64 builds are created. For other architectures you can build the image yourself like described above. 73 | #### Want to use the app without a reverse proxy? 74 | > The default internal port for both containers is port 8080. 75 | > If you want to use docker compose and just want to change the external port of the API container, you could add the environment variable `LOCAL_API_PORT=8080` to the UI container. 76 | > Change it to the desired external port of your docker compose file. 77 | 78 | > If you want to use the containers for example in host network mode, you could also change the port the API is listening on. 79 | > Set the variable `PORT=8081` on the API container to change the internal port to 8081. 80 | > Depending on your setup you might need to set `LOCAL_API_PORT` to the same value. 81 | 82 | > You can also set `LOACL_API_HOSTNAME` and `LOCAL_API_PROTOCOL` if needed. 83 | #### Want to have a single column all the time? 84 | > Set the variable `USE_SINGLE_COLUMN=true` in the UI container to always use a single column layout. 85 | 86 | ## Developing 87 | 88 | ### Frontend 89 | Install node and node modules. 90 | Running locally requires you to change the API URL in [+page.svelte](ui/src/routes/+page.svelte). 91 | Change `currentProtocol`, `currentHostname` and `currentPort` in the _onMount_ method to where your API development is running on. 92 | ```bash 93 | cd ui 94 | 95 | # install dependencies 96 | pnpm i 97 | 98 | # run ui 99 | pnpm dev 100 | ``` 101 | You could also use regular npm instead of pnpm. 102 | ### Backend 103 | Install golang and run the following commands. 104 | ```bash 105 | cd api 106 | 107 | # install dependencies 108 | go get . 109 | 110 | # install swag (optional) 111 | go install github.com/swaggo/swag/cmd/swag@latest 112 | 113 | # generate swagger documentation (optional) 114 | swag init 115 | 116 | # run api 117 | go run . 118 | ``` 119 | 120 | ## Credits 121 | - [NumberFlow](https://number-flow.barvian.me/svelte) 122 | - [Swag](https://github.com/swaggo/swag) -------------------------------------------------------------------------------- /api/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.23.6-alpine AS builder 2 | 3 | COPY . /app 4 | WORKDIR /app 5 | 6 | ENV GOOS=linux 7 | RUN go get .; \ 8 | go install github.com/swaggo/swag/cmd/swag@latest; \ 9 | swag init; \ 10 | go build -o thrifty .; \ 11 | chmod +x thrifty; 12 | 13 | FROM scratch 14 | COPY --from=builder /app/thrifty /thrifty 15 | ENTRYPOINT ["/thrifty"] 16 | -------------------------------------------------------------------------------- /api/docs/docs.go: -------------------------------------------------------------------------------- 1 | // Package docs Code generated by swaggo/swag. DO NOT EDIT 2 | package docs 3 | 4 | import "github.com/swaggo/swag" 5 | 6 | const docTemplate = `{ 7 | "schemes": {{ marshal .Schemes }}, 8 | "swagger": "2.0", 9 | "info": { 10 | "description": "{{escape .Description}}", 11 | "title": "{{.Title}}", 12 | "contact": {}, 13 | "version": "{{.Version}}" 14 | }, 15 | "host": "{{.Host}}", 16 | "basePath": "{{.BasePath}}", 17 | "paths": { 18 | "/flows": { 19 | "get": { 20 | "description": "Get all flows in a JSON object", 21 | "produces": [ 22 | "application/json" 23 | ], 24 | "tags": [ 25 | "Flows" 26 | ], 27 | "summary": "Get all flows", 28 | "responses": { 29 | "200": { 30 | "description": "OK", 31 | "schema": { 32 | "type": "array", 33 | "items": { 34 | "$ref": "#/definitions/main.Flow" 35 | } 36 | } 37 | }, 38 | "500": { 39 | "description": "Internal Server Error", 40 | "schema": { 41 | "$ref": "#/definitions/main.HTTPError" 42 | } 43 | } 44 | } 45 | }, 46 | "post": { 47 | "description": "Get all flows in a JSON object", 48 | "produces": [ 49 | "application/json" 50 | ], 51 | "tags": [ 52 | "Flows" 53 | ], 54 | "summary": "Add new flow", 55 | "parameters": [ 56 | { 57 | "description": "Flow object, id is set by the server and could be omitted", 58 | "name": "request", 59 | "in": "body", 60 | "required": true, 61 | "schema": { 62 | "$ref": "#/definitions/main.Flow" 63 | } 64 | } 65 | ], 66 | "responses": { 67 | "200": { 68 | "description": "OK", 69 | "schema": { 70 | "$ref": "#/definitions/main.Flow" 71 | } 72 | }, 73 | "500": { 74 | "description": "Internal Server Error", 75 | "schema": { 76 | "$ref": "#/definitions/main.HTTPError" 77 | } 78 | } 79 | } 80 | } 81 | }, 82 | "/flows/{id}": { 83 | "delete": { 84 | "description": "Delete an existing flow", 85 | "produces": [ 86 | "application/json" 87 | ], 88 | "tags": [ 89 | "Flows" 90 | ], 91 | "summary": "Delete a flow", 92 | "parameters": [ 93 | { 94 | "type": "integer", 95 | "description": "Flow id", 96 | "name": "id", 97 | "in": "path", 98 | "required": true 99 | } 100 | ], 101 | "responses": { 102 | "200": { 103 | "description": "OK", 104 | "schema": { 105 | "$ref": "#/definitions/main.HTTPResponse" 106 | } 107 | }, 108 | "500": { 109 | "description": "Internal Server Error", 110 | "schema": { 111 | "$ref": "#/definitions/main.HTTPError" 112 | } 113 | } 114 | } 115 | }, 116 | "patch": { 117 | "description": "Update an existing flow with new data", 118 | "produces": [ 119 | "application/json" 120 | ], 121 | "tags": [ 122 | "Flows" 123 | ], 124 | "summary": "Update existing flow", 125 | "parameters": [ 126 | { 127 | "type": "integer", 128 | "description": "Flow id", 129 | "name": "id", 130 | "in": "path", 131 | "required": true 132 | }, 133 | { 134 | "description": "Flow object, id is ignored and could be omitted", 135 | "name": "request", 136 | "in": "body", 137 | "required": true, 138 | "schema": { 139 | "$ref": "#/definitions/main.Flow" 140 | } 141 | } 142 | ], 143 | "responses": { 144 | "200": { 145 | "description": "OK", 146 | "schema": { 147 | "$ref": "#/definitions/main.Flow" 148 | } 149 | }, 150 | "500": { 151 | "description": "Internal Server Error", 152 | "schema": { 153 | "$ref": "#/definitions/main.HTTPError" 154 | } 155 | } 156 | } 157 | } 158 | } 159 | }, 160 | "definitions": { 161 | "main.Flow": { 162 | "type": "object", 163 | "properties": { 164 | "amount": { 165 | "type": "number" 166 | }, 167 | "description": { 168 | "type": "string" 169 | }, 170 | "icon": { 171 | "type": "string" 172 | }, 173 | "id": { 174 | "type": "string" 175 | }, 176 | "name": { 177 | "type": "string" 178 | } 179 | } 180 | }, 181 | "main.HTTPError": { 182 | "type": "object", 183 | "properties": { 184 | "error": { 185 | "type": "string" 186 | } 187 | } 188 | }, 189 | "main.HTTPResponse": { 190 | "type": "object", 191 | "properties": { 192 | "ok": { 193 | "type": "string" 194 | } 195 | } 196 | } 197 | } 198 | }` 199 | 200 | // SwaggerInfo holds exported Swagger Info so clients can modify it 201 | var SwaggerInfo = &swag.Spec{ 202 | Version: "", 203 | Host: "", 204 | BasePath: "", 205 | Schemes: []string{}, 206 | Title: "", 207 | Description: "", 208 | InfoInstanceName: "swagger", 209 | SwaggerTemplate: docTemplate, 210 | LeftDelim: "{{", 211 | RightDelim: "}}", 212 | } 213 | 214 | func init() { 215 | swag.Register(SwaggerInfo.InstanceName(), SwaggerInfo) 216 | } 217 | -------------------------------------------------------------------------------- /api/docs/swagger.json: -------------------------------------------------------------------------------- 1 | { 2 | "swagger": "2.0", 3 | "info": { 4 | "contact": {} 5 | }, 6 | "paths": { 7 | "/flows": { 8 | "get": { 9 | "description": "Get all flows in a JSON object", 10 | "produces": [ 11 | "application/json" 12 | ], 13 | "tags": [ 14 | "Flows" 15 | ], 16 | "summary": "Get all flows", 17 | "responses": { 18 | "200": { 19 | "description": "OK", 20 | "schema": { 21 | "type": "array", 22 | "items": { 23 | "$ref": "#/definitions/main.Flow" 24 | } 25 | } 26 | }, 27 | "500": { 28 | "description": "Internal Server Error", 29 | "schema": { 30 | "$ref": "#/definitions/main.HTTPError" 31 | } 32 | } 33 | } 34 | }, 35 | "post": { 36 | "description": "Get all flows in a JSON object", 37 | "produces": [ 38 | "application/json" 39 | ], 40 | "tags": [ 41 | "Flows" 42 | ], 43 | "summary": "Add new flow", 44 | "parameters": [ 45 | { 46 | "description": "Flow object, id is set by the server and could be omitted", 47 | "name": "request", 48 | "in": "body", 49 | "required": true, 50 | "schema": { 51 | "$ref": "#/definitions/main.Flow" 52 | } 53 | } 54 | ], 55 | "responses": { 56 | "200": { 57 | "description": "OK", 58 | "schema": { 59 | "$ref": "#/definitions/main.Flow" 60 | } 61 | }, 62 | "500": { 63 | "description": "Internal Server Error", 64 | "schema": { 65 | "$ref": "#/definitions/main.HTTPError" 66 | } 67 | } 68 | } 69 | } 70 | }, 71 | "/flows/{id}": { 72 | "delete": { 73 | "description": "Delete an existing flow", 74 | "produces": [ 75 | "application/json" 76 | ], 77 | "tags": [ 78 | "Flows" 79 | ], 80 | "summary": "Delete a flow", 81 | "parameters": [ 82 | { 83 | "type": "integer", 84 | "description": "Flow id", 85 | "name": "id", 86 | "in": "path", 87 | "required": true 88 | } 89 | ], 90 | "responses": { 91 | "200": { 92 | "description": "OK", 93 | "schema": { 94 | "$ref": "#/definitions/main.HTTPResponse" 95 | } 96 | }, 97 | "500": { 98 | "description": "Internal Server Error", 99 | "schema": { 100 | "$ref": "#/definitions/main.HTTPError" 101 | } 102 | } 103 | } 104 | }, 105 | "patch": { 106 | "description": "Update an existing flow with new data", 107 | "produces": [ 108 | "application/json" 109 | ], 110 | "tags": [ 111 | "Flows" 112 | ], 113 | "summary": "Update existing flow", 114 | "parameters": [ 115 | { 116 | "type": "integer", 117 | "description": "Flow id", 118 | "name": "id", 119 | "in": "path", 120 | "required": true 121 | }, 122 | { 123 | "description": "Flow object, id is ignored and could be omitted", 124 | "name": "request", 125 | "in": "body", 126 | "required": true, 127 | "schema": { 128 | "$ref": "#/definitions/main.Flow" 129 | } 130 | } 131 | ], 132 | "responses": { 133 | "200": { 134 | "description": "OK", 135 | "schema": { 136 | "$ref": "#/definitions/main.Flow" 137 | } 138 | }, 139 | "500": { 140 | "description": "Internal Server Error", 141 | "schema": { 142 | "$ref": "#/definitions/main.HTTPError" 143 | } 144 | } 145 | } 146 | } 147 | } 148 | }, 149 | "definitions": { 150 | "main.Flow": { 151 | "type": "object", 152 | "properties": { 153 | "amount": { 154 | "type": "number" 155 | }, 156 | "description": { 157 | "type": "string" 158 | }, 159 | "icon": { 160 | "type": "string" 161 | }, 162 | "id": { 163 | "type": "string" 164 | }, 165 | "name": { 166 | "type": "string" 167 | } 168 | } 169 | }, 170 | "main.HTTPError": { 171 | "type": "object", 172 | "properties": { 173 | "error": { 174 | "type": "string" 175 | } 176 | } 177 | }, 178 | "main.HTTPResponse": { 179 | "type": "object", 180 | "properties": { 181 | "ok": { 182 | "type": "string" 183 | } 184 | } 185 | } 186 | } 187 | } -------------------------------------------------------------------------------- /api/docs/swagger.yaml: -------------------------------------------------------------------------------- 1 | definitions: 2 | main.Flow: 3 | properties: 4 | amount: 5 | type: number 6 | description: 7 | type: string 8 | icon: 9 | type: string 10 | id: 11 | type: string 12 | name: 13 | type: string 14 | type: object 15 | main.HTTPError: 16 | properties: 17 | error: 18 | type: string 19 | type: object 20 | main.HTTPResponse: 21 | properties: 22 | ok: 23 | type: string 24 | type: object 25 | info: 26 | contact: {} 27 | paths: 28 | /flows: 29 | get: 30 | description: Get all flows in a JSON object 31 | produces: 32 | - application/json 33 | responses: 34 | "200": 35 | description: OK 36 | schema: 37 | items: 38 | $ref: '#/definitions/main.Flow' 39 | type: array 40 | "500": 41 | description: Internal Server Error 42 | schema: 43 | $ref: '#/definitions/main.HTTPError' 44 | summary: Get all flows 45 | tags: 46 | - Flows 47 | post: 48 | description: Get all flows in a JSON object 49 | parameters: 50 | - description: Flow object, id is set by the server and could be omitted 51 | in: body 52 | name: request 53 | required: true 54 | schema: 55 | $ref: '#/definitions/main.Flow' 56 | produces: 57 | - application/json 58 | responses: 59 | "200": 60 | description: OK 61 | schema: 62 | $ref: '#/definitions/main.Flow' 63 | "500": 64 | description: Internal Server Error 65 | schema: 66 | $ref: '#/definitions/main.HTTPError' 67 | summary: Add new flow 68 | tags: 69 | - Flows 70 | /flows/{id}: 71 | delete: 72 | description: Delete an existing flow 73 | parameters: 74 | - description: Flow id 75 | in: path 76 | name: id 77 | required: true 78 | type: integer 79 | produces: 80 | - application/json 81 | responses: 82 | "200": 83 | description: OK 84 | schema: 85 | $ref: '#/definitions/main.HTTPResponse' 86 | "500": 87 | description: Internal Server Error 88 | schema: 89 | $ref: '#/definitions/main.HTTPError' 90 | summary: Delete a flow 91 | tags: 92 | - Flows 93 | patch: 94 | description: Update an existing flow with new data 95 | parameters: 96 | - description: Flow id 97 | in: path 98 | name: id 99 | required: true 100 | type: integer 101 | - description: Flow object, id is ignored and could be omitted 102 | in: body 103 | name: request 104 | required: true 105 | schema: 106 | $ref: '#/definitions/main.Flow' 107 | produces: 108 | - application/json 109 | responses: 110 | "200": 111 | description: OK 112 | schema: 113 | $ref: '#/definitions/main.Flow' 114 | "500": 115 | description: Internal Server Error 116 | schema: 117 | $ref: '#/definitions/main.HTTPError' 118 | summary: Update existing flow 119 | tags: 120 | - Flows 121 | swagger: "2.0" 122 | -------------------------------------------------------------------------------- /api/go.mod: -------------------------------------------------------------------------------- 1 | module tiehfood/thrifty 2 | 3 | go 1.23.6 4 | 5 | require ( 6 | github.com/gin-contrib/cors v1.7.3 7 | github.com/gin-gonic/gin v1.10.0 8 | github.com/google/uuid v1.6.0 9 | github.com/swaggo/files v1.0.1 10 | github.com/swaggo/gin-swagger v1.6.0 11 | github.com/swaggo/swag v1.16.4 12 | modernc.org/sqlite v1.35.0 13 | ) 14 | 15 | require ( 16 | github.com/KyleBanks/depth v1.2.1 // indirect 17 | github.com/PuerkitoBio/purell v1.1.1 // indirect 18 | github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect 19 | github.com/bytedance/sonic v1.12.6 // indirect 20 | github.com/bytedance/sonic/loader v0.2.1 // indirect 21 | github.com/cloudwego/base64x v0.1.4 // indirect 22 | github.com/cloudwego/iasm v0.2.0 // indirect 23 | github.com/dustin/go-humanize v1.0.1 // indirect 24 | github.com/gabriel-vasile/mimetype v1.4.7 // indirect 25 | github.com/gin-contrib/sse v0.1.0 // indirect 26 | github.com/go-openapi/jsonpointer v0.19.5 // indirect 27 | github.com/go-openapi/jsonreference v0.19.6 // indirect 28 | github.com/go-openapi/spec v0.20.4 // indirect 29 | github.com/go-openapi/swag v0.19.15 // indirect 30 | github.com/go-playground/locales v0.14.1 // indirect 31 | github.com/go-playground/universal-translator v0.18.1 // indirect 32 | github.com/go-playground/validator/v10 v10.23.0 // indirect 33 | github.com/goccy/go-json v0.10.4 // indirect 34 | github.com/josharian/intern v1.0.0 // indirect 35 | github.com/json-iterator/go v1.1.12 // indirect 36 | github.com/klauspost/cpuid/v2 v2.2.9 // indirect 37 | github.com/leodido/go-urn v1.4.0 // indirect 38 | github.com/mailru/easyjson v0.7.6 // indirect 39 | github.com/mattn/go-isatty v0.0.20 // indirect 40 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 41 | github.com/modern-go/reflect2 v1.0.2 // indirect 42 | github.com/ncruces/go-strftime v0.1.9 // indirect 43 | github.com/pelletier/go-toml/v2 v2.2.3 // indirect 44 | github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect 45 | github.com/twitchyliquid64/golang-asm v0.15.1 // indirect 46 | github.com/ugorji/go/codec v1.2.12 // indirect 47 | golang.org/x/arch v0.12.0 // indirect 48 | golang.org/x/crypto v0.31.0 // indirect 49 | golang.org/x/exp v0.0.0-20230315142452-642cacee5cc0 // indirect 50 | golang.org/x/net v0.33.0 // indirect 51 | golang.org/x/sys v0.28.0 // indirect 52 | golang.org/x/text v0.21.0 // indirect 53 | golang.org/x/tools v0.23.0 // indirect 54 | google.golang.org/protobuf v1.36.1 // indirect 55 | gopkg.in/yaml.v2 v2.4.0 // indirect 56 | gopkg.in/yaml.v3 v3.0.1 // indirect 57 | modernc.org/libc v1.61.13 // indirect 58 | modernc.org/mathutil v1.7.1 // indirect 59 | modernc.org/memory v1.8.2 // indirect 60 | ) 61 | -------------------------------------------------------------------------------- /api/go.sum: -------------------------------------------------------------------------------- 1 | github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= 2 | github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= 3 | github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI= 4 | github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= 5 | github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M= 6 | github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= 7 | github.com/bytedance/sonic v1.12.6 h1:/isNmCUF2x3Sh8RAp/4mh4ZGkcFAX/hLrzrK3AvpRzk= 8 | github.com/bytedance/sonic v1.12.6/go.mod h1:B8Gt/XvtZ3Fqj+iSKMypzymZxw/FVwgIGKzMzT9r/rk= 9 | github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= 10 | github.com/bytedance/sonic/loader v0.2.1 h1:1GgorWTqf12TA8mma4DDSbaQigE2wOgQo7iCjjJv3+E= 11 | github.com/bytedance/sonic/loader v0.2.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= 12 | github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y= 13 | github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= 14 | github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= 15 | github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= 16 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 17 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 18 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 19 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 20 | github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= 21 | github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= 22 | github.com/gabriel-vasile/mimetype v1.4.7 h1:SKFKl7kD0RiPdbht0s7hFtjl489WcQ1VyPW8ZzUMYCA= 23 | github.com/gabriel-vasile/mimetype v1.4.7/go.mod h1:GDlAgAyIRT27BhFl53XNAFtfjzOkLaF35JdEG0P7LtU= 24 | github.com/gin-contrib/cors v1.7.3 h1:hV+a5xp8hwJoTw7OY+a70FsL8JkVVFTXw9EcfrYUdns= 25 | github.com/gin-contrib/cors v1.7.3/go.mod h1:M3bcKZhxzsvI+rlRSkkxHyljJt1ESd93COUvemZ79j4= 26 | github.com/gin-contrib/gzip v0.0.6 h1:NjcunTcGAj5CO1gn4N8jHOSIeRFHIbn51z6K+xaN4d4= 27 | github.com/gin-contrib/gzip v0.0.6/go.mod h1:QOJlmV2xmayAjkNS2Y8NQsMneuRShOU/kjovCXNuzzk= 28 | github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= 29 | github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= 30 | github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= 31 | github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= 32 | github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= 33 | github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY= 34 | github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= 35 | github.com/go-openapi/jsonreference v0.19.6 h1:UBIxjkht+AWIgYzCDSv2GN+E/togfwXUJFRTWhl2Jjs= 36 | github.com/go-openapi/jsonreference v0.19.6/go.mod h1:diGHMEHg2IqXZGKxqyvWdfWU/aim5Dprw5bqpKkTvns= 37 | github.com/go-openapi/spec v0.20.4 h1:O8hJrt0UMnhHcluhIdUgCLRWyM2x7QkBXRvOs7m+O1M= 38 | github.com/go-openapi/spec v0.20.4/go.mod h1:faYFR1CvsJZ0mNsmsphTMSoRrNV3TEDoAM7FOEWeq8I= 39 | github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= 40 | github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM= 41 | github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= 42 | github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= 43 | github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= 44 | github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= 45 | github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= 46 | github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= 47 | github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= 48 | github.com/go-playground/validator/v10 v10.23.0 h1:/PwmTwZhS0dPkav3cdK9kV1FsAmrL8sThn8IHr/sO+o= 49 | github.com/go-playground/validator/v10 v10.23.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= 50 | github.com/goccy/go-json v0.10.4 h1:JSwxQzIqKfmFX1swYPpUThQZp/Ka4wzJdK0LWVytLPM= 51 | github.com/goccy/go-json v0.10.4/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= 52 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 53 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 54 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 55 | github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd h1:gbpYu9NMq8jhDVbvlGkMFWCjLFlqqEZjEmObmhUy6Vo= 56 | github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw= 57 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 58 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 59 | github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= 60 | github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= 61 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 62 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 63 | github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= 64 | github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY= 65 | github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8= 66 | github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= 67 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 68 | github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= 69 | github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= 70 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 71 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 72 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 73 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 74 | github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= 75 | github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= 76 | github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= 77 | github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= 78 | github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA= 79 | github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= 80 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 81 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 82 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 83 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 84 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 85 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 86 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 87 | github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= 88 | github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= 89 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= 90 | github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= 91 | github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= 92 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 93 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 94 | github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= 95 | github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= 96 | github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= 97 | github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= 98 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 99 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 100 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 101 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 102 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 103 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 104 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 105 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 106 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 107 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 108 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 109 | github.com/swaggo/files v1.0.1 h1:J1bVJ4XHZNq0I46UU90611i9/YzdrF7x92oX1ig5IdE= 110 | github.com/swaggo/files v1.0.1/go.mod h1:0qXmMNH6sXNf+73t65aKeB+ApmgxdnkQzVTAj2uaMUg= 111 | github.com/swaggo/gin-swagger v1.6.0 h1:y8sxvQ3E20/RCyrXeFfg60r6H0Z+SwpTjMYsMm+zy8M= 112 | github.com/swaggo/gin-swagger v1.6.0/go.mod h1:BG00cCEy294xtVpyIAHG6+e2Qzj/xKlRdOqDkvq0uzo= 113 | github.com/swaggo/swag v1.16.4 h1:clWJtd9LStiG3VeijiCfOVODP6VpHtKdQy9ELFG3s1A= 114 | github.com/swaggo/swag v1.16.4/go.mod h1:VBsHJRsDvfYvqoiMKnsdwhNV9LEMHgEDZcyVYX0sxPg= 115 | github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= 116 | github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= 117 | github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= 118 | github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= 119 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 120 | golang.org/x/arch v0.12.0 h1:UsYJhbzPYGsT0HbEdmYcqtCv8UNGvnaL561NnIUvaKg= 121 | golang.org/x/arch v0.12.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= 122 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 123 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 124 | golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= 125 | golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= 126 | golang.org/x/exp v0.0.0-20230315142452-642cacee5cc0 h1:pVgRXcIictcr+lBQIFeiwuwtDIs4eL21OuM9nyAADmo= 127 | golang.org/x/exp v0.0.0-20230315142452-642cacee5cc0/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= 128 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 129 | golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= 130 | golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= 131 | golang.org/x/mod v0.19.0 h1:fEdghXQSo20giMthA7cd28ZC+jts4amQ3YMXiP5oMQ8= 132 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 133 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 134 | golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM= 135 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 136 | golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 137 | golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= 138 | golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= 139 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 140 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 141 | golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= 142 | golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 143 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 144 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 145 | golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 146 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 147 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 148 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 149 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 150 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 151 | golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= 152 | golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 153 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 154 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 155 | golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= 156 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 157 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 158 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 159 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 160 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 161 | golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= 162 | golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= 163 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 164 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 165 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 166 | golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= 167 | golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= 168 | golang.org/x/tools v0.23.0 h1:SGsXPZ+2l4JsgaCKkx+FQ9YZ5XEtA1GZYuoDjenLjvg= 169 | golang.org/x/tools v0.23.0/go.mod h1:pnu6ufv6vQkll6szChhK3C3L/ruaIv5eBeztNG8wtsI= 170 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 171 | google.golang.org/protobuf v1.36.1 h1:yBPeRvTftaleIgM3PZ/WBIZ7XM/eEYAaEyCwvyjq/gk= 172 | google.golang.org/protobuf v1.36.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= 173 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 174 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 175 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 176 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 177 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 178 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 179 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 180 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 181 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 182 | gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 183 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 184 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 185 | modernc.org/cc/v4 v4.21.4 h1:3Be/Rdo1fpr8GrQ7IVw9OHtplU4gWbb+wNgeoBMmGLQ= 186 | modernc.org/cc/v4 v4.21.4/go.mod h1:HM7VJTZbUCR3rV8EYBi9wxnJ0ZBRiGE5OeGXNA0IsLQ= 187 | modernc.org/cc/v4 v4.24.4 h1:TFkx1s6dCkQpd6dKurBNmpo+G8Zl4Sq/ztJ+2+DEsh0= 188 | modernc.org/ccgo/v4 v4.19.2 h1:lwQZgvboKD0jBwdaeVCTouxhxAyN6iawF3STraAal8Y= 189 | modernc.org/ccgo/v4 v4.19.2/go.mod h1:ysS3mxiMV38XGRTTcgo0DQTeTmAO4oCmJl1nX9VFI3s= 190 | modernc.org/ccgo/v4 v4.23.16 h1:Z2N+kk38b7SfySC1ZkpGLN2vthNJP1+ZzGZIlH7uBxo= 191 | modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE= 192 | modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ= 193 | modernc.org/gc/v2 v2.4.1 h1:9cNzOqPyMJBvrUipmynX0ZohMhcxPtMccYgGOJdOiBw= 194 | modernc.org/gc/v2 v2.4.1/go.mod h1:wzN5dK1AzVGoH6XOzc3YZ+ey/jPgYHLuVckd62P0GYU= 195 | modernc.org/gc/v2 v2.6.3 h1:aJVhcqAte49LF+mGveZ5KPlsp4tdGdAOT4sipJXADjw= 196 | modernc.org/libc v1.55.3 h1:AzcW1mhlPNrRtjS5sS+eW2ISCgSOLLNyFzRh/V3Qj/U= 197 | modernc.org/libc v1.55.3/go.mod h1:qFXepLhz+JjFThQ4kzwzOjA/y/artDeg+pcYnY+Q83w= 198 | modernc.org/libc v1.61.13 h1:3LRd6ZO1ezsFiX1y+bHd1ipyEHIJKvuprv0sLTBwLW8= 199 | modernc.org/libc v1.61.13/go.mod h1:8F/uJWL/3nNil0Lgt1Dpz+GgkApWh04N3el3hxJcA6E= 200 | modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4= 201 | modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo= 202 | modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= 203 | modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= 204 | modernc.org/memory v1.8.0 h1:IqGTL6eFMaDZZhEWwcREgeMXYwmW83LYW8cROZYkg+E= 205 | modernc.org/memory v1.8.0/go.mod h1:XPZ936zp5OMKGWPqbD3JShgd/ZoQ7899TUuQqxY+peU= 206 | modernc.org/memory v1.8.2 h1:cL9L4bcoAObu4NkxOlKWBWtNHIsnnACGF/TbqQ6sbcI= 207 | modernc.org/memory v1.8.2/go.mod h1:ZbjSvMO5NQ1A2i3bWeDiVMxIorXwdClKE/0SZ+BMotU= 208 | modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4= 209 | modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= 210 | modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8= 211 | modernc.org/sortutil v1.2.0 h1:jQiD3PfS2REGJNzNCMMaLSp/wdMNieTbKX920Cqdgqc= 212 | modernc.org/sortutil v1.2.0/go.mod h1:TKU2s7kJMf1AE84OoiGppNHJwvB753OYfNl2WRb++Ss= 213 | modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= 214 | modernc.org/sqlite v1.34.5 h1:Bb6SR13/fjp15jt70CL4f18JIN7p7dnMExd+UFnF15g= 215 | modernc.org/sqlite v1.34.5/go.mod h1:YLuNmX9NKs8wRNK2ko1LW1NGYcc9FkBO69JOt1AR9JE= 216 | modernc.org/sqlite v1.35.0 h1:yQps4fegMnZFdphtzlfQTCNBWtS0CZv48pRpW3RFHRw= 217 | modernc.org/sqlite v1.35.0/go.mod h1:9cr2sicr7jIaWTBKQmAxQLfBv9LL0su4ZTEV+utt3ic= 218 | modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA= 219 | modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0= 220 | modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= 221 | modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= 222 | modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= 223 | nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= 224 | -------------------------------------------------------------------------------- /api/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crypto/md5" 5 | "database/sql" 6 | "encoding/json" 7 | "fmt" 8 | "github.com/gin-contrib/cors" 9 | "github.com/gin-gonic/gin" 10 | "github.com/google/uuid" 11 | swaggerFiles "github.com/swaggo/files" 12 | ginSwagger "github.com/swaggo/gin-swagger" 13 | _ "modernc.org/sqlite" 14 | "net/http" 15 | "os" 16 | "strconv" 17 | "tiehfood/thrifty/docs" 18 | ) 19 | 20 | type Flow struct { 21 | ID string `json:"id"` 22 | Name string `json:"name"` 23 | Description string `json:"description"` 24 | Amount float64 `json:"amount"` 25 | Icon string `json:"icon"` 26 | } 27 | 28 | type HTTPResponse struct { 29 | Ok string `json:"ok"` 30 | } 31 | 32 | type HTTPError struct { 33 | Error string `json:"error"` 34 | } 35 | 36 | var defaultIconId = "00000000-0000-0000-0000-000000000000" 37 | var errorPrefix = "Error: " 38 | 39 | func main() { 40 | dbCon, err := initAndOpenDb() 41 | if err != nil { 42 | fmt.Println(errorPrefix, err) 43 | } 44 | 45 | docs.SwaggerInfo.Title = "Thrifty API" 46 | docs.SwaggerInfo.Description = "This is the documentation for the Thrifty API" 47 | docs.SwaggerInfo.BasePath = "/api" 48 | docs.SwaggerInfo.Version = "1.0" 49 | 50 | gin.SetMode(gin.ReleaseMode) 51 | router := gin.Default() 52 | 53 | router.Use(cors.New(cors.Config{ 54 | AllowOrigins: []string{"*"}, 55 | AllowMethods: []string{"GET", "POST", "PATCH", "OPTIONS", "DELETE"}, 56 | AllowHeaders: []string{"Content-Type"}, 57 | })) 58 | 59 | router.Use(func(context *gin.Context) { 60 | context.Set("dbCon", dbCon) 61 | context.Next() 62 | }) 63 | 64 | v1 := router.Group("/api") 65 | { 66 | flows := v1.Group("/flows") 67 | { 68 | flows.GET("", getFlows) 69 | flows.POST("", addFlow) 70 | flows.PATCH(":id", updateFlow) 71 | flows.DELETE(":id", deleteFlow) 72 | } 73 | } 74 | 75 | router.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler)) 76 | port, err := getAndValidatePort() 77 | if err != nil { 78 | fmt.Println(errorPrefix, err) 79 | } 80 | fmt.Printf("Running on port: %d\n", port) 81 | err = router.Run(fmt.Sprintf(":%d", port)) 82 | if err != nil { 83 | fmt.Println(errorPrefix, err) 84 | } 85 | } 86 | 87 | func getAndValidatePort() (int, error) { 88 | portStr := os.Getenv("PORT") 89 | if portStr == "" { 90 | return 8080, nil 91 | } 92 | 93 | port, err := strconv.Atoi(portStr) 94 | if err != nil { 95 | return 8080, fmt.Errorf("invalid PORT value: %v", err) 96 | } 97 | 98 | if port < 1 || port > 65535 { 99 | return 0, fmt.Errorf("PORT value out of range (1-65535)") 100 | } 101 | 102 | return port, nil 103 | } 104 | 105 | func isInvalidFlow(flow Flow) bool { 106 | if flow.Amount != 0.0 { 107 | if len(flow.Name) > 0 { 108 | return false 109 | } 110 | } 111 | return true 112 | } 113 | 114 | func initAndOpenDb() (*sql.DB, error) { 115 | dataSourceName := os.Getenv("SQLITE_DB_PATH") 116 | defaultIconFlow := Flow{ 117 | Icon: "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPHN2ZyBpZD0iTGF5ZXJfMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB2ZXJzaW9uPSIxLjEiIHZpZXdCb3g9IjAgMCAyNTQuMiAyNTQuMiI+CiAgPGRlZnM+CiAgICA8c3R5bGU+CiAgICAgIC5zdDAgewogICAgICAgIGZpbGw6ICM0NDliMzg7CiAgICAgIH0KCiAgICAgIC5zdDEgewogICAgICAgIGZpbGw6ICNmZmMzMzY7CiAgICAgIH0KCiAgICAgIC5zdDIgewogICAgICAgIGZpbGw6ICNjODkzMTg7CiAgICAgIH0KCiAgICAgIC5zdDMgewogICAgICAgIGZpbGw6ICM2MmM3NTE7CiAgICAgIH0KCiAgICAgIC5zdDQgewogICAgICAgIGZpbGw6ICNlYWIwMjg7CiAgICAgIH0KICAgIDwvc3R5bGU+CiAgPC9kZWZzPgogIDxyZWN0IGNsYXNzPSJzdDAiIHk9IjU0LjYiIHdpZHRoPSIyNTQuMiIgaGVpZ2h0PSIxMzAuNyIvPgogIDxwYXRoIGNsYXNzPSJzdDMiIGQ9Ik0yMTMuNSw3My41SDQwLjhjMCwxMi4xLTkuOCwyMS45LTIxLjksMjEuOXY0OS4yYzEyLjEsMCwyMS45LDkuOCwyMS45LDIxLjloMTcyLjdjMC0uMiwwLS40LDAtLjcsMC0xMi4xLDkuOC0yMS45LDIxLjktMjEuOXYtNDkuMmMtMTEuOCwwLTIxLjUtOS40LTIxLjgtMjEuMloiLz4KICA8Y2lyY2xlIGNsYXNzPSJzdDAiIGN4PSIxMjcuMSIgY3k9IjEyMCIgcj0iMzguOSIvPgogIDxjaXJjbGUgY2xhc3M9InN0MCIgY3g9IjY3IiBjeT0iMTIwIiByPSI3LjgiLz4KICA8Y2lyY2xlIGNsYXNzPSJzdDAiIGN4PSIxODcuMiIgY3k9IjEyMCIgcj0iNy44IiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgzOC44IDI4NS41KSByb3RhdGUoLTgwLjgpIi8+CiAgPHBhdGggY2xhc3M9InN0MyIgZD0iTTEyNi44LDE0MC40Yy01LjksMC0xMS4yLTMuMS0xMy4zLThsNy43LTMuNWMuOCwxLjgsMy4xLDMsNS42LDNzNC44LTEuMiw1LjYtM2MuMy0uNi4zLTEuMi4yLTEuNS0uMi0uNS0xLjQtMS45LTYuNy0zLjUtNC40LTEuMy05LjctMy40LTExLjktNy45LS45LTEuOC0xLjUtNC43LDAtOC41LDIuMi01LDcuNS04LjEsMTMuNC04LjFzMTEuMiwzLjEsMTMuMyw4bC03LjcsMy41Yy0uOC0xLjgtMy4xLTMtNS42LTNzLTQuOCwxLjItNS42LDNjLS4zLjYtLjMsMS4yLS4yLDEuNS4yLjUsMS40LDEuOSw2LjcsMy41LDQuNCwxLjMsOS43LDMuNCwxMS45LDcuOS45LDEuOCwxLjUsNC43LDAsOC41LTIuMiw1LTcuNSw4LjEtMTMuNCw4LjFaIi8+CiAgPHJlY3QgY2xhc3M9InN0MyIgeD0iMTIyLjYiIHk9IjEzNi4yIiB3aWR0aD0iOC41IiBoZWlnaHQ9IjkuOCIvPgogIDxyZWN0IGNsYXNzPSJzdDMiIHg9IjEyMi42IiB5PSI5My45IiB3aWR0aD0iOC41IiBoZWlnaHQ9IjkuOCIvPgogIDxjaXJjbGUgY2xhc3M9InN0MyIgY3g9IjI0LjQiIGN5PSI3OC44IiByPSI1LjUiLz4KICA8Y2lyY2xlIGNsYXNzPSJzdDMiIGN4PSIyNC40IiBjeT0iMTYxIiByPSI1LjUiLz4KICA8Y2lyY2xlIGNsYXNzPSJzdDMiIGN4PSIyMjkuOCIgY3k9Ijc4LjkiIHI9IjUuNSIvPgogIDxjaXJjbGUgY2xhc3M9InN0MyIgY3g9IjIyOS44IiBjeT0iMTYxLjEiIHI9IjUuNSIvPgogIDxjaXJjbGUgY2xhc3M9InN0NCIgY3g9IjIxNy43IiBjeT0iMTY2LjYiIHI9IjI5LjUiLz4KICA8Y2lyY2xlIGNsYXNzPSJzdDEiIGN4PSIyMTcuNyIgY3k9IjE2Ni42IiByPSIyMy45Ii8+CiAgPHBhdGggY2xhc3M9InN0MiIgZD0iTTIxNy41LDE3OS4xYy0zLjYsMC02LjktMS45LTguMi00LjlsNC44LTIuMWMuNSwxLjEsMS45LDEuOCwzLjQsMS44czMtLjgsMy40LTEuOGMuMi0uNC4yLS43LjEtLjktLjEtLjMtLjktMS4xLTQuMS0yLjEtMi43LS44LTYtMi4xLTcuMy00LjktLjUtMS4xLS45LTIuOSwwLTUuMiwxLjQtMy4xLDQuNi01LDguMi01czYuOSwxLjksOC4yLDQuOWwtNC44LDIuMWMtLjUtMS4xLTEuOS0xLjgtMy40LTEuOHMtMywuOC0zLjQsMS44Yy0uMi40LS4yLjctLjEuOS4xLjMuOSwxLjEsNC4xLDIuMSwyLjcuOCw2LDIuMSw3LjMsNC45LjUsMS4xLjksMi45LDAsNS4yLTEuNCwzLjEtNC42LDUtOC4yLDVaIi8+CiAgPHJlY3QgY2xhc3M9InN0MiIgeD0iMjE0LjkiIHk9IjE3Ni41IiB3aWR0aD0iNS4yIiBoZWlnaHQ9IjYiLz4KICA8cmVjdCBjbGFzcz0ic3QyIiB4PSIyMTQuOSIgeT0iMTUwLjYiIHdpZHRoPSI1LjIiIGhlaWdodD0iNiIvPgogIDxjaXJjbGUgY2xhc3M9InN0NCIgY3g9IjE3MC42IiBjeT0iMTgzLjEiIHI9IjI5LjUiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0zNy41IDMyMi4xKSByb3RhdGUoLTgwLjgpIi8+CiAgPGNpcmNsZSBjbGFzcz0ic3QxIiBjeD0iMTcwLjUiIGN5PSIxODMuMSIgcj0iMjMuOSIvPgogIDxwYXRoIGNsYXNzPSJzdDIiIGQ9Ik0xNzAuMywxOTUuN2MtMy42LDAtNi45LTEuOS04LjItNC45bDQuOC0yLjFjLjUsMS4xLDEuOSwxLjgsMy40LDEuOHMzLS44LDMuNC0xLjhjLjItLjQuMi0uNy4xLS45LS4xLS4zLS45LTEuMS00LjEtMi4xLTIuNy0uOC02LTIuMS03LjMtNC45LS41LTEuMS0uOS0yLjksMC01LjIsMS40LTMuMSw0LjYtNSw4LjItNXM2LjksMS45LDguMiw0LjlsLTQuOCwyLjFjLS41LTEuMS0xLjktMS44LTMuNC0xLjhzLTMsLjgtMy40LDEuOGMtLjIuNC0uMi43LS4xLjkuMS4zLjksMS4xLDQuMSwyLjEsMi43LjgsNiwyLjEsNy4zLDQuOS41LDEuMS45LDIuOSwwLDUuMi0xLjQsMy4xLTQuNiw1LTguMiw1WiIvPgogIDxyZWN0IGNsYXNzPSJzdDIiIHg9IjE2Ny43IiB5PSIxOTMuMSIgd2lkdGg9IjUuMiIgaGVpZ2h0PSI2Ii8+CiAgPHJlY3QgY2xhc3M9InN0MiIgeD0iMTY3LjciIHk9IjE2Ny4xIiB3aWR0aD0iNS4yIiBoZWlnaHQ9IjYiLz4KICA8Y2lyY2xlIGNsYXNzPSJzdDQiIGN4PSIyMTQuOCIgY3k9IjIwNS42IiByPSIyOS41Ii8+CiAgPGNpcmNsZSBjbGFzcz0ic3QxIiBjeD0iMjE0LjgiIGN5PSIyMDUuNiIgcj0iMjMuOSIvPgogIDxwYXRoIGNsYXNzPSJzdDIiIGQ9Ik0yMTQuNiwyMTguMmMtMy42LDAtNi45LTEuOS04LjItNC45bDQuOC0yLjFjLjUsMS4xLDEuOSwxLjgsMy40LDEuOHMzLS44LDMuNC0xLjhjLjItLjQuMi0uNy4xLS45LS4xLS4zLS45LTEuMS00LjEtMi4xLTIuNy0uOC02LTIuMS03LjMtNC45LS41LTEuMS0uOS0yLjksMC01LjIsMS40LTMuMSw0LjYtNSw4LjItNXM2LjksMS45LDguMiw0LjlsLTQuOCwyLjFjLS41LTEuMS0xLjktMS44LTMuNC0xLjhzLTMsLjgtMy40LDEuOGMtLjIuNC0uMi43LS4xLjkuMS4zLjksMS4xLDQuMSwyLjEsMi43LjgsNiwyLjEsNy4zLDQuOS41LDEuMS45LDIuOSwwLDUuMi0xLjQsMy4xLTQuNiw1LTguMiw1WiIvPgogIDxyZWN0IGNsYXNzPSJzdDIiIHg9IjIxMiIgeT0iMjE1LjYiIHdpZHRoPSI1LjIiIGhlaWdodD0iNiIvPgogIDxyZWN0IGNsYXNzPSJzdDIiIHg9IjIxMiIgeT0iMTg5LjYiIHdpZHRoPSI1LjIiIGhlaWdodD0iNiIvPgo8L3N2Zz4=", 118 | } 119 | 120 | if len(dataSourceName) == 0 { 121 | dataSourceName = "thrifty.sqlite" 122 | } 123 | dbCon, err := sql.Open("sqlite", dataSourceName) 124 | if err != nil { 125 | return nil, err 126 | } 127 | 128 | queryFlows := fmt.Sprintf(` 129 | CREATE TABLE IF NOT EXISTS flows ( 130 | id TEXT NOT NULL PRIMARY KEY, 131 | name TEXT, 132 | description TEXT, 133 | amount float, 134 | iconId TEXT DEFAULT '%s', 135 | FOREIGN KEY (iconId) REFERENCES icons(id)); 136 | `, defaultIconId) 137 | 138 | queryIcons := ` 139 | CREATE TABLE IF NOT EXISTS icons ( 140 | id TEXT NOT NULL PRIMARY KEY, 141 | data TEXT, 142 | hash TEXT); 143 | ` 144 | printSqlVersion(dbCon) 145 | execSql(dbCon, queryFlows) 146 | execSql(dbCon, queryIcons) 147 | insertIcon(dbCon, defaultIconFlow, &defaultIconId) 148 | return dbCon, nil 149 | } 150 | 151 | func printSqlVersion(dbCon *sql.DB) { 152 | query := "SELECT sqlite_version();" 153 | row, err := getSqlRow(dbCon, query, nil) 154 | if err != nil { 155 | fmt.Println("Error: ", err) 156 | } 157 | fmt.Println("SQLite version: ", row[0]) 158 | } 159 | 160 | func execSql(dbCon *sql.DB, query string, args ...interface{}) { 161 | result, err := dbCon.Exec(query, args...) 162 | if err != nil { 163 | fmt.Println(errorPrefix, err) 164 | } 165 | 166 | id, _ := result.LastInsertId() 167 | numRows, _ := result.RowsAffected() 168 | fmt.Printf("LastInsertId %d, RowsAffected: %d\n", id, numRows) 169 | } 170 | 171 | func getSqlRow(dbCon *sql.DB, query string, args ...interface{}) ([]interface{}, error) { 172 | rows, err := dbCon.Query(query, args...) 173 | if err != nil { 174 | return nil, err 175 | } 176 | defer rows.Close() 177 | 178 | columns, err := rows.Columns() 179 | if err != nil { 180 | return nil, err 181 | } 182 | 183 | row := make([]interface{}, len(columns)) 184 | scanArgs := make([]interface{}, len(columns)) 185 | for i := range row { 186 | scanArgs[i] = &row[i] 187 | } 188 | 189 | // get first row 190 | if rows.Next() { 191 | err = rows.Scan(scanArgs...) 192 | if err != nil { 193 | return nil, err 194 | } 195 | } 196 | return row, rows.Err() 197 | } 198 | 199 | func getMD5(input string) string { 200 | hash := md5.Sum([]byte(input)) 201 | return fmt.Sprintf("%x", hash) 202 | } 203 | 204 | func getDbConnection(c *gin.Context) *sql.DB { 205 | dbConVar, exists := c.Get("dbCon") 206 | if !exists { 207 | c.JSON(http.StatusInternalServerError, HTTPError{Error: "Database connection not found"}) 208 | return nil 209 | } 210 | dbCon, ok := dbConVar.(*sql.DB) 211 | if !ok { 212 | c.JSON(http.StatusInternalServerError, HTTPError{Error: "Invalid database connection"}) 213 | return nil 214 | } 215 | return dbCon 216 | } 217 | 218 | func insertIcon(dbCon *sql.DB, flow Flow, iconId *string) string { 219 | hash := getMD5(flow.Icon) 220 | 221 | if iconId == nil { 222 | var id = uuid.New().String() 223 | iconId = &id 224 | } 225 | 226 | query := "SELECT id, data, hash FROM icons WHERE hash = ?" 227 | row, err := getSqlRow(dbCon, query, []interface{}{hash}...) 228 | 229 | if err != nil { 230 | fmt.Println(errorPrefix, err) 231 | } 232 | 233 | if row[0] == nil { 234 | query = "INSERT OR REPLACE INTO icons (id, data, hash) VALUES (?, ?, ?)" 235 | queryArgs := []interface{}{*iconId, flow.Icon, hash} 236 | execSql(dbCon, query, queryArgs...) 237 | } else { 238 | var value = row[0].(string) 239 | iconId = &value 240 | } 241 | return *iconId 242 | } 243 | 244 | // getFlows godoc 245 | // @Summary Get all flows 246 | // @Description Get all flows in a JSON object 247 | // @Tags Flows 248 | // @Produce json 249 | // @Success 200 {array} main.Flow 250 | // @Failure 500 {object} main.HTTPError 251 | // @Router /flows [get] 252 | func getFlows(c *gin.Context) { 253 | query := ` 254 | SELECT 255 | json_group_array(json_object('id', id, 'name', name, 'description', description, 'amount', amount, 'icon', data)) AS json_result 256 | FROM 257 | ( 258 | SELECT 259 | f.id, f.name as name, f.description, f.amount, icons.data 260 | FROM 261 | flows f 262 | LEFT JOIN icons ON f.iconId = icons.id 263 | ORDER BY 264 | amount DESC, name 265 | ); 266 | ` 267 | dbCon := getDbConnection(c) 268 | if dbCon == nil { 269 | return 270 | } 271 | 272 | var jsonResult []byte 273 | err := dbCon.QueryRow(query).Scan(&jsonResult) 274 | if err != nil { 275 | c.JSON(http.StatusInternalServerError, HTTPError{Error: "Failed to query database"}) 276 | return 277 | } 278 | 279 | if !json.Valid(jsonResult) { 280 | c.JSON(http.StatusInternalServerError, HTTPError{Error: "Invalid data returned from database"}) 281 | } 282 | c.Header("Content-Type", "application/json") 283 | c.Data(http.StatusOK, "application/json", jsonResult) 284 | } 285 | 286 | // addFlow godoc 287 | // @Summary Add new flow 288 | // @Description Get all flows in a JSON object 289 | // @Tags Flows 290 | // @Param request body main.Flow true "Flow object, id is set by the server and could be omitted" 291 | // @Produce json 292 | // @Success 200 {object} main.Flow 293 | // @Failure 500 {object} main.HTTPError 294 | // @Router /flows [post] 295 | func addFlow(c *gin.Context) { 296 | var newFlow Flow 297 | 298 | if err := c.BindJSON(&newFlow); err != nil { 299 | fmt.Println(errorPrefix, err) 300 | c.JSON(http.StatusInternalServerError, HTTPError{Error: "Wrong data format"}) 301 | return 302 | } 303 | 304 | if isInvalidFlow(newFlow) { 305 | c.JSON(http.StatusInternalServerError, HTTPError{Error: "Please provide name and amount"}) 306 | return 307 | } 308 | 309 | newFlow.ID = uuid.New().String() 310 | 311 | dbCon := getDbConnection(c) 312 | if dbCon == nil { 313 | return 314 | } 315 | 316 | query := "INSERT OR REPLACE INTO flows (id, name, description, amount) VALUES (?, ?, ?, ?)" 317 | queryArgs := []interface{}{newFlow.ID, newFlow.Name, newFlow.Description, newFlow.Amount} 318 | if newFlow.Icon != "" { 319 | iconId := insertIcon(dbCon, newFlow, nil) 320 | query = "INSERT OR REPLACE INTO flows (id, name, description, amount, iconId) VALUES (?, ?, ?, ?, ?)" 321 | queryArgs = []interface{}{newFlow.ID, newFlow.Name, newFlow.Description, newFlow.Amount, iconId} 322 | } 323 | 324 | execSql(dbCon, query, queryArgs...) 325 | 326 | c.IndentedJSON(http.StatusCreated, newFlow) 327 | } 328 | 329 | // updateFlow godoc 330 | // @Summary Update existing flow 331 | // @Description Update an existing flow with new data 332 | // @Tags Flows 333 | // @Param id path int true "Flow id" 334 | // @Param request body main.Flow true "Flow object, id is ignored and could be omitted" 335 | // @Produce json 336 | // @Success 200 {object} main.Flow 337 | // @Failure 500 {object} main.HTTPError 338 | // @Router /flows/{id} [patch] 339 | func updateFlow(c *gin.Context) { 340 | var newFlow Flow 341 | 342 | if err := c.BindJSON(&newFlow); err != nil { 343 | fmt.Println(errorPrefix, err) 344 | c.JSON(http.StatusInternalServerError, HTTPError{Error: "Wrong data format"}) 345 | return 346 | } 347 | 348 | if isInvalidFlow(newFlow) { 349 | c.JSON(http.StatusInternalServerError, HTTPError{Error: "Please provide name and amount"}) 350 | return 351 | } 352 | 353 | newFlow.ID = c.Param("id") 354 | 355 | dbCon := getDbConnection(c) 356 | if dbCon == nil { 357 | return 358 | } 359 | 360 | query := "SELECT flows.id, iconId, hash FROM flows JOIN icons ON flows.iconId = icons.id WHERE flows.id = ?" 361 | row, err := getSqlRow(dbCon, query, []interface{}{newFlow.ID}...) 362 | 363 | if err != nil || row[0] == nil { 364 | c.JSON(http.StatusInternalServerError, HTTPError{Error: "Flow with given ID does not exist"}) 365 | return 366 | } 367 | 368 | iconId, ok := row[1].(string) 369 | if !ok { 370 | c.JSON(http.StatusInternalServerError, HTTPError{Error: "Database error"}) 371 | return 372 | } 373 | 374 | hash, ok := row[2].(string) 375 | if !ok { 376 | c.JSON(http.StatusInternalServerError, HTTPError{Error: "Database error"}) 377 | return 378 | } 379 | 380 | if iconId != defaultIconId && hash != getMD5(newFlow.Icon) { 381 | query = "SELECT count(id) FROM flows WHERE iconId = ?" 382 | row, err = getSqlRow(dbCon, query, []interface{}{iconId}...) 383 | count, ok := row[0].(int64) 384 | 385 | if err != nil || !ok { 386 | c.JSON(http.StatusInternalServerError, HTTPError{Error: "Database error"}) 387 | return 388 | } 389 | 390 | if count == 1 { 391 | query = "DELETE FROM icons WHERE id = ?" 392 | execSql(dbCon, query, []interface{}{iconId}...) 393 | } 394 | } 395 | 396 | query = "UPDATE flows SET name = ?, description = ?, amount = ? WHERE id = ?" 397 | queryArgs := []interface{}{newFlow.Name, newFlow.Description, newFlow.Amount, newFlow.ID} 398 | if newFlow.Icon != "" { 399 | iconId := insertIcon(dbCon, newFlow, nil) 400 | query = "UPDATE flows SET name = ?, description = ?, amount = ?, iconId = ? WHERE id = ?" 401 | queryArgs = []interface{}{newFlow.Name, newFlow.Description, newFlow.Amount, iconId, newFlow.ID} 402 | } 403 | 404 | execSql(dbCon, query, queryArgs...) 405 | c.JSON(http.StatusOK, newFlow) 406 | } 407 | 408 | // deleteFlow godoc 409 | // @Summary Delete a flow 410 | // @Description Delete an existing flow 411 | // @Tags Flows 412 | // @Param id path int true "Flow id" 413 | // @Produce json 414 | // @Success 200 {object} main.HTTPResponse 415 | // @Failure 500 {object} main.HTTPError 416 | // @Router /flows/{id} [delete] 417 | func deleteFlow(c *gin.Context) { 418 | flowId := c.Param("id") 419 | dbCon := getDbConnection(c) 420 | if dbCon == nil { 421 | return 422 | } 423 | 424 | query := "SELECT id, iconId FROM flows WHERE id = ?" 425 | row, err := getSqlRow(dbCon, query, []interface{}{flowId}...) 426 | 427 | if err != nil || row[0] == nil { 428 | c.JSON(http.StatusInternalServerError, HTTPError{Error: "Flow with given ID does not exist"}) 429 | return 430 | } 431 | 432 | iconId, ok := row[1].(string) 433 | if !ok { 434 | c.JSON(http.StatusInternalServerError, HTTPError{Error: "Database error"}) 435 | return 436 | } 437 | 438 | if iconId != defaultIconId { 439 | query = "SELECT count(id) FROM flows WHERE iconId = ?" 440 | row, err = getSqlRow(dbCon, query, []interface{}{iconId}...) 441 | count, ok := row[0].(int64) 442 | 443 | if err != nil || !ok { 444 | c.JSON(http.StatusInternalServerError, HTTPError{Error: "Database error"}) 445 | return 446 | } 447 | 448 | if count == 1 { 449 | query = "DELETE FROM icons WHERE id = ?" 450 | execSql(dbCon, query, []interface{}{iconId}...) 451 | } 452 | } 453 | query = "DELETE FROM flows WHERE id = ?" 454 | execSql(dbCon, query, []interface{}{flowId}...) 455 | c.JSON(http.StatusOK, HTTPResponse{Ok: "Flow deleted"}) 456 | } 457 | -------------------------------------------------------------------------------- /doc/default-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /doc/screenshot_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tiehfood/thrifty/03c6e67a2db40890b3cae3bfad27c480d1cdaa10/doc/screenshot_1.png -------------------------------------------------------------------------------- /doc/screenshot_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tiehfood/thrifty/03c6e67a2db40890b3cae3bfad27c480d1cdaa10/doc/screenshot_2.png -------------------------------------------------------------------------------- /doc/screenshot_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tiehfood/thrifty/03c6e67a2db40890b3cae3bfad27c480d1cdaa10/doc/screenshot_3.png -------------------------------------------------------------------------------- /doc/screenshot_4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tiehfood/thrifty/03c6e67a2db40890b3cae3bfad27c480d1cdaa10/doc/screenshot_4.png -------------------------------------------------------------------------------- /docker-compose-build.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | traefik: 3 | container_name: traefik 4 | image: traefik:3.3 5 | ports: 6 | - "9090:80" 7 | environment: 8 | - TRAEFIK_GLOBAL_SENDANONYMOUSUSAGE=false 9 | - TRAEFIK_PROVIDERS_DOCKER=true 10 | - TRAEFIK_PROVIDERS_DOCKER_EXPOSEDBYDEFAULT=true 11 | - TRAEFIK_ENTRYPOINTS_main=true 12 | - TRAEFIK_ENTRYPOINTS_main_ADDRESS=:80 13 | - TRAEFIK_ENTRYPOINTS_main_HTTP_MIDDLEWARES=gzip@docker 14 | volumes: 15 | - /var/run/docker.sock:/var/run/docker.sock:ro 16 | labels: 17 | - "traefik.enable=true" 18 | - "traefik.http.middlewares.gzip.compress=true" 19 | 20 | ui: 21 | container_name: ui 22 | build: 23 | context: ui 24 | dockerfile: Dockerfile 25 | environment: 26 | - CURRENCY_ISO=EUR 27 | labels: 28 | - "traefik.http.routers.ui.entrypoints=main" 29 | - "traefik.http.routers.ui.rule=PathPrefix(`/`)" 30 | - "traefik.http.routers.ui.service=ui" 31 | - "traefik.http.services.ui.loadbalancer.server.port=8080" 32 | 33 | api: 34 | container_name: api 35 | build: 36 | context: api 37 | dockerfile: Dockerfile 38 | environment: 39 | - SQLITE_DB_PATH=/data/thrifty.sqlite 40 | volumes: 41 | - database:/data 42 | labels: 43 | - "traefik.http.routers.api.entrypoints=main" 44 | - "traefik.http.routers.api.rule=PathRegexp(`^/(api|swagger)/`)" 45 | - "traefik.http.routers.api.service=api" 46 | - "traefik.http.services.api.loadbalancer.server.port=8080" 47 | 48 | volumes: 49 | database: 50 | -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | traefik: 3 | container_name: traefik 4 | image: traefik:3.3 5 | ports: 6 | - "9090:80" 7 | environment: 8 | - TRAEFIK_GLOBAL_SENDANONYMOUSUSAGE=false 9 | - TRAEFIK_PROVIDERS_DOCKER=true 10 | - TRAEFIK_PROVIDERS_DOCKER_EXPOSEDBYDEFAULT=true 11 | - TRAEFIK_ENTRYPOINTS_main=true 12 | - TRAEFIK_ENTRYPOINTS_main_ADDRESS=:80 13 | - TRAEFIK_ENTRYPOINTS_main_HTTP_MIDDLEWARES=gzip@docker 14 | volumes: 15 | - /var/run/docker.sock:/var/run/docker.sock:ro 16 | labels: 17 | - "traefik.enable=true" 18 | - "traefik.http.middlewares.gzip.compress=true" 19 | 20 | ui: 21 | container_name: ui 22 | image: tiehfood/thrifty-ui:latest 23 | environment: 24 | - CURRENCY_ISO=EUR 25 | labels: 26 | - "traefik.http.routers.ui.entrypoints=main" 27 | - "traefik.http.routers.ui.rule=PathPrefix(`/`)" 28 | - "traefik.http.routers.ui.service=ui" 29 | - "traefik.http.services.ui.loadbalancer.server.port=8080" 30 | 31 | api: 32 | container_name: api 33 | image: tiehfood/thrifty-api:latest 34 | environment: 35 | - SQLITE_DB_PATH=/data/thrifty.sqlite 36 | volumes: 37 | - database:/data 38 | labels: 39 | - "traefik.http.routers.api.entrypoints=main" 40 | - "traefik.http.routers.api.rule=PathRegexp(`^/(api|swagger)/`)" 41 | - "traefik.http.routers.api.service=api" 42 | - "traefik.http.services.api.loadbalancer.server.port=8080" 43 | 44 | volumes: 45 | database: 46 | -------------------------------------------------------------------------------- /ui/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | 3 | # Output 4 | .output 5 | .vercel 6 | .netlify 7 | .wrangler 8 | /.svelte-kit 9 | /build 10 | 11 | # OS 12 | .DS_Store 13 | Thumbs.db 14 | 15 | # Env 16 | .env 17 | .env.* 18 | !.env.example 19 | !.env.test 20 | 21 | # Vite 22 | vite.config.js.timestamp-* 23 | vite.config.ts.timestamp-* 24 | -------------------------------------------------------------------------------- /ui/.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict=true 2 | -------------------------------------------------------------------------------- /ui/01-sub.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ash 2 | 3 | CONFIG_FILE=/etc/nginx/conf.d/01-sub_filter.conf 4 | 5 | cat << EOF > "$CONFIG_FILE" 6 | sub_filter_types application/javascript; 7 | sub_filter_once off; 8 | EOF 9 | 10 | if [ -n "$CURRENCY_ISO" ]; then 11 | cat << EOF >> "$CONFIG_FILE" 12 | sub_filter 'currency:"EUR"' 'currency:"${CURRENCY_ISO}"'; 13 | EOF 14 | fi 15 | 16 | if [ -n "$LOCAL_API_PROTOCOL" ]; then 17 | cat << EOF >> "$CONFIG_FILE" 18 | sub_filter 'window.location.protocol.replace(":","")' '"${LOCAL_API_PROTOCOL}"'; 19 | EOF 20 | fi 21 | 22 | if [ -n "$LOCAL_API_HOSTNAME" ]; then 23 | cat << EOF >> "$CONFIG_FILE" 24 | sub_filter 'window.location.hostname.toLowerCase()' '"${LOCAL_API_HOSTNAME}"'; 25 | EOF 26 | fi 27 | 28 | if [ -n "$LOCAL_API_PORT" ]; then 29 | cat << EOF >> "$CONFIG_FILE" 30 | sub_filter 'window.location.port.trim()' '"${LOCAL_API_PORT}"'; 31 | EOF 32 | fi 33 | 34 | if [ "$USE_SINGLE_COLUMN" = true ]; then 35 | cat << EOF >> "$CONFIG_FILE" 36 | sub_filter 'md:grid-cols-2 ' ''; 37 | EOF 38 | fi 39 | -------------------------------------------------------------------------------- /ui/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:alpine AS builder 2 | 3 | COPY . /app 4 | WORKDIR /app 5 | 6 | RUN wget -qO- https://get.pnpm.io/install.sh | ENV="$HOME/.shrc" SHELL="$(which sh)" sh -; \ 7 | source /root/.shrc; \ 8 | pnpm install; \ 9 | pnpm build; 10 | 11 | FROM nginxinc/nginx-unprivileged:alpine3.20 12 | 13 | COPY --from=builder /app/build /usr/share/nginx/html 14 | COPY 01-sub.sh /docker-entrypoint.d/ 15 | USER nginx -------------------------------------------------------------------------------- /ui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ui", 3 | "private": true, 4 | "version": "0.0.1", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite dev --host", 8 | "build": "vite build", 9 | "preview": "vite preview --host", 10 | "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", 11 | "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch" 12 | }, 13 | "devDependencies": { 14 | "@number-flow/svelte": "^0.3.3", 15 | "@poppanator/sveltekit-svg": "^5.0.0", 16 | "@sveltejs/adapter-auto": "^4.0.0", 17 | "@sveltejs/adapter-static": "^3.0.8", 18 | "@sveltejs/kit": "^2.17.1", 19 | "@sveltejs/vite-plugin-svelte": "^5.0.3", 20 | "@tailwindcss/vite": "^4.0.8", 21 | "autoprefixer": "^10.4.20", 22 | "debounce": "^2.2.0", 23 | "flowbite": "^3.1.2", 24 | "flowbite-svelte": "^0.47.4", 25 | "svelte": "^5.20.2", 26 | "svelte-check": "^4.1.4", 27 | "tailwindcss": "^4.0.6", 28 | "typescript": "^5.7.3", 29 | "vite": "^6.1.0" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /ui/pnpm-lock.yaml: -------------------------------------------------------------------------------- 1 | lockfileVersion: '9.0' 2 | 3 | settings: 4 | autoInstallPeers: true 5 | excludeLinksFromLockfile: false 6 | 7 | importers: 8 | 9 | .: 10 | devDependencies: 11 | '@number-flow/svelte': 12 | specifier: ^0.3.3 13 | version: 0.3.3(svelte@5.20.2) 14 | '@poppanator/sveltekit-svg': 15 | specifier: ^5.0.0 16 | version: 5.0.0(rollup@4.34.6)(svelte@5.20.2)(svgo@3.3.2)(vite@6.1.0(jiti@2.4.2)(lightningcss@1.29.1)(yaml@2.7.0)) 17 | '@sveltejs/adapter-auto': 18 | specifier: ^4.0.0 19 | version: 4.0.0(@sveltejs/kit@2.17.1(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.20.2)(vite@6.1.0(jiti@2.4.2)(lightningcss@1.29.1)(yaml@2.7.0)))(svelte@5.20.2)(vite@6.1.0(jiti@2.4.2)(lightningcss@1.29.1)(yaml@2.7.0))) 20 | '@sveltejs/adapter-static': 21 | specifier: ^3.0.8 22 | version: 3.0.8(@sveltejs/kit@2.17.1(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.20.2)(vite@6.1.0(jiti@2.4.2)(lightningcss@1.29.1)(yaml@2.7.0)))(svelte@5.20.2)(vite@6.1.0(jiti@2.4.2)(lightningcss@1.29.1)(yaml@2.7.0))) 23 | '@sveltejs/kit': 24 | specifier: ^2.17.1 25 | version: 2.17.1(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.20.2)(vite@6.1.0(jiti@2.4.2)(lightningcss@1.29.1)(yaml@2.7.0)))(svelte@5.20.2)(vite@6.1.0(jiti@2.4.2)(lightningcss@1.29.1)(yaml@2.7.0)) 26 | '@sveltejs/vite-plugin-svelte': 27 | specifier: ^5.0.3 28 | version: 5.0.3(svelte@5.20.2)(vite@6.1.0(jiti@2.4.2)(lightningcss@1.29.1)(yaml@2.7.0)) 29 | '@tailwindcss/vite': 30 | specifier: ^4.0.8 31 | version: 4.0.8(vite@6.1.0(jiti@2.4.2)(lightningcss@1.29.1)(yaml@2.7.0)) 32 | autoprefixer: 33 | specifier: ^10.4.20 34 | version: 10.4.20(postcss@8.5.2) 35 | debounce: 36 | specifier: ^2.2.0 37 | version: 2.2.0 38 | flowbite: 39 | specifier: ^3.1.2 40 | version: 3.1.2(rollup@4.34.6) 41 | flowbite-svelte: 42 | specifier: ^0.47.4 43 | version: 0.47.4(rollup@4.34.6)(svelte@5.20.2) 44 | svelte: 45 | specifier: ^5.20.2 46 | version: 5.20.2 47 | svelte-check: 48 | specifier: ^4.1.4 49 | version: 4.1.4(picomatch@4.0.2)(svelte@5.20.2)(typescript@5.7.3) 50 | tailwindcss: 51 | specifier: ^4.0.6 52 | version: 4.0.6 53 | typescript: 54 | specifier: ^5.7.3 55 | version: 5.7.3 56 | vite: 57 | specifier: ^6.1.0 58 | version: 6.1.0(jiti@2.4.2)(lightningcss@1.29.1)(yaml@2.7.0) 59 | 60 | packages: 61 | 62 | '@ampproject/remapping@2.3.0': 63 | resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} 64 | engines: {node: '>=6.0.0'} 65 | 66 | '@esbuild/aix-ppc64@0.24.2': 67 | resolution: {integrity: sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA==} 68 | engines: {node: '>=18'} 69 | cpu: [ppc64] 70 | os: [aix] 71 | 72 | '@esbuild/android-arm64@0.24.2': 73 | resolution: {integrity: sha512-cNLgeqCqV8WxfcTIOeL4OAtSmL8JjcN6m09XIgro1Wi7cF4t/THaWEa7eL5CMoMBdjoHOTh/vwTO/o2TRXIyzg==} 74 | engines: {node: '>=18'} 75 | cpu: [arm64] 76 | os: [android] 77 | 78 | '@esbuild/android-arm@0.24.2': 79 | resolution: {integrity: sha512-tmwl4hJkCfNHwFB3nBa8z1Uy3ypZpxqxfTQOcHX+xRByyYgunVbZ9MzUUfb0RxaHIMnbHagwAxuTL+tnNM+1/Q==} 80 | engines: {node: '>=18'} 81 | cpu: [arm] 82 | os: [android] 83 | 84 | '@esbuild/android-x64@0.24.2': 85 | resolution: {integrity: sha512-B6Q0YQDqMx9D7rvIcsXfmJfvUYLoP722bgfBlO5cGvNVb5V/+Y7nhBE3mHV9OpxBf4eAS2S68KZztiPaWq4XYw==} 86 | engines: {node: '>=18'} 87 | cpu: [x64] 88 | os: [android] 89 | 90 | '@esbuild/darwin-arm64@0.24.2': 91 | resolution: {integrity: sha512-kj3AnYWc+CekmZnS5IPu9D+HWtUI49hbnyqk0FLEJDbzCIQt7hg7ucF1SQAilhtYpIujfaHr6O0UHlzzSPdOeA==} 92 | engines: {node: '>=18'} 93 | cpu: [arm64] 94 | os: [darwin] 95 | 96 | '@esbuild/darwin-x64@0.24.2': 97 | resolution: {integrity: sha512-WeSrmwwHaPkNR5H3yYfowhZcbriGqooyu3zI/3GGpF8AyUdsrrP0X6KumITGA9WOyiJavnGZUwPGvxvwfWPHIA==} 98 | engines: {node: '>=18'} 99 | cpu: [x64] 100 | os: [darwin] 101 | 102 | '@esbuild/freebsd-arm64@0.24.2': 103 | resolution: {integrity: sha512-UN8HXjtJ0k/Mj6a9+5u6+2eZ2ERD7Edt1Q9IZiB5UZAIdPnVKDoG7mdTVGhHJIeEml60JteamR3qhsr1r8gXvg==} 104 | engines: {node: '>=18'} 105 | cpu: [arm64] 106 | os: [freebsd] 107 | 108 | '@esbuild/freebsd-x64@0.24.2': 109 | resolution: {integrity: sha512-TvW7wE/89PYW+IevEJXZ5sF6gJRDY/14hyIGFXdIucxCsbRmLUcjseQu1SyTko+2idmCw94TgyaEZi9HUSOe3Q==} 110 | engines: {node: '>=18'} 111 | cpu: [x64] 112 | os: [freebsd] 113 | 114 | '@esbuild/linux-arm64@0.24.2': 115 | resolution: {integrity: sha512-7HnAD6074BW43YvvUmE/35Id9/NB7BeX5EoNkK9obndmZBUk8xmJJeU7DwmUeN7tkysslb2eSl6CTrYz6oEMQg==} 116 | engines: {node: '>=18'} 117 | cpu: [arm64] 118 | os: [linux] 119 | 120 | '@esbuild/linux-arm@0.24.2': 121 | resolution: {integrity: sha512-n0WRM/gWIdU29J57hJyUdIsk0WarGd6To0s+Y+LwvlC55wt+GT/OgkwoXCXvIue1i1sSNWblHEig00GBWiJgfA==} 122 | engines: {node: '>=18'} 123 | cpu: [arm] 124 | os: [linux] 125 | 126 | '@esbuild/linux-ia32@0.24.2': 127 | resolution: {integrity: sha512-sfv0tGPQhcZOgTKO3oBE9xpHuUqguHvSo4jl+wjnKwFpapx+vUDcawbwPNuBIAYdRAvIDBfZVvXprIj3HA+Ugw==} 128 | engines: {node: '>=18'} 129 | cpu: [ia32] 130 | os: [linux] 131 | 132 | '@esbuild/linux-loong64@0.24.2': 133 | resolution: {integrity: sha512-CN9AZr8kEndGooS35ntToZLTQLHEjtVB5n7dl8ZcTZMonJ7CCfStrYhrzF97eAecqVbVJ7APOEe18RPI4KLhwQ==} 134 | engines: {node: '>=18'} 135 | cpu: [loong64] 136 | os: [linux] 137 | 138 | '@esbuild/linux-mips64el@0.24.2': 139 | resolution: {integrity: sha512-iMkk7qr/wl3exJATwkISxI7kTcmHKE+BlymIAbHO8xanq/TjHaaVThFF6ipWzPHryoFsesNQJPE/3wFJw4+huw==} 140 | engines: {node: '>=18'} 141 | cpu: [mips64el] 142 | os: [linux] 143 | 144 | '@esbuild/linux-ppc64@0.24.2': 145 | resolution: {integrity: sha512-shsVrgCZ57Vr2L8mm39kO5PPIb+843FStGt7sGGoqiiWYconSxwTiuswC1VJZLCjNiMLAMh34jg4VSEQb+iEbw==} 146 | engines: {node: '>=18'} 147 | cpu: [ppc64] 148 | os: [linux] 149 | 150 | '@esbuild/linux-riscv64@0.24.2': 151 | resolution: {integrity: sha512-4eSFWnU9Hhd68fW16GD0TINewo1L6dRrB+oLNNbYyMUAeOD2yCK5KXGK1GH4qD/kT+bTEXjsyTCiJGHPZ3eM9Q==} 152 | engines: {node: '>=18'} 153 | cpu: [riscv64] 154 | os: [linux] 155 | 156 | '@esbuild/linux-s390x@0.24.2': 157 | resolution: {integrity: sha512-S0Bh0A53b0YHL2XEXC20bHLuGMOhFDO6GN4b3YjRLK//Ep3ql3erpNcPlEFed93hsQAjAQDNsvcK+hV90FubSw==} 158 | engines: {node: '>=18'} 159 | cpu: [s390x] 160 | os: [linux] 161 | 162 | '@esbuild/linux-x64@0.24.2': 163 | resolution: {integrity: sha512-8Qi4nQcCTbLnK9WoMjdC9NiTG6/E38RNICU6sUNqK0QFxCYgoARqVqxdFmWkdonVsvGqWhmm7MO0jyTqLqwj0Q==} 164 | engines: {node: '>=18'} 165 | cpu: [x64] 166 | os: [linux] 167 | 168 | '@esbuild/netbsd-arm64@0.24.2': 169 | resolution: {integrity: sha512-wuLK/VztRRpMt9zyHSazyCVdCXlpHkKm34WUyinD2lzK07FAHTq0KQvZZlXikNWkDGoT6x3TD51jKQ7gMVpopw==} 170 | engines: {node: '>=18'} 171 | cpu: [arm64] 172 | os: [netbsd] 173 | 174 | '@esbuild/netbsd-x64@0.24.2': 175 | resolution: {integrity: sha512-VefFaQUc4FMmJuAxmIHgUmfNiLXY438XrL4GDNV1Y1H/RW3qow68xTwjZKfj/+Plp9NANmzbH5R40Meudu8mmw==} 176 | engines: {node: '>=18'} 177 | cpu: [x64] 178 | os: [netbsd] 179 | 180 | '@esbuild/openbsd-arm64@0.24.2': 181 | resolution: {integrity: sha512-YQbi46SBct6iKnszhSvdluqDmxCJA+Pu280Av9WICNwQmMxV7nLRHZfjQzwbPs3jeWnuAhE9Jy0NrnJ12Oz+0A==} 182 | engines: {node: '>=18'} 183 | cpu: [arm64] 184 | os: [openbsd] 185 | 186 | '@esbuild/openbsd-x64@0.24.2': 187 | resolution: {integrity: sha512-+iDS6zpNM6EnJyWv0bMGLWSWeXGN/HTaF/LXHXHwejGsVi+ooqDfMCCTerNFxEkM3wYVcExkeGXNqshc9iMaOA==} 188 | engines: {node: '>=18'} 189 | cpu: [x64] 190 | os: [openbsd] 191 | 192 | '@esbuild/sunos-x64@0.24.2': 193 | resolution: {integrity: sha512-hTdsW27jcktEvpwNHJU4ZwWFGkz2zRJUz8pvddmXPtXDzVKTTINmlmga3ZzwcuMpUvLw7JkLy9QLKyGpD2Yxig==} 194 | engines: {node: '>=18'} 195 | cpu: [x64] 196 | os: [sunos] 197 | 198 | '@esbuild/win32-arm64@0.24.2': 199 | resolution: {integrity: sha512-LihEQ2BBKVFLOC9ZItT9iFprsE9tqjDjnbulhHoFxYQtQfai7qfluVODIYxt1PgdoyQkz23+01rzwNwYfutxUQ==} 200 | engines: {node: '>=18'} 201 | cpu: [arm64] 202 | os: [win32] 203 | 204 | '@esbuild/win32-ia32@0.24.2': 205 | resolution: {integrity: sha512-q+iGUwfs8tncmFC9pcnD5IvRHAzmbwQ3GPS5/ceCyHdjXubwQWI12MKWSNSMYLJMq23/IUCvJMS76PDqXe1fxA==} 206 | engines: {node: '>=18'} 207 | cpu: [ia32] 208 | os: [win32] 209 | 210 | '@esbuild/win32-x64@0.24.2': 211 | resolution: {integrity: sha512-7VTgWzgMGvup6aSqDPLiW5zHaxYJGTO4OokMjIlrCtf+VpEL+cXKtCvg723iguPYI5oaUNdS+/V7OU2gvXVWEg==} 212 | engines: {node: '>=18'} 213 | cpu: [x64] 214 | os: [win32] 215 | 216 | '@floating-ui/core@1.6.9': 217 | resolution: {integrity: sha512-uMXCuQ3BItDUbAMhIXw7UPXRfAlOAvZzdK9BWpE60MCn+Svt3aLn9jsPTi/WNGlRUu2uI0v5S7JiIUsbsvh3fw==} 218 | 219 | '@floating-ui/dom@1.6.13': 220 | resolution: {integrity: sha512-umqzocjDgNRGTuO7Q8CU32dkHkECqI8ZdMZ5Swb6QAM0t5rnlrN3lGo1hdpscRd3WS8T6DKYK4ephgIH9iRh3w==} 221 | 222 | '@floating-ui/utils@0.2.9': 223 | resolution: {integrity: sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==} 224 | 225 | '@jridgewell/gen-mapping@0.3.8': 226 | resolution: {integrity: sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==} 227 | engines: {node: '>=6.0.0'} 228 | 229 | '@jridgewell/resolve-uri@3.1.2': 230 | resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} 231 | engines: {node: '>=6.0.0'} 232 | 233 | '@jridgewell/set-array@1.2.1': 234 | resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==} 235 | engines: {node: '>=6.0.0'} 236 | 237 | '@jridgewell/sourcemap-codec@1.5.0': 238 | resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} 239 | 240 | '@jridgewell/trace-mapping@0.3.25': 241 | resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} 242 | 243 | '@number-flow/svelte@0.3.3': 244 | resolution: {integrity: sha512-wo5tiU1qKc74k61sNx8GwvnmZ7yKdbfj1onjzaAa67HRXFClRcrfLlXXatBSQ/uK2XTz9156ILs3VmpuSG1sFA==} 245 | peerDependencies: 246 | svelte: ^4 || ^5 247 | 248 | '@polka/url@1.0.0-next.28': 249 | resolution: {integrity: sha512-8LduaNlMZGwdZ6qWrKlfa+2M4gahzFkprZiAt2TF8uS0qQgBizKXpXURqvTJ4WtmupWxaLqjRb2UCTe72mu+Aw==} 250 | 251 | '@poppanator/sveltekit-svg@5.0.0': 252 | resolution: {integrity: sha512-b7hk55SF0HjTS+xFgMG20hy6W0F/m+yRA/ZWcjnsa391rB3Ys3desCiUyIKQYcfvcyuRiQCPedUJMYgu00VdCA==} 253 | peerDependencies: 254 | svelte: '>=5.x' 255 | svgo: '>=3.x' 256 | vite: '>=5.x' 257 | 258 | '@popperjs/core@2.11.8': 259 | resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==} 260 | 261 | '@rollup/plugin-node-resolve@15.3.1': 262 | resolution: {integrity: sha512-tgg6b91pAybXHJQMAAwW9VuWBO6Thi+q7BCNARLwSqlmsHz0XYURtGvh/AuwSADXSI4h/2uHbs7s4FzlZDGSGA==} 263 | engines: {node: '>=14.0.0'} 264 | peerDependencies: 265 | rollup: ^2.78.0||^3.0.0||^4.0.0 266 | peerDependenciesMeta: 267 | rollup: 268 | optional: true 269 | 270 | '@rollup/pluginutils@5.1.4': 271 | resolution: {integrity: sha512-USm05zrsFxYLPdWWq+K3STlWiT/3ELn3RcV5hJMghpeAIhxfsUIg6mt12CBJBInWMV4VneoV7SfGv8xIwo2qNQ==} 272 | engines: {node: '>=14.0.0'} 273 | peerDependencies: 274 | rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 275 | peerDependenciesMeta: 276 | rollup: 277 | optional: true 278 | 279 | '@rollup/rollup-android-arm-eabi@4.34.6': 280 | resolution: {integrity: sha512-+GcCXtOQoWuC7hhX1P00LqjjIiS/iOouHXhMdiDSnq/1DGTox4SpUvO52Xm+div6+106r+TcvOeo/cxvyEyTgg==} 281 | cpu: [arm] 282 | os: [android] 283 | 284 | '@rollup/rollup-android-arm64@4.34.6': 285 | resolution: {integrity: sha512-E8+2qCIjciYUnCa1AiVF1BkRgqIGW9KzJeesQqVfyRITGQN+dFuoivO0hnro1DjT74wXLRZ7QF8MIbz+luGaJA==} 286 | cpu: [arm64] 287 | os: [android] 288 | 289 | '@rollup/rollup-darwin-arm64@4.34.6': 290 | resolution: {integrity: sha512-z9Ib+OzqN3DZEjX7PDQMHEhtF+t6Mi2z/ueChQPLS/qUMKY7Ybn5A2ggFoKRNRh1q1T03YTQfBTQCJZiepESAg==} 291 | cpu: [arm64] 292 | os: [darwin] 293 | 294 | '@rollup/rollup-darwin-x64@4.34.6': 295 | resolution: {integrity: sha512-PShKVY4u0FDAR7jskyFIYVyHEPCPnIQY8s5OcXkdU8mz3Y7eXDJPdyM/ZWjkYdR2m0izD9HHWA8sGcXn+Qrsyg==} 296 | cpu: [x64] 297 | os: [darwin] 298 | 299 | '@rollup/rollup-freebsd-arm64@4.34.6': 300 | resolution: {integrity: sha512-YSwyOqlDAdKqs0iKuqvRHLN4SrD2TiswfoLfvYXseKbL47ht1grQpq46MSiQAx6rQEN8o8URtpXARCpqabqxGQ==} 301 | cpu: [arm64] 302 | os: [freebsd] 303 | 304 | '@rollup/rollup-freebsd-x64@4.34.6': 305 | resolution: {integrity: sha512-HEP4CgPAY1RxXwwL5sPFv6BBM3tVeLnshF03HMhJYCNc6kvSqBgTMmsEjb72RkZBAWIqiPUyF1JpEBv5XT9wKQ==} 306 | cpu: [x64] 307 | os: [freebsd] 308 | 309 | '@rollup/rollup-linux-arm-gnueabihf@4.34.6': 310 | resolution: {integrity: sha512-88fSzjC5xeH9S2Vg3rPgXJULkHcLYMkh8faix8DX4h4TIAL65ekwuQMA/g2CXq8W+NJC43V6fUpYZNjaX3+IIg==} 311 | cpu: [arm] 312 | os: [linux] 313 | 314 | '@rollup/rollup-linux-arm-musleabihf@4.34.6': 315 | resolution: {integrity: sha512-wM4ztnutBqYFyvNeR7Av+reWI/enK9tDOTKNF+6Kk2Q96k9bwhDDOlnCUNRPvromlVXo04riSliMBs/Z7RteEg==} 316 | cpu: [arm] 317 | os: [linux] 318 | 319 | '@rollup/rollup-linux-arm64-gnu@4.34.6': 320 | resolution: {integrity: sha512-9RyprECbRa9zEjXLtvvshhw4CMrRa3K+0wcp3KME0zmBe1ILmvcVHnypZ/aIDXpRyfhSYSuN4EPdCCj5Du8FIA==} 321 | cpu: [arm64] 322 | os: [linux] 323 | 324 | '@rollup/rollup-linux-arm64-musl@4.34.6': 325 | resolution: {integrity: sha512-qTmklhCTyaJSB05S+iSovfo++EwnIEZxHkzv5dep4qoszUMX5Ca4WM4zAVUMbfdviLgCSQOu5oU8YoGk1s6M9Q==} 326 | cpu: [arm64] 327 | os: [linux] 328 | 329 | '@rollup/rollup-linux-loongarch64-gnu@4.34.6': 330 | resolution: {integrity: sha512-4Qmkaps9yqmpjY5pvpkfOerYgKNUGzQpFxV6rnS7c/JfYbDSU0y6WpbbredB5cCpLFGJEqYX40WUmxMkwhWCjw==} 331 | cpu: [loong64] 332 | os: [linux] 333 | 334 | '@rollup/rollup-linux-powerpc64le-gnu@4.34.6': 335 | resolution: {integrity: sha512-Zsrtux3PuaxuBTX/zHdLaFmcofWGzaWW1scwLU3ZbW/X+hSsFbz9wDIp6XvnT7pzYRl9MezWqEqKy7ssmDEnuQ==} 336 | cpu: [ppc64] 337 | os: [linux] 338 | 339 | '@rollup/rollup-linux-riscv64-gnu@4.34.6': 340 | resolution: {integrity: sha512-aK+Zp+CRM55iPrlyKiU3/zyhgzWBxLVrw2mwiQSYJRobCURb781+XstzvA8Gkjg/hbdQFuDw44aUOxVQFycrAg==} 341 | cpu: [riscv64] 342 | os: [linux] 343 | 344 | '@rollup/rollup-linux-s390x-gnu@4.34.6': 345 | resolution: {integrity: sha512-WoKLVrY9ogmaYPXwTH326+ErlCIgMmsoRSx6bO+l68YgJnlOXhygDYSZe/qbUJCSiCiZAQ+tKm88NcWuUXqOzw==} 346 | cpu: [s390x] 347 | os: [linux] 348 | 349 | '@rollup/rollup-linux-x64-gnu@4.34.6': 350 | resolution: {integrity: sha512-Sht4aFvmA4ToHd2vFzwMFaQCiYm2lDFho5rPcvPBT5pCdC+GwHG6CMch4GQfmWTQ1SwRKS0dhDYb54khSrjDWw==} 351 | cpu: [x64] 352 | os: [linux] 353 | 354 | '@rollup/rollup-linux-x64-musl@4.34.6': 355 | resolution: {integrity: sha512-zmmpOQh8vXc2QITsnCiODCDGXFC8LMi64+/oPpPx5qz3pqv0s6x46ps4xoycfUiVZps5PFn1gksZzo4RGTKT+A==} 356 | cpu: [x64] 357 | os: [linux] 358 | 359 | '@rollup/rollup-win32-arm64-msvc@4.34.6': 360 | resolution: {integrity: sha512-3/q1qUsO/tLqGBaD4uXsB6coVGB3usxw3qyeVb59aArCgedSF66MPdgRStUd7vbZOsko/CgVaY5fo2vkvPLWiA==} 361 | cpu: [arm64] 362 | os: [win32] 363 | 364 | '@rollup/rollup-win32-ia32-msvc@4.34.6': 365 | resolution: {integrity: sha512-oLHxuyywc6efdKVTxvc0135zPrRdtYVjtVD5GUm55I3ODxhU/PwkQFD97z16Xzxa1Fz0AEe4W/2hzRtd+IfpOA==} 366 | cpu: [ia32] 367 | os: [win32] 368 | 369 | '@rollup/rollup-win32-x64-msvc@4.34.6': 370 | resolution: {integrity: sha512-0PVwmgzZ8+TZ9oGBmdZoQVXflbvuwzN/HRclujpl4N/q3i+y0lqLw8n1bXA8ru3sApDjlmONaNAuYr38y1Kr9w==} 371 | cpu: [x64] 372 | os: [win32] 373 | 374 | '@sveltejs/adapter-auto@4.0.0': 375 | resolution: {integrity: sha512-kmuYSQdD2AwThymQF0haQhM8rE5rhutQXG4LNbnbShwhMO4qQGnKaaTy+88DuNSuoQDi58+thpq8XpHc1+oEKQ==} 376 | peerDependencies: 377 | '@sveltejs/kit': ^2.0.0 378 | 379 | '@sveltejs/adapter-static@3.0.8': 380 | resolution: {integrity: sha512-YaDrquRpZwfcXbnlDsSrBQNCChVOT9MGuSg+dMAyfsAa1SmiAhrA5jUYUiIMC59G92kIbY/AaQOWcBdq+lh+zg==} 381 | peerDependencies: 382 | '@sveltejs/kit': ^2.0.0 383 | 384 | '@sveltejs/kit@2.17.1': 385 | resolution: {integrity: sha512-CpoGSLqE2MCmcQwA2CWJvOsZ9vW+p/1H3itrFykdgajUNAEyQPbsaSn7fZb6PLHQwe+07njxje9ss0fjZoCAyw==} 386 | engines: {node: '>=18.13'} 387 | hasBin: true 388 | peerDependencies: 389 | '@sveltejs/vite-plugin-svelte': ^3.0.0 || ^4.0.0-next.1 || ^5.0.0 390 | svelte: ^4.0.0 || ^5.0.0-next.0 391 | vite: ^5.0.3 || ^6.0.0 392 | 393 | '@sveltejs/vite-plugin-svelte-inspector@4.0.1': 394 | resolution: {integrity: sha512-J/Nmb2Q2y7mck2hyCX4ckVHcR5tu2J+MtBEQqpDrrgELZ2uvraQcK/ioCV61AqkdXFgriksOKIceDcQmqnGhVw==} 395 | engines: {node: ^18.0.0 || ^20.0.0 || >=22} 396 | peerDependencies: 397 | '@sveltejs/vite-plugin-svelte': ^5.0.0 398 | svelte: ^5.0.0 399 | vite: ^6.0.0 400 | 401 | '@sveltejs/vite-plugin-svelte@5.0.3': 402 | resolution: {integrity: sha512-MCFS6CrQDu1yGwspm4qtli0e63vaPCehf6V7pIMP15AsWgMKrqDGCPFF/0kn4SP0ii4aySu4Pa62+fIRGFMjgw==} 403 | engines: {node: ^18.0.0 || ^20.0.0 || >=22} 404 | peerDependencies: 405 | svelte: ^5.0.0 406 | vite: ^6.0.0 407 | 408 | '@tailwindcss/node@4.0.8': 409 | resolution: {integrity: sha512-FKArQpbrbwv08TNT0k7ejYXpF+R8knZFAatNc0acOxbgeqLzwb86r+P3LGOjIeI3Idqe9CVkZrh4GlsJLJKkkw==} 410 | 411 | '@tailwindcss/oxide-android-arm64@4.0.8': 412 | resolution: {integrity: sha512-We7K79+Sm4mwJHk26Yzu/GAj7C7myemm7PeXvpgMxyxO70SSFSL3uCcqFbz9JA5M5UPkrl7N9fkBe/Y0iazqpA==} 413 | engines: {node: '>= 10'} 414 | cpu: [arm64] 415 | os: [android] 416 | 417 | '@tailwindcss/oxide-darwin-arm64@4.0.8': 418 | resolution: {integrity: sha512-Lv9Isi2EwkCTG1sRHNDi0uRNN1UGFdEThUAGFrydRmQZnraGLMjN8gahzg2FFnOizDl7LB2TykLUuiw833DSNg==} 419 | engines: {node: '>= 10'} 420 | cpu: [arm64] 421 | os: [darwin] 422 | 423 | '@tailwindcss/oxide-darwin-x64@4.0.8': 424 | resolution: {integrity: sha512-fWfywfYIlSWtKoqWTjukTHLWV3ARaBRjXCC2Eo0l6KVpaqGY4c2y8snUjp1xpxUtpqwMvCvFWFaleMoz1Vhzlw==} 425 | engines: {node: '>= 10'} 426 | cpu: [x64] 427 | os: [darwin] 428 | 429 | '@tailwindcss/oxide-freebsd-x64@4.0.8': 430 | resolution: {integrity: sha512-SO+dyvjJV9G94bnmq2288Ke0BIdvrbSbvtPLaQdqjqHR83v5L2fWADyFO+1oecHo9Owsk8MxcXh1agGVPIKIqw==} 431 | engines: {node: '>= 10'} 432 | cpu: [x64] 433 | os: [freebsd] 434 | 435 | '@tailwindcss/oxide-linux-arm-gnueabihf@4.0.8': 436 | resolution: {integrity: sha512-ZSHggWiEblQNV69V0qUK5vuAtHP+I+S2eGrKGJ5lPgwgJeAd6GjLsVBN+Mqn2SPVfYM3BOpS9jX/zVg9RWQVDQ==} 437 | engines: {node: '>= 10'} 438 | cpu: [arm] 439 | os: [linux] 440 | 441 | '@tailwindcss/oxide-linux-arm64-gnu@4.0.8': 442 | resolution: {integrity: sha512-xWpr6M0OZLDNsr7+bQz+3X7zcnDJZJ1N9gtBWCtfhkEtDjjxYEp+Lr5L5nc/yXlL4MyCHnn0uonGVXy3fhxaVA==} 443 | engines: {node: '>= 10'} 444 | cpu: [arm64] 445 | os: [linux] 446 | 447 | '@tailwindcss/oxide-linux-arm64-musl@4.0.8': 448 | resolution: {integrity: sha512-5tz2IL7LN58ssGEq7h/staD7pu/izF/KeMWdlJ86WDe2Ah46LF3ET6ZGKTr5eZMrnEA0M9cVFuSPprKRHNgjeg==} 449 | engines: {node: '>= 10'} 450 | cpu: [arm64] 451 | os: [linux] 452 | 453 | '@tailwindcss/oxide-linux-x64-gnu@4.0.8': 454 | resolution: {integrity: sha512-KSzMkhyrxAQyY2o194NKVKU9j/c+NFSoMvnHWFaNHKi3P1lb+Vq1UC19tLHrmxSkKapcMMu69D7+G1+FVGNDXQ==} 455 | engines: {node: '>= 10'} 456 | cpu: [x64] 457 | os: [linux] 458 | 459 | '@tailwindcss/oxide-linux-x64-musl@4.0.8': 460 | resolution: {integrity: sha512-yFYKG5UtHTRimjtqxUWXBgI4Tc6NJe3USjRIVdlTczpLRxq/SFwgzGl5JbatCxgSRDPBFwRrNPxq+ukfQFGdrw==} 461 | engines: {node: '>= 10'} 462 | cpu: [x64] 463 | os: [linux] 464 | 465 | '@tailwindcss/oxide-win32-arm64-msvc@4.0.8': 466 | resolution: {integrity: sha512-tndGujmCSba85cRCnQzXgpA2jx5gXimyspsUYae5jlPyLRG0RjXbDshFKOheVXU4TLflo7FSG8EHCBJ0EHTKdQ==} 467 | engines: {node: '>= 10'} 468 | cpu: [arm64] 469 | os: [win32] 470 | 471 | '@tailwindcss/oxide-win32-x64-msvc@4.0.8': 472 | resolution: {integrity: sha512-T77jroAc0p4EHVVgTUiNeFn6Nj3jtD3IeNId2X+0k+N1XxfNipy81BEkYErpKLiOkNhpNFjPee8/ZVas29b2OQ==} 473 | engines: {node: '>= 10'} 474 | cpu: [x64] 475 | os: [win32] 476 | 477 | '@tailwindcss/oxide@4.0.8': 478 | resolution: {integrity: sha512-KfMcuAu/Iw+DcV1e8twrFyr2yN8/ZDC/odIGta4wuuJOGkrkHZbvJvRNIbQNhGh7erZTYV6Ie0IeD6WC9Y8Hcw==} 479 | engines: {node: '>= 10'} 480 | 481 | '@tailwindcss/vite@4.0.8': 482 | resolution: {integrity: sha512-+SAq44yLzYlzyrb7QTcFCdU8Xa7FOA0jp+Xby7fPMUie+MY9HhJysM7Vp+vL8qIp8ceQJfLD+FjgJuJ4lL6nyg==} 483 | peerDependencies: 484 | vite: ^5.2.0 || ^6 485 | 486 | '@trysound/sax@0.2.0': 487 | resolution: {integrity: sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==} 488 | engines: {node: '>=10.13.0'} 489 | 490 | '@types/cookie@0.6.0': 491 | resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==} 492 | 493 | '@types/estree@1.0.6': 494 | resolution: {integrity: sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==} 495 | 496 | '@types/resolve@1.20.2': 497 | resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==} 498 | 499 | '@yr/monotone-cubic-spline@1.0.3': 500 | resolution: {integrity: sha512-FQXkOta0XBSUPHndIKON2Y9JeQz5ZeMqLYZVVK93FliNBFm7LNMIZmY6FrMEB9XPcDbE2bekMbZD6kzDkxwYjA==} 501 | 502 | acorn-typescript@1.4.13: 503 | resolution: {integrity: sha512-xsc9Xv0xlVfwp2o7sQ+GCQ1PgbkdcpWdTzrwXxO3xDMTAywVS3oXVOcOHuRjAPkS4P9b+yc/qNF15460v+jp4Q==} 504 | peerDependencies: 505 | acorn: '>=8.9.0' 506 | 507 | acorn@8.14.0: 508 | resolution: {integrity: sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==} 509 | engines: {node: '>=0.4.0'} 510 | hasBin: true 511 | 512 | apexcharts@3.54.1: 513 | resolution: {integrity: sha512-E4et0h/J1U3r3EwS/WlqJCQIbepKbp6wGUmaAwJOMjHUP4Ci0gxanLa7FR3okx6p9coi4st6J853/Cb1NP0vpA==} 514 | 515 | aria-query@5.3.2: 516 | resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==} 517 | engines: {node: '>= 0.4'} 518 | 519 | autoprefixer@10.4.20: 520 | resolution: {integrity: sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g==} 521 | engines: {node: ^10 || ^12 || >=14} 522 | hasBin: true 523 | peerDependencies: 524 | postcss: ^8.1.0 525 | 526 | axobject-query@4.1.0: 527 | resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} 528 | engines: {node: '>= 0.4'} 529 | 530 | boolbase@1.0.0: 531 | resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} 532 | 533 | browserslist@4.24.4: 534 | resolution: {integrity: sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==} 535 | engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} 536 | hasBin: true 537 | 538 | caniuse-lite@1.0.30001695: 539 | resolution: {integrity: sha512-vHyLade6wTgI2u1ec3WQBxv+2BrTERV28UXQu9LO6lZ9pYeMk34vjXFLOxo1A4UBA8XTL4njRQZdno/yYaSmWw==} 540 | 541 | chokidar@4.0.3: 542 | resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} 543 | engines: {node: '>= 14.16.0'} 544 | 545 | clsx@2.1.1: 546 | resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} 547 | engines: {node: '>=6'} 548 | 549 | commander@7.2.0: 550 | resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==} 551 | engines: {node: '>= 10'} 552 | 553 | cookie@0.6.0: 554 | resolution: {integrity: sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==} 555 | engines: {node: '>= 0.6'} 556 | 557 | css-select@5.1.0: 558 | resolution: {integrity: sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==} 559 | 560 | css-tree@2.2.1: 561 | resolution: {integrity: sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==} 562 | engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0, npm: '>=7.0.0'} 563 | 564 | css-tree@2.3.1: 565 | resolution: {integrity: sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==} 566 | engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} 567 | 568 | css-what@6.1.0: 569 | resolution: {integrity: sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==} 570 | engines: {node: '>= 6'} 571 | 572 | csso@5.0.5: 573 | resolution: {integrity: sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==} 574 | engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0, npm: '>=7.0.0'} 575 | 576 | debounce@2.2.0: 577 | resolution: {integrity: sha512-Xks6RUDLZFdz8LIdR6q0MTH44k7FikOmnh5xkSjMig6ch45afc8sjTjRQf3P6ax8dMgcQrYO/AR2RGWURrruqw==} 578 | engines: {node: '>=18'} 579 | 580 | debug@4.4.0: 581 | resolution: {integrity: sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==} 582 | engines: {node: '>=6.0'} 583 | peerDependencies: 584 | supports-color: '*' 585 | peerDependenciesMeta: 586 | supports-color: 587 | optional: true 588 | 589 | deepmerge@4.3.1: 590 | resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} 591 | engines: {node: '>=0.10.0'} 592 | 593 | detect-libc@1.0.3: 594 | resolution: {integrity: sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==} 595 | engines: {node: '>=0.10'} 596 | hasBin: true 597 | 598 | devalue@5.1.1: 599 | resolution: {integrity: sha512-maua5KUiapvEwiEAe+XnlZ3Rh0GD+qI1J/nb9vrJc3muPXvcF/8gXYTWF76+5DAqHyDUtOIImEuo0YKE9mshVw==} 600 | 601 | dom-serializer@2.0.0: 602 | resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==} 603 | 604 | domelementtype@2.3.0: 605 | resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} 606 | 607 | domhandler@5.0.3: 608 | resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} 609 | engines: {node: '>= 4'} 610 | 611 | domutils@3.2.2: 612 | resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==} 613 | 614 | electron-to-chromium@1.5.84: 615 | resolution: {integrity: sha512-I+DQ8xgafao9Ha6y0qjHHvpZ9OfyA1qKlkHkjywxzniORU2awxyz7f/iVJcULmrF2yrM3nHQf+iDjJtbbexd/g==} 616 | 617 | enhanced-resolve@5.18.1: 618 | resolution: {integrity: sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==} 619 | engines: {node: '>=10.13.0'} 620 | 621 | entities@4.5.0: 622 | resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} 623 | engines: {node: '>=0.12'} 624 | 625 | esbuild@0.24.2: 626 | resolution: {integrity: sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA==} 627 | engines: {node: '>=18'} 628 | hasBin: true 629 | 630 | escalade@3.2.0: 631 | resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} 632 | engines: {node: '>=6'} 633 | 634 | esm-env@1.2.2: 635 | resolution: {integrity: sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==} 636 | 637 | esrap@1.4.5: 638 | resolution: {integrity: sha512-CjNMjkBWWZeHn+VX+gS8YvFwJ5+NDhg8aWZBSFJPR8qQduDNjbJodA2WcwCm7uQa5Rjqj+nZvVmceg1RbHFB9g==} 639 | 640 | estree-walker@2.0.2: 641 | resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} 642 | 643 | fdir@6.4.3: 644 | resolution: {integrity: sha512-PMXmW2y1hDDfTSRc9gaXIuCCRpuoz3Kaz8cUelp3smouvfT632ozg2vrT6lJsHKKOF59YLbOGfAWGUcKEfRMQw==} 645 | peerDependencies: 646 | picomatch: ^3 || ^4 647 | peerDependenciesMeta: 648 | picomatch: 649 | optional: true 650 | 651 | flowbite-datepicker@1.3.2: 652 | resolution: {integrity: sha512-6Nfm0MCVX3mpaR7YSCjmEO2GO8CDt6CX8ZpQnGdeu03WUCWtEPQ/uy0PUiNtIJjJZWnX0Cm3H55MOhbD1g+E/g==} 653 | 654 | flowbite-svelte@0.47.4: 655 | resolution: {integrity: sha512-8oiY/oeWA7fgkDF91MZKEBo5VmjL8El3wuqTDWAFO1j7p45BHIL6G1VGnnidgCEYlbADDQN9BIGCvyPq4J3g+w==} 656 | peerDependencies: 657 | svelte: ^3.55.1 || ^4.0.0 || ^5.0.0 658 | 659 | flowbite@2.5.2: 660 | resolution: {integrity: sha512-kwFD3n8/YW4EG8GlY3Od9IoKND97kitO+/ejISHSqpn3vw2i5K/+ZI8Jm2V+KC4fGdnfi0XZ+TzYqQb4Q1LshA==} 661 | 662 | flowbite@3.1.2: 663 | resolution: {integrity: sha512-MkwSgbbybCYgMC+go6Da5idEKUFfMqc/AmSjm/2ZbdmvoKf5frLPq/eIhXc9P+rC8t9boZtUXzHDgt5whZ6A/Q==} 664 | 665 | fraction.js@4.3.7: 666 | resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==} 667 | 668 | fsevents@2.3.3: 669 | resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} 670 | engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} 671 | os: [darwin] 672 | 673 | function-bind@1.1.2: 674 | resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} 675 | 676 | graceful-fs@4.2.11: 677 | resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} 678 | 679 | hasown@2.0.2: 680 | resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} 681 | engines: {node: '>= 0.4'} 682 | 683 | import-meta-resolve@4.1.0: 684 | resolution: {integrity: sha512-I6fiaX09Xivtk+THaMfAwnA3MVA5Big1WHF1Dfx9hFuvNIWpXnorlkzhcQf6ehrqQiiZECRt1poOAkPmer3ruw==} 685 | 686 | is-core-module@2.16.1: 687 | resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} 688 | engines: {node: '>= 0.4'} 689 | 690 | is-module@1.0.0: 691 | resolution: {integrity: sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==} 692 | 693 | is-reference@3.0.3: 694 | resolution: {integrity: sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==} 695 | 696 | jiti@2.4.2: 697 | resolution: {integrity: sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==} 698 | hasBin: true 699 | 700 | kleur@4.1.5: 701 | resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} 702 | engines: {node: '>=6'} 703 | 704 | lightningcss-darwin-arm64@1.29.1: 705 | resolution: {integrity: sha512-HtR5XJ5A0lvCqYAoSv2QdZZyoHNttBpa5EP9aNuzBQeKGfbyH5+UipLWvVzpP4Uml5ej4BYs5I9Lco9u1fECqw==} 706 | engines: {node: '>= 12.0.0'} 707 | cpu: [arm64] 708 | os: [darwin] 709 | 710 | lightningcss-darwin-x64@1.29.1: 711 | resolution: {integrity: sha512-k33G9IzKUpHy/J/3+9MCO4e+PzaFblsgBjSGlpAaFikeBFm8B/CkO3cKU9oI4g+fjS2KlkLM/Bza9K/aw8wsNA==} 712 | engines: {node: '>= 12.0.0'} 713 | cpu: [x64] 714 | os: [darwin] 715 | 716 | lightningcss-freebsd-x64@1.29.1: 717 | resolution: {integrity: sha512-0SUW22fv/8kln2LnIdOCmSuXnxgxVC276W5KLTwoehiO0hxkacBxjHOL5EtHD8BAXg2BvuhsJPmVMasvby3LiQ==} 718 | engines: {node: '>= 12.0.0'} 719 | cpu: [x64] 720 | os: [freebsd] 721 | 722 | lightningcss-linux-arm-gnueabihf@1.29.1: 723 | resolution: {integrity: sha512-sD32pFvlR0kDlqsOZmYqH/68SqUMPNj+0pucGxToXZi4XZgZmqeX/NkxNKCPsswAXU3UeYgDSpGhu05eAufjDg==} 724 | engines: {node: '>= 12.0.0'} 725 | cpu: [arm] 726 | os: [linux] 727 | 728 | lightningcss-linux-arm64-gnu@1.29.1: 729 | resolution: {integrity: sha512-0+vClRIZ6mmJl/dxGuRsE197o1HDEeeRk6nzycSy2GofC2JsY4ifCRnvUWf/CUBQmlrvMzt6SMQNMSEu22csWQ==} 730 | engines: {node: '>= 12.0.0'} 731 | cpu: [arm64] 732 | os: [linux] 733 | 734 | lightningcss-linux-arm64-musl@1.29.1: 735 | resolution: {integrity: sha512-UKMFrG4rL/uHNgelBsDwJcBqVpzNJbzsKkbI3Ja5fg00sgQnHw/VrzUTEc4jhZ+AN2BvQYz/tkHu4vt1kLuJyw==} 736 | engines: {node: '>= 12.0.0'} 737 | cpu: [arm64] 738 | os: [linux] 739 | 740 | lightningcss-linux-x64-gnu@1.29.1: 741 | resolution: {integrity: sha512-u1S+xdODy/eEtjADqirA774y3jLcm8RPtYztwReEXoZKdzgsHYPl0s5V52Tst+GKzqjebkULT86XMSxejzfISw==} 742 | engines: {node: '>= 12.0.0'} 743 | cpu: [x64] 744 | os: [linux] 745 | 746 | lightningcss-linux-x64-musl@1.29.1: 747 | resolution: {integrity: sha512-L0Tx0DtaNUTzXv0lbGCLB/c/qEADanHbu4QdcNOXLIe1i8i22rZRpbT3gpWYsCh9aSL9zFujY/WmEXIatWvXbw==} 748 | engines: {node: '>= 12.0.0'} 749 | cpu: [x64] 750 | os: [linux] 751 | 752 | lightningcss-win32-arm64-msvc@1.29.1: 753 | resolution: {integrity: sha512-QoOVnkIEFfbW4xPi+dpdft/zAKmgLgsRHfJalEPYuJDOWf7cLQzYg0DEh8/sn737FaeMJxHZRc1oBreiwZCjog==} 754 | engines: {node: '>= 12.0.0'} 755 | cpu: [arm64] 756 | os: [win32] 757 | 758 | lightningcss-win32-x64-msvc@1.29.1: 759 | resolution: {integrity: sha512-NygcbThNBe4JElP+olyTI/doBNGJvLs3bFCRPdvuCcxZCcCZ71B858IHpdm7L1btZex0FvCmM17FK98Y9MRy1Q==} 760 | engines: {node: '>= 12.0.0'} 761 | cpu: [x64] 762 | os: [win32] 763 | 764 | lightningcss@1.29.1: 765 | resolution: {integrity: sha512-FmGoeD4S05ewj+AkhTY+D+myDvXI6eL27FjHIjoyUkO/uw7WZD1fBVs0QxeYWa7E17CUHJaYX/RUGISCtcrG4Q==} 766 | engines: {node: '>= 12.0.0'} 767 | 768 | locate-character@3.0.0: 769 | resolution: {integrity: sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==} 770 | 771 | magic-string@0.30.17: 772 | resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==} 773 | 774 | mdn-data@2.0.28: 775 | resolution: {integrity: sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==} 776 | 777 | mdn-data@2.0.30: 778 | resolution: {integrity: sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==} 779 | 780 | mini-svg-data-uri@1.4.4: 781 | resolution: {integrity: sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg==} 782 | hasBin: true 783 | 784 | mri@1.2.0: 785 | resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} 786 | engines: {node: '>=4'} 787 | 788 | mrmime@2.0.0: 789 | resolution: {integrity: sha512-eu38+hdgojoyq63s+yTpN4XMBdt5l8HhMhc4VKLO9KM5caLIBvUm4thi7fFaxyTmCKeNnXZ5pAlBwCUnhA09uw==} 790 | engines: {node: '>=10'} 791 | 792 | ms@2.1.3: 793 | resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} 794 | 795 | nanoid@3.3.8: 796 | resolution: {integrity: sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==} 797 | engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} 798 | hasBin: true 799 | 800 | node-releases@2.0.19: 801 | resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==} 802 | 803 | normalize-range@0.1.2: 804 | resolution: {integrity: sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==} 805 | engines: {node: '>=0.10.0'} 806 | 807 | nth-check@2.1.1: 808 | resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} 809 | 810 | number-flow@0.5.3: 811 | resolution: {integrity: sha512-iLKyssImNWQmJ41rza9K7P5lHRZTyishi/9FarWPLQHYY2Ydtl6eiXINEjZ1fa8dHeY0O7+YOD+Py3ZsJddYkg==} 812 | 813 | path-parse@1.0.7: 814 | resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} 815 | 816 | picocolors@1.1.1: 817 | resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} 818 | 819 | picomatch@4.0.2: 820 | resolution: {integrity: sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==} 821 | engines: {node: '>=12'} 822 | 823 | postcss-value-parser@4.2.0: 824 | resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} 825 | 826 | postcss@8.5.1: 827 | resolution: {integrity: sha512-6oz2beyjc5VMn/KV1pPw8fliQkhBXrVn1Z3TVyqZxU8kZpzEKhBdmCFqI6ZbmGtamQvQGuU1sgPTk8ZrXDD7jQ==} 828 | engines: {node: ^10 || ^12 || >=14} 829 | 830 | postcss@8.5.2: 831 | resolution: {integrity: sha512-MjOadfU3Ys9KYoX0AdkBlFEF1Vx37uCCeN4ZHnmwm9FfpbsGWMZeBLMmmpY+6Ocqod7mkdZ0DT31OlbsFrLlkA==} 832 | engines: {node: ^10 || ^12 || >=14} 833 | 834 | readdirp@4.1.1: 835 | resolution: {integrity: sha512-h80JrZu/MHUZCyHu5ciuoI0+WxsCxzxJTILn6Fs8rxSnFPh+UVHYfeIxK1nVGugMqkfC4vJcBOYbkfkwYK0+gw==} 836 | engines: {node: '>= 14.18.0'} 837 | 838 | resolve@1.22.10: 839 | resolution: {integrity: sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==} 840 | engines: {node: '>= 0.4'} 841 | hasBin: true 842 | 843 | rollup@4.34.6: 844 | resolution: {integrity: sha512-wc2cBWqJgkU3Iz5oztRkQbfVkbxoz5EhnCGOrnJvnLnQ7O0WhQUYyv18qQI79O8L7DdHrrlJNeCHd4VGpnaXKQ==} 845 | engines: {node: '>=18.0.0', npm: '>=8.0.0'} 846 | hasBin: true 847 | 848 | sade@1.8.1: 849 | resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==} 850 | engines: {node: '>=6'} 851 | 852 | set-cookie-parser@2.7.1: 853 | resolution: {integrity: sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==} 854 | 855 | sirv@3.0.0: 856 | resolution: {integrity: sha512-BPwJGUeDaDCHihkORDchNyyTvWFhcusy1XMmhEVTQTwGeybFbp8YEmB+njbPnth1FibULBSBVwCQni25XlCUDg==} 857 | engines: {node: '>=18'} 858 | 859 | source-map-js@1.2.1: 860 | resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} 861 | engines: {node: '>=0.10.0'} 862 | 863 | supports-preserve-symlinks-flag@1.0.0: 864 | resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} 865 | engines: {node: '>= 0.4'} 866 | 867 | svelte-check@4.1.4: 868 | resolution: {integrity: sha512-v0j7yLbT29MezzaQJPEDwksybTE2Ups9rUxEXy92T06TiA0cbqcO8wAOwNUVkFW6B0hsYHA+oAX3BS8b/2oHtw==} 869 | engines: {node: '>= 18.0.0'} 870 | hasBin: true 871 | peerDependencies: 872 | svelte: ^4.0.0 || ^5.0.0-next.0 873 | typescript: '>=5.0.0' 874 | 875 | svelte@5.20.2: 876 | resolution: {integrity: sha512-aYXJreNUiyTob0QOzRZeBXZMGeFZDch6SrSRV8QTncZb6zj0O3BEdUzPpojuHQ1pTvk+KX7I6rZCXPUf8pTPxA==} 877 | engines: {node: '>=18'} 878 | 879 | svg.draggable.js@2.2.2: 880 | resolution: {integrity: sha512-JzNHBc2fLQMzYCZ90KZHN2ohXL0BQJGQimK1kGk6AvSeibuKcIdDX9Kr0dT9+UJ5O8nYA0RB839Lhvk4CY4MZw==} 881 | engines: {node: '>= 0.8.0'} 882 | 883 | svg.easing.js@2.0.0: 884 | resolution: {integrity: sha512-//ctPdJMGy22YoYGV+3HEfHbm6/69LJUTAqI2/5qBvaNHZ9uUFVC82B0Pl299HzgH13rKrBgi4+XyXXyVWWthA==} 885 | engines: {node: '>= 0.8.0'} 886 | 887 | svg.filter.js@2.0.2: 888 | resolution: {integrity: sha512-xkGBwU+dKBzqg5PtilaTb0EYPqPfJ9Q6saVldX+5vCRy31P6TlRCP3U9NxH3HEufkKkpNgdTLBJnmhDHeTqAkw==} 889 | engines: {node: '>= 0.8.0'} 890 | 891 | svg.js@2.7.1: 892 | resolution: {integrity: sha512-ycbxpizEQktk3FYvn/8BH+6/EuWXg7ZpQREJvgacqn46gIddG24tNNe4Son6omdXCnSOaApnpZw6MPCBA1dODA==} 893 | 894 | svg.pathmorphing.js@0.1.3: 895 | resolution: {integrity: sha512-49HWI9X4XQR/JG1qXkSDV8xViuTLIWm/B/7YuQELV5KMOPtXjiwH4XPJvr/ghEDibmLQ9Oc22dpWpG0vUDDNww==} 896 | engines: {node: '>= 0.8.0'} 897 | 898 | svg.resize.js@1.4.3: 899 | resolution: {integrity: sha512-9k5sXJuPKp+mVzXNvxz7U0uC9oVMQrrf7cFsETznzUDDm0x8+77dtZkWdMfRlmbkEEYvUn9btKuZ3n41oNA+uw==} 900 | engines: {node: '>= 0.8.0'} 901 | 902 | svg.select.js@2.1.2: 903 | resolution: {integrity: sha512-tH6ABEyJsAOVAhwcCjF8mw4crjXSI1aa7j2VQR8ZuJ37H2MBUbyeqYr5nEO7sSN3cy9AR9DUwNg0t/962HlDbQ==} 904 | engines: {node: '>= 0.8.0'} 905 | 906 | svg.select.js@3.0.1: 907 | resolution: {integrity: sha512-h5IS/hKkuVCbKSieR9uQCj9w+zLHoPh+ce19bBYyqF53g6mnPB8sAtIbe1s9dh2S2fCmYX2xel1Ln3PJBbK4kw==} 908 | engines: {node: '>= 0.8.0'} 909 | 910 | svgo@3.3.2: 911 | resolution: {integrity: sha512-OoohrmuUlBs8B8o6MB2Aevn+pRIH9zDALSR+6hhqVfa6fRwG/Qw9VUMSMW9VNg2CFc/MTIfabtdOVl9ODIJjpw==} 912 | engines: {node: '>=14.0.0'} 913 | hasBin: true 914 | 915 | tailwind-merge@2.6.0: 916 | resolution: {integrity: sha512-P+Vu1qXfzediirmHOC3xKGAYeZtPcV9g76X+xg2FD4tYgR71ewMA35Y3sCz3zhiN/dwefRpJX0yBcgwi1fXNQA==} 917 | 918 | tailwindcss@4.0.6: 919 | resolution: {integrity: sha512-mysewHYJKaXgNOW6pp5xon/emCsfAMnO8WMaGKZZ35fomnR/T5gYnRg2/yRTTrtXiEl1tiVkeRt0eMO6HxEZqw==} 920 | 921 | tailwindcss@4.0.8: 922 | resolution: {integrity: sha512-Me7N5CKR+D2A1xdWA5t5+kjjT7bwnxZOE6/yDI/ixJdJokszsn2n++mdU5yJwrsTpqFX2B9ZNMBJDwcqk9C9lw==} 923 | 924 | tapable@2.2.1: 925 | resolution: {integrity: sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==} 926 | engines: {node: '>=6'} 927 | 928 | totalist@3.0.1: 929 | resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} 930 | engines: {node: '>=6'} 931 | 932 | typescript@5.7.3: 933 | resolution: {integrity: sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==} 934 | engines: {node: '>=14.17'} 935 | hasBin: true 936 | 937 | update-browserslist-db@1.1.2: 938 | resolution: {integrity: sha512-PPypAm5qvlD7XMZC3BujecnaOxwhrtoFR+Dqkk5Aa/6DssiH0ibKoketaj9w8LP7Bont1rYeoV5plxD7RTEPRg==} 939 | hasBin: true 940 | peerDependencies: 941 | browserslist: '>= 4.21.0' 942 | 943 | vite@6.1.0: 944 | resolution: {integrity: sha512-RjjMipCKVoR4hVfPY6GQTgveinjNuyLw+qruksLDvA5ktI1150VmcMBKmQaEWJhg/j6Uaf6dNCNA0AfdzUb/hQ==} 945 | engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} 946 | hasBin: true 947 | peerDependencies: 948 | '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 949 | jiti: '>=1.21.0' 950 | less: '*' 951 | lightningcss: ^1.21.0 952 | sass: '*' 953 | sass-embedded: '*' 954 | stylus: '*' 955 | sugarss: '*' 956 | terser: ^5.16.0 957 | tsx: ^4.8.1 958 | yaml: ^2.4.2 959 | peerDependenciesMeta: 960 | '@types/node': 961 | optional: true 962 | jiti: 963 | optional: true 964 | less: 965 | optional: true 966 | lightningcss: 967 | optional: true 968 | sass: 969 | optional: true 970 | sass-embedded: 971 | optional: true 972 | stylus: 973 | optional: true 974 | sugarss: 975 | optional: true 976 | terser: 977 | optional: true 978 | tsx: 979 | optional: true 980 | yaml: 981 | optional: true 982 | 983 | vitefu@1.0.5: 984 | resolution: {integrity: sha512-h4Vflt9gxODPFNGPwp4zAMZRpZR7eslzwH2c5hn5kNZ5rhnKyRJ50U+yGCdc2IRaBs8O4haIgLNGrV5CrpMsCA==} 985 | peerDependencies: 986 | vite: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 987 | peerDependenciesMeta: 988 | vite: 989 | optional: true 990 | 991 | yaml@2.7.0: 992 | resolution: {integrity: sha512-+hSoy/QHluxmC9kCIJyL/uyFmLmc+e5CFR5Wa+bpIhIj85LVb9ZH2nVnqrHoSvKogwODv0ClqZkmiSSaIH5LTA==} 993 | engines: {node: '>= 14'} 994 | hasBin: true 995 | 996 | zimmerframe@1.1.2: 997 | resolution: {integrity: sha512-rAbqEGa8ovJy4pyBxZM70hg4pE6gDgaQ0Sl9M3enG3I0d6H4XSAM3GeNGLKnsBpuijUow064sf7ww1nutC5/3w==} 998 | 999 | snapshots: 1000 | 1001 | '@ampproject/remapping@2.3.0': 1002 | dependencies: 1003 | '@jridgewell/gen-mapping': 0.3.8 1004 | '@jridgewell/trace-mapping': 0.3.25 1005 | 1006 | '@esbuild/aix-ppc64@0.24.2': 1007 | optional: true 1008 | 1009 | '@esbuild/android-arm64@0.24.2': 1010 | optional: true 1011 | 1012 | '@esbuild/android-arm@0.24.2': 1013 | optional: true 1014 | 1015 | '@esbuild/android-x64@0.24.2': 1016 | optional: true 1017 | 1018 | '@esbuild/darwin-arm64@0.24.2': 1019 | optional: true 1020 | 1021 | '@esbuild/darwin-x64@0.24.2': 1022 | optional: true 1023 | 1024 | '@esbuild/freebsd-arm64@0.24.2': 1025 | optional: true 1026 | 1027 | '@esbuild/freebsd-x64@0.24.2': 1028 | optional: true 1029 | 1030 | '@esbuild/linux-arm64@0.24.2': 1031 | optional: true 1032 | 1033 | '@esbuild/linux-arm@0.24.2': 1034 | optional: true 1035 | 1036 | '@esbuild/linux-ia32@0.24.2': 1037 | optional: true 1038 | 1039 | '@esbuild/linux-loong64@0.24.2': 1040 | optional: true 1041 | 1042 | '@esbuild/linux-mips64el@0.24.2': 1043 | optional: true 1044 | 1045 | '@esbuild/linux-ppc64@0.24.2': 1046 | optional: true 1047 | 1048 | '@esbuild/linux-riscv64@0.24.2': 1049 | optional: true 1050 | 1051 | '@esbuild/linux-s390x@0.24.2': 1052 | optional: true 1053 | 1054 | '@esbuild/linux-x64@0.24.2': 1055 | optional: true 1056 | 1057 | '@esbuild/netbsd-arm64@0.24.2': 1058 | optional: true 1059 | 1060 | '@esbuild/netbsd-x64@0.24.2': 1061 | optional: true 1062 | 1063 | '@esbuild/openbsd-arm64@0.24.2': 1064 | optional: true 1065 | 1066 | '@esbuild/openbsd-x64@0.24.2': 1067 | optional: true 1068 | 1069 | '@esbuild/sunos-x64@0.24.2': 1070 | optional: true 1071 | 1072 | '@esbuild/win32-arm64@0.24.2': 1073 | optional: true 1074 | 1075 | '@esbuild/win32-ia32@0.24.2': 1076 | optional: true 1077 | 1078 | '@esbuild/win32-x64@0.24.2': 1079 | optional: true 1080 | 1081 | '@floating-ui/core@1.6.9': 1082 | dependencies: 1083 | '@floating-ui/utils': 0.2.9 1084 | 1085 | '@floating-ui/dom@1.6.13': 1086 | dependencies: 1087 | '@floating-ui/core': 1.6.9 1088 | '@floating-ui/utils': 0.2.9 1089 | 1090 | '@floating-ui/utils@0.2.9': {} 1091 | 1092 | '@jridgewell/gen-mapping@0.3.8': 1093 | dependencies: 1094 | '@jridgewell/set-array': 1.2.1 1095 | '@jridgewell/sourcemap-codec': 1.5.0 1096 | '@jridgewell/trace-mapping': 0.3.25 1097 | 1098 | '@jridgewell/resolve-uri@3.1.2': {} 1099 | 1100 | '@jridgewell/set-array@1.2.1': {} 1101 | 1102 | '@jridgewell/sourcemap-codec@1.5.0': {} 1103 | 1104 | '@jridgewell/trace-mapping@0.3.25': 1105 | dependencies: 1106 | '@jridgewell/resolve-uri': 3.1.2 1107 | '@jridgewell/sourcemap-codec': 1.5.0 1108 | 1109 | '@number-flow/svelte@0.3.3(svelte@5.20.2)': 1110 | dependencies: 1111 | esm-env: 1.2.2 1112 | number-flow: 0.5.3 1113 | svelte: 5.20.2 1114 | 1115 | '@polka/url@1.0.0-next.28': {} 1116 | 1117 | '@poppanator/sveltekit-svg@5.0.0(rollup@4.34.6)(svelte@5.20.2)(svgo@3.3.2)(vite@6.1.0(jiti@2.4.2)(lightningcss@1.29.1)(yaml@2.7.0))': 1118 | dependencies: 1119 | '@rollup/pluginutils': 5.1.4(rollup@4.34.6) 1120 | svelte: 5.20.2 1121 | svgo: 3.3.2 1122 | vite: 6.1.0(jiti@2.4.2)(lightningcss@1.29.1)(yaml@2.7.0) 1123 | transitivePeerDependencies: 1124 | - rollup 1125 | 1126 | '@popperjs/core@2.11.8': {} 1127 | 1128 | '@rollup/plugin-node-resolve@15.3.1(rollup@4.34.6)': 1129 | dependencies: 1130 | '@rollup/pluginutils': 5.1.4(rollup@4.34.6) 1131 | '@types/resolve': 1.20.2 1132 | deepmerge: 4.3.1 1133 | is-module: 1.0.0 1134 | resolve: 1.22.10 1135 | optionalDependencies: 1136 | rollup: 4.34.6 1137 | 1138 | '@rollup/pluginutils@5.1.4(rollup@4.34.6)': 1139 | dependencies: 1140 | '@types/estree': 1.0.6 1141 | estree-walker: 2.0.2 1142 | picomatch: 4.0.2 1143 | optionalDependencies: 1144 | rollup: 4.34.6 1145 | 1146 | '@rollup/rollup-android-arm-eabi@4.34.6': 1147 | optional: true 1148 | 1149 | '@rollup/rollup-android-arm64@4.34.6': 1150 | optional: true 1151 | 1152 | '@rollup/rollup-darwin-arm64@4.34.6': 1153 | optional: true 1154 | 1155 | '@rollup/rollup-darwin-x64@4.34.6': 1156 | optional: true 1157 | 1158 | '@rollup/rollup-freebsd-arm64@4.34.6': 1159 | optional: true 1160 | 1161 | '@rollup/rollup-freebsd-x64@4.34.6': 1162 | optional: true 1163 | 1164 | '@rollup/rollup-linux-arm-gnueabihf@4.34.6': 1165 | optional: true 1166 | 1167 | '@rollup/rollup-linux-arm-musleabihf@4.34.6': 1168 | optional: true 1169 | 1170 | '@rollup/rollup-linux-arm64-gnu@4.34.6': 1171 | optional: true 1172 | 1173 | '@rollup/rollup-linux-arm64-musl@4.34.6': 1174 | optional: true 1175 | 1176 | '@rollup/rollup-linux-loongarch64-gnu@4.34.6': 1177 | optional: true 1178 | 1179 | '@rollup/rollup-linux-powerpc64le-gnu@4.34.6': 1180 | optional: true 1181 | 1182 | '@rollup/rollup-linux-riscv64-gnu@4.34.6': 1183 | optional: true 1184 | 1185 | '@rollup/rollup-linux-s390x-gnu@4.34.6': 1186 | optional: true 1187 | 1188 | '@rollup/rollup-linux-x64-gnu@4.34.6': 1189 | optional: true 1190 | 1191 | '@rollup/rollup-linux-x64-musl@4.34.6': 1192 | optional: true 1193 | 1194 | '@rollup/rollup-win32-arm64-msvc@4.34.6': 1195 | optional: true 1196 | 1197 | '@rollup/rollup-win32-ia32-msvc@4.34.6': 1198 | optional: true 1199 | 1200 | '@rollup/rollup-win32-x64-msvc@4.34.6': 1201 | optional: true 1202 | 1203 | '@sveltejs/adapter-auto@4.0.0(@sveltejs/kit@2.17.1(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.20.2)(vite@6.1.0(jiti@2.4.2)(lightningcss@1.29.1)(yaml@2.7.0)))(svelte@5.20.2)(vite@6.1.0(jiti@2.4.2)(lightningcss@1.29.1)(yaml@2.7.0)))': 1204 | dependencies: 1205 | '@sveltejs/kit': 2.17.1(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.20.2)(vite@6.1.0(jiti@2.4.2)(lightningcss@1.29.1)(yaml@2.7.0)))(svelte@5.20.2)(vite@6.1.0(jiti@2.4.2)(lightningcss@1.29.1)(yaml@2.7.0)) 1206 | import-meta-resolve: 4.1.0 1207 | 1208 | '@sveltejs/adapter-static@3.0.8(@sveltejs/kit@2.17.1(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.20.2)(vite@6.1.0(jiti@2.4.2)(lightningcss@1.29.1)(yaml@2.7.0)))(svelte@5.20.2)(vite@6.1.0(jiti@2.4.2)(lightningcss@1.29.1)(yaml@2.7.0)))': 1209 | dependencies: 1210 | '@sveltejs/kit': 2.17.1(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.20.2)(vite@6.1.0(jiti@2.4.2)(lightningcss@1.29.1)(yaml@2.7.0)))(svelte@5.20.2)(vite@6.1.0(jiti@2.4.2)(lightningcss@1.29.1)(yaml@2.7.0)) 1211 | 1212 | '@sveltejs/kit@2.17.1(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.20.2)(vite@6.1.0(jiti@2.4.2)(lightningcss@1.29.1)(yaml@2.7.0)))(svelte@5.20.2)(vite@6.1.0(jiti@2.4.2)(lightningcss@1.29.1)(yaml@2.7.0))': 1213 | dependencies: 1214 | '@sveltejs/vite-plugin-svelte': 5.0.3(svelte@5.20.2)(vite@6.1.0(jiti@2.4.2)(lightningcss@1.29.1)(yaml@2.7.0)) 1215 | '@types/cookie': 0.6.0 1216 | cookie: 0.6.0 1217 | devalue: 5.1.1 1218 | esm-env: 1.2.2 1219 | import-meta-resolve: 4.1.0 1220 | kleur: 4.1.5 1221 | magic-string: 0.30.17 1222 | mrmime: 2.0.0 1223 | sade: 1.8.1 1224 | set-cookie-parser: 2.7.1 1225 | sirv: 3.0.0 1226 | svelte: 5.20.2 1227 | vite: 6.1.0(jiti@2.4.2)(lightningcss@1.29.1)(yaml@2.7.0) 1228 | 1229 | '@sveltejs/vite-plugin-svelte-inspector@4.0.1(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.20.2)(vite@6.1.0(jiti@2.4.2)(lightningcss@1.29.1)(yaml@2.7.0)))(svelte@5.20.2)(vite@6.1.0(jiti@2.4.2)(lightningcss@1.29.1)(yaml@2.7.0))': 1230 | dependencies: 1231 | '@sveltejs/vite-plugin-svelte': 5.0.3(svelte@5.20.2)(vite@6.1.0(jiti@2.4.2)(lightningcss@1.29.1)(yaml@2.7.0)) 1232 | debug: 4.4.0 1233 | svelte: 5.20.2 1234 | vite: 6.1.0(jiti@2.4.2)(lightningcss@1.29.1)(yaml@2.7.0) 1235 | transitivePeerDependencies: 1236 | - supports-color 1237 | 1238 | '@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.20.2)(vite@6.1.0(jiti@2.4.2)(lightningcss@1.29.1)(yaml@2.7.0))': 1239 | dependencies: 1240 | '@sveltejs/vite-plugin-svelte-inspector': 4.0.1(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.20.2)(vite@6.1.0(jiti@2.4.2)(lightningcss@1.29.1)(yaml@2.7.0)))(svelte@5.20.2)(vite@6.1.0(jiti@2.4.2)(lightningcss@1.29.1)(yaml@2.7.0)) 1241 | debug: 4.4.0 1242 | deepmerge: 4.3.1 1243 | kleur: 4.1.5 1244 | magic-string: 0.30.17 1245 | svelte: 5.20.2 1246 | vite: 6.1.0(jiti@2.4.2)(lightningcss@1.29.1)(yaml@2.7.0) 1247 | vitefu: 1.0.5(vite@6.1.0(jiti@2.4.2)(lightningcss@1.29.1)(yaml@2.7.0)) 1248 | transitivePeerDependencies: 1249 | - supports-color 1250 | 1251 | '@tailwindcss/node@4.0.8': 1252 | dependencies: 1253 | enhanced-resolve: 5.18.1 1254 | jiti: 2.4.2 1255 | tailwindcss: 4.0.8 1256 | 1257 | '@tailwindcss/oxide-android-arm64@4.0.8': 1258 | optional: true 1259 | 1260 | '@tailwindcss/oxide-darwin-arm64@4.0.8': 1261 | optional: true 1262 | 1263 | '@tailwindcss/oxide-darwin-x64@4.0.8': 1264 | optional: true 1265 | 1266 | '@tailwindcss/oxide-freebsd-x64@4.0.8': 1267 | optional: true 1268 | 1269 | '@tailwindcss/oxide-linux-arm-gnueabihf@4.0.8': 1270 | optional: true 1271 | 1272 | '@tailwindcss/oxide-linux-arm64-gnu@4.0.8': 1273 | optional: true 1274 | 1275 | '@tailwindcss/oxide-linux-arm64-musl@4.0.8': 1276 | optional: true 1277 | 1278 | '@tailwindcss/oxide-linux-x64-gnu@4.0.8': 1279 | optional: true 1280 | 1281 | '@tailwindcss/oxide-linux-x64-musl@4.0.8': 1282 | optional: true 1283 | 1284 | '@tailwindcss/oxide-win32-arm64-msvc@4.0.8': 1285 | optional: true 1286 | 1287 | '@tailwindcss/oxide-win32-x64-msvc@4.0.8': 1288 | optional: true 1289 | 1290 | '@tailwindcss/oxide@4.0.8': 1291 | optionalDependencies: 1292 | '@tailwindcss/oxide-android-arm64': 4.0.8 1293 | '@tailwindcss/oxide-darwin-arm64': 4.0.8 1294 | '@tailwindcss/oxide-darwin-x64': 4.0.8 1295 | '@tailwindcss/oxide-freebsd-x64': 4.0.8 1296 | '@tailwindcss/oxide-linux-arm-gnueabihf': 4.0.8 1297 | '@tailwindcss/oxide-linux-arm64-gnu': 4.0.8 1298 | '@tailwindcss/oxide-linux-arm64-musl': 4.0.8 1299 | '@tailwindcss/oxide-linux-x64-gnu': 4.0.8 1300 | '@tailwindcss/oxide-linux-x64-musl': 4.0.8 1301 | '@tailwindcss/oxide-win32-arm64-msvc': 4.0.8 1302 | '@tailwindcss/oxide-win32-x64-msvc': 4.0.8 1303 | 1304 | '@tailwindcss/vite@4.0.8(vite@6.1.0(jiti@2.4.2)(lightningcss@1.29.1)(yaml@2.7.0))': 1305 | dependencies: 1306 | '@tailwindcss/node': 4.0.8 1307 | '@tailwindcss/oxide': 4.0.8 1308 | lightningcss: 1.29.1 1309 | tailwindcss: 4.0.8 1310 | vite: 6.1.0(jiti@2.4.2)(lightningcss@1.29.1)(yaml@2.7.0) 1311 | 1312 | '@trysound/sax@0.2.0': {} 1313 | 1314 | '@types/cookie@0.6.0': {} 1315 | 1316 | '@types/estree@1.0.6': {} 1317 | 1318 | '@types/resolve@1.20.2': {} 1319 | 1320 | '@yr/monotone-cubic-spline@1.0.3': {} 1321 | 1322 | acorn-typescript@1.4.13(acorn@8.14.0): 1323 | dependencies: 1324 | acorn: 8.14.0 1325 | 1326 | acorn@8.14.0: {} 1327 | 1328 | apexcharts@3.54.1: 1329 | dependencies: 1330 | '@yr/monotone-cubic-spline': 1.0.3 1331 | svg.draggable.js: 2.2.2 1332 | svg.easing.js: 2.0.0 1333 | svg.filter.js: 2.0.2 1334 | svg.pathmorphing.js: 0.1.3 1335 | svg.resize.js: 1.4.3 1336 | svg.select.js: 3.0.1 1337 | 1338 | aria-query@5.3.2: {} 1339 | 1340 | autoprefixer@10.4.20(postcss@8.5.2): 1341 | dependencies: 1342 | browserslist: 4.24.4 1343 | caniuse-lite: 1.0.30001695 1344 | fraction.js: 4.3.7 1345 | normalize-range: 0.1.2 1346 | picocolors: 1.1.1 1347 | postcss: 8.5.2 1348 | postcss-value-parser: 4.2.0 1349 | 1350 | axobject-query@4.1.0: {} 1351 | 1352 | boolbase@1.0.0: {} 1353 | 1354 | browserslist@4.24.4: 1355 | dependencies: 1356 | caniuse-lite: 1.0.30001695 1357 | electron-to-chromium: 1.5.84 1358 | node-releases: 2.0.19 1359 | update-browserslist-db: 1.1.2(browserslist@4.24.4) 1360 | 1361 | caniuse-lite@1.0.30001695: {} 1362 | 1363 | chokidar@4.0.3: 1364 | dependencies: 1365 | readdirp: 4.1.1 1366 | 1367 | clsx@2.1.1: {} 1368 | 1369 | commander@7.2.0: {} 1370 | 1371 | cookie@0.6.0: {} 1372 | 1373 | css-select@5.1.0: 1374 | dependencies: 1375 | boolbase: 1.0.0 1376 | css-what: 6.1.0 1377 | domhandler: 5.0.3 1378 | domutils: 3.2.2 1379 | nth-check: 2.1.1 1380 | 1381 | css-tree@2.2.1: 1382 | dependencies: 1383 | mdn-data: 2.0.28 1384 | source-map-js: 1.2.1 1385 | 1386 | css-tree@2.3.1: 1387 | dependencies: 1388 | mdn-data: 2.0.30 1389 | source-map-js: 1.2.1 1390 | 1391 | css-what@6.1.0: {} 1392 | 1393 | csso@5.0.5: 1394 | dependencies: 1395 | css-tree: 2.2.1 1396 | 1397 | debounce@2.2.0: {} 1398 | 1399 | debug@4.4.0: 1400 | dependencies: 1401 | ms: 2.1.3 1402 | 1403 | deepmerge@4.3.1: {} 1404 | 1405 | detect-libc@1.0.3: {} 1406 | 1407 | devalue@5.1.1: {} 1408 | 1409 | dom-serializer@2.0.0: 1410 | dependencies: 1411 | domelementtype: 2.3.0 1412 | domhandler: 5.0.3 1413 | entities: 4.5.0 1414 | 1415 | domelementtype@2.3.0: {} 1416 | 1417 | domhandler@5.0.3: 1418 | dependencies: 1419 | domelementtype: 2.3.0 1420 | 1421 | domutils@3.2.2: 1422 | dependencies: 1423 | dom-serializer: 2.0.0 1424 | domelementtype: 2.3.0 1425 | domhandler: 5.0.3 1426 | 1427 | electron-to-chromium@1.5.84: {} 1428 | 1429 | enhanced-resolve@5.18.1: 1430 | dependencies: 1431 | graceful-fs: 4.2.11 1432 | tapable: 2.2.1 1433 | 1434 | entities@4.5.0: {} 1435 | 1436 | esbuild@0.24.2: 1437 | optionalDependencies: 1438 | '@esbuild/aix-ppc64': 0.24.2 1439 | '@esbuild/android-arm': 0.24.2 1440 | '@esbuild/android-arm64': 0.24.2 1441 | '@esbuild/android-x64': 0.24.2 1442 | '@esbuild/darwin-arm64': 0.24.2 1443 | '@esbuild/darwin-x64': 0.24.2 1444 | '@esbuild/freebsd-arm64': 0.24.2 1445 | '@esbuild/freebsd-x64': 0.24.2 1446 | '@esbuild/linux-arm': 0.24.2 1447 | '@esbuild/linux-arm64': 0.24.2 1448 | '@esbuild/linux-ia32': 0.24.2 1449 | '@esbuild/linux-loong64': 0.24.2 1450 | '@esbuild/linux-mips64el': 0.24.2 1451 | '@esbuild/linux-ppc64': 0.24.2 1452 | '@esbuild/linux-riscv64': 0.24.2 1453 | '@esbuild/linux-s390x': 0.24.2 1454 | '@esbuild/linux-x64': 0.24.2 1455 | '@esbuild/netbsd-arm64': 0.24.2 1456 | '@esbuild/netbsd-x64': 0.24.2 1457 | '@esbuild/openbsd-arm64': 0.24.2 1458 | '@esbuild/openbsd-x64': 0.24.2 1459 | '@esbuild/sunos-x64': 0.24.2 1460 | '@esbuild/win32-arm64': 0.24.2 1461 | '@esbuild/win32-ia32': 0.24.2 1462 | '@esbuild/win32-x64': 0.24.2 1463 | 1464 | escalade@3.2.0: {} 1465 | 1466 | esm-env@1.2.2: {} 1467 | 1468 | esrap@1.4.5: 1469 | dependencies: 1470 | '@jridgewell/sourcemap-codec': 1.5.0 1471 | 1472 | estree-walker@2.0.2: {} 1473 | 1474 | fdir@6.4.3(picomatch@4.0.2): 1475 | optionalDependencies: 1476 | picomatch: 4.0.2 1477 | 1478 | flowbite-datepicker@1.3.2(rollup@4.34.6): 1479 | dependencies: 1480 | '@rollup/plugin-node-resolve': 15.3.1(rollup@4.34.6) 1481 | flowbite: 2.5.2(rollup@4.34.6) 1482 | transitivePeerDependencies: 1483 | - rollup 1484 | 1485 | flowbite-svelte@0.47.4(rollup@4.34.6)(svelte@5.20.2): 1486 | dependencies: 1487 | '@floating-ui/dom': 1.6.13 1488 | apexcharts: 3.54.1 1489 | flowbite: 2.5.2(rollup@4.34.6) 1490 | svelte: 5.20.2 1491 | tailwind-merge: 2.6.0 1492 | transitivePeerDependencies: 1493 | - rollup 1494 | 1495 | flowbite@2.5.2(rollup@4.34.6): 1496 | dependencies: 1497 | '@popperjs/core': 2.11.8 1498 | flowbite-datepicker: 1.3.2(rollup@4.34.6) 1499 | mini-svg-data-uri: 1.4.4 1500 | transitivePeerDependencies: 1501 | - rollup 1502 | 1503 | flowbite@3.1.2(rollup@4.34.6): 1504 | dependencies: 1505 | '@popperjs/core': 2.11.8 1506 | flowbite-datepicker: 1.3.2(rollup@4.34.6) 1507 | mini-svg-data-uri: 1.4.4 1508 | postcss: 8.5.2 1509 | transitivePeerDependencies: 1510 | - rollup 1511 | 1512 | fraction.js@4.3.7: {} 1513 | 1514 | fsevents@2.3.3: 1515 | optional: true 1516 | 1517 | function-bind@1.1.2: {} 1518 | 1519 | graceful-fs@4.2.11: {} 1520 | 1521 | hasown@2.0.2: 1522 | dependencies: 1523 | function-bind: 1.1.2 1524 | 1525 | import-meta-resolve@4.1.0: {} 1526 | 1527 | is-core-module@2.16.1: 1528 | dependencies: 1529 | hasown: 2.0.2 1530 | 1531 | is-module@1.0.0: {} 1532 | 1533 | is-reference@3.0.3: 1534 | dependencies: 1535 | '@types/estree': 1.0.6 1536 | 1537 | jiti@2.4.2: {} 1538 | 1539 | kleur@4.1.5: {} 1540 | 1541 | lightningcss-darwin-arm64@1.29.1: 1542 | optional: true 1543 | 1544 | lightningcss-darwin-x64@1.29.1: 1545 | optional: true 1546 | 1547 | lightningcss-freebsd-x64@1.29.1: 1548 | optional: true 1549 | 1550 | lightningcss-linux-arm-gnueabihf@1.29.1: 1551 | optional: true 1552 | 1553 | lightningcss-linux-arm64-gnu@1.29.1: 1554 | optional: true 1555 | 1556 | lightningcss-linux-arm64-musl@1.29.1: 1557 | optional: true 1558 | 1559 | lightningcss-linux-x64-gnu@1.29.1: 1560 | optional: true 1561 | 1562 | lightningcss-linux-x64-musl@1.29.1: 1563 | optional: true 1564 | 1565 | lightningcss-win32-arm64-msvc@1.29.1: 1566 | optional: true 1567 | 1568 | lightningcss-win32-x64-msvc@1.29.1: 1569 | optional: true 1570 | 1571 | lightningcss@1.29.1: 1572 | dependencies: 1573 | detect-libc: 1.0.3 1574 | optionalDependencies: 1575 | lightningcss-darwin-arm64: 1.29.1 1576 | lightningcss-darwin-x64: 1.29.1 1577 | lightningcss-freebsd-x64: 1.29.1 1578 | lightningcss-linux-arm-gnueabihf: 1.29.1 1579 | lightningcss-linux-arm64-gnu: 1.29.1 1580 | lightningcss-linux-arm64-musl: 1.29.1 1581 | lightningcss-linux-x64-gnu: 1.29.1 1582 | lightningcss-linux-x64-musl: 1.29.1 1583 | lightningcss-win32-arm64-msvc: 1.29.1 1584 | lightningcss-win32-x64-msvc: 1.29.1 1585 | 1586 | locate-character@3.0.0: {} 1587 | 1588 | magic-string@0.30.17: 1589 | dependencies: 1590 | '@jridgewell/sourcemap-codec': 1.5.0 1591 | 1592 | mdn-data@2.0.28: {} 1593 | 1594 | mdn-data@2.0.30: {} 1595 | 1596 | mini-svg-data-uri@1.4.4: {} 1597 | 1598 | mri@1.2.0: {} 1599 | 1600 | mrmime@2.0.0: {} 1601 | 1602 | ms@2.1.3: {} 1603 | 1604 | nanoid@3.3.8: {} 1605 | 1606 | node-releases@2.0.19: {} 1607 | 1608 | normalize-range@0.1.2: {} 1609 | 1610 | nth-check@2.1.1: 1611 | dependencies: 1612 | boolbase: 1.0.0 1613 | 1614 | number-flow@0.5.3: 1615 | dependencies: 1616 | esm-env: 1.2.2 1617 | 1618 | path-parse@1.0.7: {} 1619 | 1620 | picocolors@1.1.1: {} 1621 | 1622 | picomatch@4.0.2: {} 1623 | 1624 | postcss-value-parser@4.2.0: {} 1625 | 1626 | postcss@8.5.1: 1627 | dependencies: 1628 | nanoid: 3.3.8 1629 | picocolors: 1.1.1 1630 | source-map-js: 1.2.1 1631 | 1632 | postcss@8.5.2: 1633 | dependencies: 1634 | nanoid: 3.3.8 1635 | picocolors: 1.1.1 1636 | source-map-js: 1.2.1 1637 | 1638 | readdirp@4.1.1: {} 1639 | 1640 | resolve@1.22.10: 1641 | dependencies: 1642 | is-core-module: 2.16.1 1643 | path-parse: 1.0.7 1644 | supports-preserve-symlinks-flag: 1.0.0 1645 | 1646 | rollup@4.34.6: 1647 | dependencies: 1648 | '@types/estree': 1.0.6 1649 | optionalDependencies: 1650 | '@rollup/rollup-android-arm-eabi': 4.34.6 1651 | '@rollup/rollup-android-arm64': 4.34.6 1652 | '@rollup/rollup-darwin-arm64': 4.34.6 1653 | '@rollup/rollup-darwin-x64': 4.34.6 1654 | '@rollup/rollup-freebsd-arm64': 4.34.6 1655 | '@rollup/rollup-freebsd-x64': 4.34.6 1656 | '@rollup/rollup-linux-arm-gnueabihf': 4.34.6 1657 | '@rollup/rollup-linux-arm-musleabihf': 4.34.6 1658 | '@rollup/rollup-linux-arm64-gnu': 4.34.6 1659 | '@rollup/rollup-linux-arm64-musl': 4.34.6 1660 | '@rollup/rollup-linux-loongarch64-gnu': 4.34.6 1661 | '@rollup/rollup-linux-powerpc64le-gnu': 4.34.6 1662 | '@rollup/rollup-linux-riscv64-gnu': 4.34.6 1663 | '@rollup/rollup-linux-s390x-gnu': 4.34.6 1664 | '@rollup/rollup-linux-x64-gnu': 4.34.6 1665 | '@rollup/rollup-linux-x64-musl': 4.34.6 1666 | '@rollup/rollup-win32-arm64-msvc': 4.34.6 1667 | '@rollup/rollup-win32-ia32-msvc': 4.34.6 1668 | '@rollup/rollup-win32-x64-msvc': 4.34.6 1669 | fsevents: 2.3.3 1670 | 1671 | sade@1.8.1: 1672 | dependencies: 1673 | mri: 1.2.0 1674 | 1675 | set-cookie-parser@2.7.1: {} 1676 | 1677 | sirv@3.0.0: 1678 | dependencies: 1679 | '@polka/url': 1.0.0-next.28 1680 | mrmime: 2.0.0 1681 | totalist: 3.0.1 1682 | 1683 | source-map-js@1.2.1: {} 1684 | 1685 | supports-preserve-symlinks-flag@1.0.0: {} 1686 | 1687 | svelte-check@4.1.4(picomatch@4.0.2)(svelte@5.20.2)(typescript@5.7.3): 1688 | dependencies: 1689 | '@jridgewell/trace-mapping': 0.3.25 1690 | chokidar: 4.0.3 1691 | fdir: 6.4.3(picomatch@4.0.2) 1692 | picocolors: 1.1.1 1693 | sade: 1.8.1 1694 | svelte: 5.20.2 1695 | typescript: 5.7.3 1696 | transitivePeerDependencies: 1697 | - picomatch 1698 | 1699 | svelte@5.20.2: 1700 | dependencies: 1701 | '@ampproject/remapping': 2.3.0 1702 | '@jridgewell/sourcemap-codec': 1.5.0 1703 | '@types/estree': 1.0.6 1704 | acorn: 8.14.0 1705 | acorn-typescript: 1.4.13(acorn@8.14.0) 1706 | aria-query: 5.3.2 1707 | axobject-query: 4.1.0 1708 | clsx: 2.1.1 1709 | esm-env: 1.2.2 1710 | esrap: 1.4.5 1711 | is-reference: 3.0.3 1712 | locate-character: 3.0.0 1713 | magic-string: 0.30.17 1714 | zimmerframe: 1.1.2 1715 | 1716 | svg.draggable.js@2.2.2: 1717 | dependencies: 1718 | svg.js: 2.7.1 1719 | 1720 | svg.easing.js@2.0.0: 1721 | dependencies: 1722 | svg.js: 2.7.1 1723 | 1724 | svg.filter.js@2.0.2: 1725 | dependencies: 1726 | svg.js: 2.7.1 1727 | 1728 | svg.js@2.7.1: {} 1729 | 1730 | svg.pathmorphing.js@0.1.3: 1731 | dependencies: 1732 | svg.js: 2.7.1 1733 | 1734 | svg.resize.js@1.4.3: 1735 | dependencies: 1736 | svg.js: 2.7.1 1737 | svg.select.js: 2.1.2 1738 | 1739 | svg.select.js@2.1.2: 1740 | dependencies: 1741 | svg.js: 2.7.1 1742 | 1743 | svg.select.js@3.0.1: 1744 | dependencies: 1745 | svg.js: 2.7.1 1746 | 1747 | svgo@3.3.2: 1748 | dependencies: 1749 | '@trysound/sax': 0.2.0 1750 | commander: 7.2.0 1751 | css-select: 5.1.0 1752 | css-tree: 2.3.1 1753 | css-what: 6.1.0 1754 | csso: 5.0.5 1755 | picocolors: 1.1.1 1756 | 1757 | tailwind-merge@2.6.0: {} 1758 | 1759 | tailwindcss@4.0.6: {} 1760 | 1761 | tailwindcss@4.0.8: {} 1762 | 1763 | tapable@2.2.1: {} 1764 | 1765 | totalist@3.0.1: {} 1766 | 1767 | typescript@5.7.3: {} 1768 | 1769 | update-browserslist-db@1.1.2(browserslist@4.24.4): 1770 | dependencies: 1771 | browserslist: 4.24.4 1772 | escalade: 3.2.0 1773 | picocolors: 1.1.1 1774 | 1775 | vite@6.1.0(jiti@2.4.2)(lightningcss@1.29.1)(yaml@2.7.0): 1776 | dependencies: 1777 | esbuild: 0.24.2 1778 | postcss: 8.5.1 1779 | rollup: 4.34.6 1780 | optionalDependencies: 1781 | fsevents: 2.3.3 1782 | jiti: 2.4.2 1783 | lightningcss: 1.29.1 1784 | yaml: 2.7.0 1785 | 1786 | vitefu@1.0.5(vite@6.1.0(jiti@2.4.2)(lightningcss@1.29.1)(yaml@2.7.0)): 1787 | optionalDependencies: 1788 | vite: 6.1.0(jiti@2.4.2)(lightningcss@1.29.1)(yaml@2.7.0) 1789 | 1790 | yaml@2.7.0: 1791 | optional: true 1792 | 1793 | zimmerframe@1.1.2: {} 1794 | -------------------------------------------------------------------------------- /ui/src/app.css: -------------------------------------------------------------------------------- 1 | @import "tailwindcss"; 2 | 3 | @plugin "flowbite/plugin"; 4 | @source "../node_modules/flowbite"; 5 | 6 | @custom-variant dark (&:where(.dark, .dark *)); 7 | 8 | @theme { 9 | --color-primary-50: #FFF5F2; 10 | --color-primary-100: #FFF1EE; 11 | --color-primary-200: #FFE4DE; 12 | --color-primary-300: #FFD5CC; 13 | --color-primary-400: #FFBCAD; 14 | --color-primary-500: #FE795D; 15 | --color-primary-600: #EF562F; 16 | --color-primary-700: #EB4F27; 17 | --color-primary-800: #CC4522; 18 | --color-primary-900: #A5371B; 19 | 20 | --font-sans: 'Inter', 'ui-sans-serif', 'system-ui', '-apple-system', 'system-ui', 'Segoe UI', 'Roboto', 'Helvetica Neue', 'Arial', 'Noto Sans', 'sans-serif', 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'; 21 | --font-body: 'Inter', 'ui-sans-serif', 'system-ui', '-apple-system', 'system-ui', 'Segoe UI', 'Roboto', 'Helvetica Neue', 'Arial', 'Noto Sans', 'sans-serif', 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'; 22 | --font-mono: 'ui-monospace', 'SFMono-Regular', 'Menlo', 'Monaco', 'Consolas', 'Liberation Mono', 'Courier New', 'monospace'; 23 | } -------------------------------------------------------------------------------- /ui/src/app.d.ts: -------------------------------------------------------------------------------- 1 | // See https://svelte.dev/docs/kit/types#app.d.ts 2 | // for information about these interfaces 3 | import '@poppanator/sveltekit-svg/dist/svg' 4 | 5 | declare global { 6 | namespace App { 7 | // interface Error {} 8 | // interface Locals {} 9 | // interface PageData {} 10 | // interface PageState {} 11 | // interface Platform {} 12 | } 13 | } 14 | 15 | export {}; 16 | -------------------------------------------------------------------------------- /ui/src/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | %sveltekit.head% 7 | 8 | 9 |
%sveltekit.body%
10 | 11 | 12 | -------------------------------------------------------------------------------- /ui/src/icons/fox.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /ui/src/lib/stores.ts: -------------------------------------------------------------------------------- 1 | import { writable } from "svelte/store"; 2 | import type { Writable } from "svelte/store"; 3 | import type { Flow } from "$lib/types"; 4 | 5 | export const newFlowHandlerStore: Writable<(flow: Flow) => void> = writable(); 6 | export const editFlowHandlerStore: Writable<(flow: Flow) => void> = writable(); 7 | -------------------------------------------------------------------------------- /ui/src/lib/types.ts: -------------------------------------------------------------------------------- 1 | import type { MouseEventHandler } from "svelte/elements"; 2 | 3 | export interface Flow { 4 | id: string | undefined; 5 | name: string; 6 | description: string; 7 | amount: number; 8 | icon?: string; 9 | } 10 | 11 | export interface PageButton { 12 | name: string; 13 | clickHandle?: MouseEventHandler; 14 | color?: "alternative" | "none" | "red" | "yellow" | "green" | "purple" | "blue" | "light" | "dark" | "primary"; 15 | hidden?: boolean; 16 | } 17 | -------------------------------------------------------------------------------- /ui/src/routes/+layout.svelte: -------------------------------------------------------------------------------- 1 | 123 | 124 | 129 | 130 | 131 | Thrifty 132 | 133 | 134 | 135 | 136 | 137 | {#snippet addButton(button: PageButton, type?: "submit" | "reset" | "button" | null | undefined)} 138 | 142 | {/snippet} 143 | 144 | 145 | 146 | 147 | Thrifty 148 | 149 |
150 | {#each buttons as button} 151 | 156 | {/each} 157 |
158 |
159 | 160 | {@render children()} 161 | 162 |
163 | 167 | 171 |
172 | 176 |
177 | 178 | 185 |
186 | 187 |
188 |
189 | {#if currentFlow.id} 190 | {@render addButton(modalEditButton, "submit")} 191 | {:else} 192 | {@render addButton(modalAddButton, "submit")} 193 | {/if} 194 | {@render addButton(modalCloseButton)} 195 |
196 |
197 |
198 | 199 | 203 | -------------------------------------------------------------------------------- /ui/src/routes/+layout.ts: -------------------------------------------------------------------------------- 1 | export const prerender = true; -------------------------------------------------------------------------------- /ui/src/routes/+page.svelte: -------------------------------------------------------------------------------- 1 | 114 | 115 | {#snippet flowCard(flows: Flow[])} 116 |
117 | {#each flows as flow} 118 |
119 | 120 |
121 | Icon 122 |
123 |

124 | {flow.name} 125 |

126 |

127 | {flow.description} 128 |

129 |
130 |
131 | 132 |
133 |
134 | {#if (sharedState.isEditMode)} 135 | 141 | {/if} 142 |
143 |
144 | {/each} 145 |
146 | {/snippet} 147 | 148 |
149 |
150 | 151 |
152 |
153 |

154 | Total 155 |

156 |
157 |
158 | 159 |
160 |
161 |
162 |
163 | {#if (flows.length > 0)} 164 |
165 | {/if} 166 |
167 | {@render flowCard(getIncome(flows))} 168 | {@render flowCard(getExpenses(flows))} 169 |
170 |
171 | -------------------------------------------------------------------------------- /ui/src/routes/layout.svelte.ts: -------------------------------------------------------------------------------- 1 | export const sharedState = $state({"isEditMode": false}); 2 | -------------------------------------------------------------------------------- /ui/static/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tiehfood/thrifty/03c6e67a2db40890b3cae3bfad27c480d1cdaa10/ui/static/favicon.png -------------------------------------------------------------------------------- /ui/static/icons/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tiehfood/thrifty/03c6e67a2db40890b3cae3bfad27c480d1cdaa10/ui/static/icons/android-chrome-192x192.png -------------------------------------------------------------------------------- /ui/static/icons/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tiehfood/thrifty/03c6e67a2db40890b3cae3bfad27c480d1cdaa10/ui/static/icons/android-chrome-512x512.png -------------------------------------------------------------------------------- /ui/static/icons/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tiehfood/thrifty/03c6e67a2db40890b3cae3bfad27c480d1cdaa10/ui/static/icons/apple-touch-icon.png -------------------------------------------------------------------------------- /ui/static/manifest.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "icons": [ 3 | { 4 | "src": "./icons/android-chrome-192x192.png", 5 | "sizes": "192x192", 6 | "type": "image/png" 7 | }, 8 | { 9 | "src": "./icons/android-chrome-512x512.png", 10 | "sizes": "512x512", 11 | "type": "image/png" 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /ui/svelte.config.js: -------------------------------------------------------------------------------- 1 | import adapter from '@sveltejs/adapter-static'; 2 | import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; 3 | 4 | /** @type {import('@sveltejs/kit').Config} */ 5 | const config = { 6 | preprocess: vitePreprocess(), 7 | kit: { 8 | adapter: adapter() 9 | } 10 | }; 11 | 12 | export default config; 13 | -------------------------------------------------------------------------------- /ui/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./.svelte-kit/tsconfig.json", 3 | "compilerOptions": { 4 | "allowJs": true, 5 | "checkJs": true, 6 | "esModuleInterop": true, 7 | "forceConsistentCasingInFileNames": true, 8 | "resolveJsonModule": true, 9 | "skipLibCheck": true, 10 | "sourceMap": true, 11 | "strict": true, 12 | "moduleResolution": "bundler" 13 | } 14 | // Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias 15 | // except $lib which is handled by https://svelte.dev/docs/kit/configuration#files 16 | // 17 | // If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes 18 | // from the referenced tsconfig.json - TypeScript does not merge them in 19 | } 20 | -------------------------------------------------------------------------------- /ui/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { sveltekit } from '@sveltejs/kit/vite'; 2 | import { defineConfig } from 'vite'; 3 | import tailwindcss from '@tailwindcss/vite' 4 | import svg from '@poppanator/sveltekit-svg' 5 | 6 | export default defineConfig({ 7 | plugins: [ 8 | tailwindcss(), 9 | sveltekit(), 10 | svg({ 11 | includePaths: ['./src/icons/'] 12 | }) 13 | ] 14 | }); 15 | --------------------------------------------------------------------------------