diff --git a/.gitignore b/.gitignore
index 4f99426..36484f4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,6 +6,7 @@ web/*_templ.go
web/static/*.css
web/static/*.css.map
.sass-cache/*
+*/node_modules/*
# --> nix
.direnv/
diff --git a/flake.nix b/flake.nix
index d0d10a9..363d7fd 100644
--- a/flake.nix
+++ b/flake.nix
@@ -75,6 +75,7 @@
default = pkgs.mkShell {
buildInputs = with pkgs; [
air # live go app reloading
+ corepack # npm install for the web folder
dart-sass
go
gopls
diff --git a/types/types.go b/types/types.go
index 339ae08..a95953d 100644
--- a/types/types.go
+++ b/types/types.go
@@ -25,3 +25,8 @@ type HumanLegibleTransaction struct {
BucketName sql.NullString `db:"bucket_name" json:"BucketName"`
Date time.Time `db:"trns_date" json:"TransactionDate"`
}
+
+type ChartjsData struct {
+ Labels []string `json:"labels"`
+ Data []int `json:"data"`
+}
diff --git a/web/dashboard.templ b/web/dashboard.templ
index df67906..33e4ffa 100644
--- a/web/dashboard.templ
+++ b/web/dashboard.templ
@@ -9,6 +9,18 @@ templ dashboard() {
+
diff --git a/web/data_endpoints.go b/web/data_endpoints.go
new file mode 100644
index 0000000..eb6af3c
--- /dev/null
+++ b/web/data_endpoints.go
@@ -0,0 +1,53 @@
+package web
+
+import (
+ "context"
+ "database/sql"
+ "encoding/json"
+ "fmt"
+ "net/http"
+ "time"
+
+ "github.com/rs/zerolog/log"
+ "nickiel.net/recount_server/types"
+)
+
+
+const DEFAULT_RESULT_COUNT = 20;
+
+func getTransactions(w http.ResponseWriter, req *http.Request) {
+ transactions := make([]types.HumanLegibleTransaction, 10)
+
+ // Populate the slice with dummy data (you can replace this with your actual data)
+ for i := 10; i > 0; i-- {
+ transaction := types.HumanLegibleTransaction{
+ Id: i,
+ Amount: fmt.Sprintf("%d.00", (i+1)*100),
+ Description: sql.NullString{String: fmt.Sprintf("Transaction %d", i+1), Valid: true},
+ AccountName: sql.NullString{String: "Savings", Valid: true},
+ Account: 123,
+ Bucket: sql.NullInt64{Int64: int64(i + 100), Valid: true},
+ BucketName: sql.NullString{String: fmt.Sprintf("Bucket %d", i+1), Valid: true},
+ Date: time.Now(),
+ }
+
+ transactions[10 - i] = transaction
+ }
+
+ component := transaction_rows(&transactions);
+ component.Render(context.Background(), w);
+}
+
+func getExpenditureChart(w http.ResponseWriter, req *http.Request) {
+ data_package := types.ChartjsData {
+ Labels: []string{"Income", "Expenses"},
+ Data: []int{500, 200},
+ };
+
+ json_data, err := json.Marshal(data_package);
+ if err != nil {
+ log.Fatal().Err(err).Msg("Could not jsonify data_package");
+ }
+
+ w.Write(json_data);
+}
diff --git a/web/package-lock.json b/web/package-lock.json
new file mode 100644
index 0000000..8de758d
--- /dev/null
+++ b/web/package-lock.json
@@ -0,0 +1,28 @@
+{
+ "name": "web",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "dependencies": {
+ "chart.js": "^4.4.1"
+ }
+ },
+ "node_modules/@kurkle/color": {
+ "version": "0.3.2",
+ "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.2.tgz",
+ "integrity": "sha512-fuscdXJ9G1qb7W8VdHi+IwRqij3lBkosAm4ydQtEmbY58OzHXqQhvlxqEkoz0yssNVn38bcpRWgA9PP+OGoisw=="
+ },
+ "node_modules/chart.js": {
+ "version": "4.4.1",
+ "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.1.tgz",
+ "integrity": "sha512-C74QN1bxwV1v2PEujhmKjOZ7iUM4w6BWs23Md/6aOZZSlwMzeCIDGuZay++rBgChYru7/+QFeoQW0fQoP534Dg==",
+ "dependencies": {
+ "@kurkle/color": "^0.3.0"
+ },
+ "engines": {
+ "pnpm": ">=7"
+ }
+ }
+ }
+}
diff --git a/web/package.json b/web/package.json
new file mode 100644
index 0000000..3df43b7
--- /dev/null
+++ b/web/package.json
@@ -0,0 +1,5 @@
+{
+ "dependencies": {
+ "chart.js": "^4.4.1"
+ }
+}
diff --git a/web/router.go b/web/router.go
index 9b6485c..6958c82 100644
--- a/web/router.go
+++ b/web/router.go
@@ -1,20 +1,16 @@
package web
import (
- "database/sql"
"html/template"
"net/http"
"bytes"
"context"
- "fmt"
- "time"
"github.com/a-h/templ"
"github.com/go-chi/chi/v5"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
- "nickiel.net/recount_server/types"
)
type TemplateState struct {
@@ -32,6 +28,7 @@ func WebRouter() http.Handler {
r := chi.NewRouter()
r.Get("/", getIndex)
r.Get("/web/transaction_table_rows", getTransactions)
+ r.Get("/web/dashboard/expenditure_chart", getExpenditureChart)
r.Get("/hello", getHello)
r.Handle("/static/*", http.StripPrefix("/static/", http.FileServer(http.Dir("web/static/"))))
return r
@@ -99,27 +96,3 @@ func getHello(w http.ResponseWriter, req *http.Request) {
}
}
-const DEFAULT_RESULT_COUNT = 20;
-
-func getTransactions(w http.ResponseWriter, req *http.Request) {
- transactions := make([]types.HumanLegibleTransaction, 10)
-
- // Populate the slice with dummy data (you can replace this with your actual data)
- for i := 10; i > 0; i-- {
- transaction := types.HumanLegibleTransaction{
- Id: i,
- Amount: fmt.Sprintf("%d.00", (i+1)*100),
- Description: sql.NullString{String: fmt.Sprintf("Transaction %d", i+1), Valid: true},
- AccountName: sql.NullString{String: "Savings", Valid: true},
- Account: 123,
- Bucket: sql.NullInt64{Int64: int64(i + 100), Valid: true},
- BucketName: sql.NullString{String: fmt.Sprintf("Bucket %d", i+1), Valid: true},
- Date: time.Now(),
- }
-
- transactions[10 - i] = transaction
- }
-
- component := transaction_rows(&transactions);
- component.Render(context.Background(), w);
-}
diff --git a/web/sass/utility-classes.scss b/web/sass/utility-classes.scss
index 23def63..b83e651 100644
--- a/web/sass/utility-classes.scss
+++ b/web/sass/utility-classes.scss
@@ -56,6 +56,9 @@ $directions: (
.w-full {
width: 100%;
}
+.w-75 {
+ width: 75%;
+}
.w-50 {
width: 50%;
}
diff --git a/web/static/chart_functions.js b/web/static/chart_functions.js
new file mode 100644
index 0000000..e2366af
--- /dev/null
+++ b/web/static/chart_functions.js
@@ -0,0 +1,39 @@
+
+function fill_charts() {
+ document.querySelectorAll(".chartjs-chart").forEach(function (el) {
+ var url = el.dataset.chartEndpoint;
+ var type = el.dataset.chartType;
+
+ fetch(url)
+ .then(response => {
+ if (!response.ok) {
+ console.log(response);
+ throw new Error("Fetch response was not ok!")
+ }
+ return response.json();
+ }).then(jsonData => {
+ const config = {
+ type: type,
+ data: {
+ labels: jsonData.labels,
+ datasets: [{
+ data: jsonData.data
+ }]
+ },
+ options: {
+ scales: {
+ y: {
+ beginAtZero: true
+ }
+ }
+ }
+ };
+ new Chart(el, config)();
+ }).catch(error => {
+ console.error("Unable to set up chart: ", error)
+ });
+
+ });
+}
+
+export {fill_charts}
diff --git a/web/static/index.js b/web/static/index.js
index 7384b62..c973658 100644
--- a/web/static/index.js
+++ b/web/static/index.js
@@ -1,3 +1,5 @@
+import {fill_charts} from "./chart_functions.js";
+
function debounce(func, delay) {
let timeoutId;
@@ -48,4 +50,4 @@ function load_in_table() {
const trigger_table_animation = debounce(load_in_table, 100);
-export {register_handlers, trigger_table_animation};
+export {register_handlers, trigger_table_animation, fill_charts};
diff --git a/web/templates/index.html b/web/templates/index.html
index 07d1042..6ed9920 100644
--- a/web/templates/index.html
+++ b/web/templates/index.html
@@ -60,13 +60,15 @@