aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorZach Berwaldt <zberwaldt@tutamail.com>2024-03-15 18:49:43 -0400
committerZach Berwaldt <zberwaldt@tutamail.com>2024-03-15 18:49:43 -0400
commit9cae9c1d2a0b4f7fa72f3075541b9ffafe1a7275 (patch)
tree960fa4f96a1328861a06d97180da8601af6855da
parent8fab2d03bce82e4dee798ebffb1e93c557f62a4b (diff)
Add routes for preference, clean up and add types
-rw-r--r--api/internal/controllers/auth.go18
-rw-r--r--api/internal/controllers/preferences.go45
-rw-r--r--api/internal/controllers/user.go59
-rw-r--r--api/internal/middleware/middleware.go7
-rw-r--r--api/internal/models/auth.go2
-rw-r--r--api/internal/models/preferences.go8
-rw-r--r--api/internal/models/statistics.go2
-rw-r--r--api/internal/models/user.go2
-rw-r--r--api/internal/router/router.go5
-rw-r--r--fe/src/app.css4
-rw-r--r--fe/src/http.ts96
-rw-r--r--fe/src/lib/Card.svelte1
-rw-r--r--fe/src/lib/Chart.svelte63
-rw-r--r--fe/src/lib/DataView.svelte40
-rw-r--r--fe/src/lib/Layout.svelte8
-rw-r--r--fe/src/lib/LoginForm.svelte2
-rw-r--r--fe/src/lib/PreferencesForm.svelte145
-rw-r--r--fe/src/lib/errors.ts6
-rw-r--r--fe/src/lib/utils.ts2
-rw-r--r--fe/src/stores/auth.ts153
-rw-r--r--fe/src/types.ts45
21 files changed, 495 insertions, 218 deletions
diff --git a/api/internal/controllers/auth.go b/api/internal/controllers/auth.go
index 58653d0..ab2fbbb 100644
--- a/api/internal/controllers/auth.go
+++ b/api/internal/controllers/auth.go
@@ -38,23 +38,27 @@ func AuthHandler (c *gin.Context) {
38 38
39 var user models.User 39 var user models.User
40 var preference models.Preference 40 var preference models.Preference
41 var size models.Size
42 41
43 row := db.QueryRow("SELECT name, uuid, password, color, size, unit FROM Users u INNER JOIN Preferences p ON p.user_id = u.id INNER JOIN Sizes s ON p.size_id = s.id WHERE u.name = ?", username) 42 row := db.QueryRow("SELECT id as 'id', name, uuid, password FROM Users WHERE name = ?", username)
44 if err := row.Scan(&user.Name, &user.UUID, &user.Password, &preference.Color, &size.Size, &size.Unit); err != nil { 43 if err := row.Scan(&user.ID, &user.Name, &user.UUID, &user.Password); err != nil {
45 if errors.Is(err, sql.ErrNoRows) { 44 if errors.Is(err, sql.ErrNoRows) {
46 c.AbortWithStatus(http.StatusUnauthorized) 45 c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
47 return 46 return
48 } 47 }
49 } 48 }
50 49
50 row = db.QueryRow("SELECT id, color, size_id, user_id FROM Preferences where user_id = ?", user.ID)
51 if err := row.Scan(&preference.ID, &preference.Color, &preference.SizeID, &preference.UserID); err != nil {
52 if errors.Is(err, sql.ErrNoRows) {
53 c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
54 }
55 }
56
51 if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password)); err != nil { 57 if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password)); err != nil {
52 c.AbortWithStatus(http.StatusUnauthorized) 58 c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
53 return 59 return
54 } 60 }
55 61
56 preference.Size = size
57
58 // Generate a simple API token 62 // Generate a simple API token
59 apiToken := generateToken() 63 apiToken := generateToken()
60 c.JSON(http.StatusOK, gin.H{"token": apiToken, "user": user, "preferences": preference}) 64 c.JSON(http.StatusOK, gin.H{"token": apiToken, "user": user, "preferences": preference})
diff --git a/api/internal/controllers/preferences.go b/api/internal/controllers/preferences.go
new file mode 100644
index 0000000..a1bcf4f
--- /dev/null
+++ b/api/internal/controllers/preferences.go
@@ -0,0 +1,45 @@
1package controllers
2
3import (
4 "github.com/gin-gonic/gin"
5 "net/http"
6 "database/sql"
7 "water/api/internal/database"
8 "water/api/internal/models"
9)
10
11func GetSizes(c *gin.Context) {
12 db := database.EstablishDBConnection()
13 defer func(db *sql.DB) {
14 err := db.Close()
15 if err != nil {
16 c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
17 }
18 }(db)
19
20 rows, err := db.Query("SELECT id, size, unit FROM Sizes")
21 if err != nil {
22 c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
23 return
24 }
25 defer func(rows *sql.Rows) {
26 err := rows.Close()
27 if err != nil {
28 c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
29 return
30 }
31 }(rows)
32
33 var data []models.Size
34
35 for rows.Next() {
36 var size models.Size
37 if err := rows.Scan(&size.ID, &size.Size, &size.Unit); err != nil {
38 c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
39 return
40 }
41 data = append(data, size)
42 }
43
44 c.JSON(http.StatusOK, data)
45}
diff --git a/api/internal/controllers/user.go b/api/internal/controllers/user.go
index 76dedc8..dbb09cf 100644
--- a/api/internal/controllers/user.go
+++ b/api/internal/controllers/user.go
@@ -1,17 +1,68 @@
1package controllers 1package controllers
2 2
3import ( 3import (
4 "github.com/gin-gonic/gin" 4 "database/sql"
5 "errors"
6 "log"
5 "net/http" 7 "net/http"
8 "water/api/internal/database"
9 "water/api/internal/models"
10
11 "github.com/gin-gonic/gin"
6) 12)
7 13
8func GetUser(c *gin.Context) { 14func GetUser(c *gin.Context) {
9 c.JSON(http.StatusOK, gin.H{"message": "User found"}) 15 c.JSON(http.StatusOK, gin.H{"message": "User found"})
10} 16}
11func GetUserPreferences(c *gin.Context) { 17func GetUserPreferences(c *gin.Context) {
12 c.JSON(http.StatusOK, gin.H{"message": "Preferences fetched successfully"}) 18 db := database.EstablishDBConnection()
19 defer func(db *sql.DB) {
20 err := db.Close()
21 if err != nil {
22 c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
23 return
24 }
25 }(db)
26
27 var preference models.Preference
28
29 row := db.QueryRow("SELECT id, user_id, color, size_id FROM Preferences WHERE user_id = ?", c.Param("id"))
30 if err := row.Scan(&preference.ID, &preference.UserID, &preference.Color, &preference.SizeID); err != nil {
31 if errors.Is(err, sql.ErrNoRows) {
32 c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
33 return
34 } else {
35 c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
36 return
37 }
38 }
39
40 c.JSON(http.StatusOK, preference)
13} 41}
14 42
15func UpdateUserPreferences(c *gin.Context) { 43func UpdateUserPreferences(c *gin.Context) {
16 c.JSON(http.StatusOK, gin.H{"message": "Preferences updated successfully"}) 44 db := database.EstablishDBConnection()
17} \ No newline at end of file 45 defer func(db *sql.DB) {
46 err := db.Close()
47 if err != nil {
48 c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
49 return
50 }
51 }(db)
52
53 var newPreferences models.Preference
54 if err := c.BindJSON(&newPreferences); err != nil {
55 c.JSON(http.StatusUnprocessableEntity, gin.H{"error": err.Error()})
56 return
57 }
58
59 log.Printf("newPreferences: %v", newPreferences)
60
61 _, err := db.Exec("UPDATE Preferences SET color = ?, size_id = ? WHERE id = ?", newPreferences.Color, newPreferences.SizeID, newPreferences.ID)
62 if err != nil {
63 c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
64 return
65 }
66
67 c.Status(http.StatusNoContent)
68}
diff --git a/api/internal/middleware/middleware.go b/api/internal/middleware/middleware.go
index 819f1e5..aa27fb8 100644
--- a/api/internal/middleware/middleware.go
+++ b/api/internal/middleware/middleware.go
@@ -2,10 +2,11 @@ package middleware
2 2
3import ( 3import (
4 "errors" 4 "errors"
5 "github.com/gin-gonic/gin"
6 "log" 5 "log"
7 "net/http" 6 "net/http"
8 "strings" 7 "strings"
8
9 "github.com/gin-gonic/gin"
9) 10)
10 11
11func TokenRequired() gin.HandlerFunc { 12func TokenRequired() gin.HandlerFunc {
@@ -27,7 +28,7 @@ func CORSMiddleware() gin.HandlerFunc {
27 c.Writer.Header().Set("Access-Control-Allow-Origin", "*") 28 c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
28 c.Writer.Header().Set("Access-Control-Allow-Credentials", "true") 29 c.Writer.Header().Set("Access-Control-Allow-Credentials", "true")
29 c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, accept, origin, Cache-Control, X-Requested-With") 30 c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, accept, origin, Cache-Control, X-Requested-With")
30 c.Writer.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS, GET, PUT") 31 c.Writer.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS, GET, PUT, PATCH")
31 32
32 if c.Request.Method == "OPTIONS" { 33 if c.Request.Method == "OPTIONS" {
33 log.Println(c.Request.Header) 34 log.Println(c.Request.Header)
@@ -52,4 +53,4 @@ func checkForTokenInContext(c *gin.Context) (string, error) {
52 } 53 }
53 54
54 return parts[1], nil 55 return parts[1], nil
55} \ No newline at end of file 56}
diff --git a/api/internal/models/auth.go b/api/internal/models/auth.go
index 41344d5..fa7dbe4 100644
--- a/api/internal/models/auth.go
+++ b/api/internal/models/auth.go
@@ -3,7 +3,7 @@ package models
3import "time" 3import "time"
4 4
5type Token struct { 5type Token struct {
6 ID int64 `json:"-"` 6 ID int64 `json:"id"`
7 UserID int64 `json:"user_id"` 7 UserID int64 `json:"user_id"`
8 Token string `json:"token"` 8 Token string `json:"token"`
9 CreatedAt time.Time `json:"created_at"` 9 CreatedAt time.Time `json:"created_at"`
diff --git a/api/internal/models/preferences.go b/api/internal/models/preferences.go
index cbbd47c..8022099 100644
--- a/api/internal/models/preferences.go
+++ b/api/internal/models/preferences.go
@@ -1,14 +1,14 @@
1package models 1package models
2 2
3type Preference struct { 3type Preference struct {
4 ID int64 `json:"-"` 4 ID int64 `json:"id"`
5 Color string `json:"color"` 5 Color string `json:"color"`
6 UserID int64 `json:"-"` 6 UserID int64 `json:"user_id"`
7 Size Size `json:"size"` 7 SizeID int64 `json:"size_id"`
8} 8}
9 9
10type Size struct { 10type Size struct {
11 ID int64 `json:"-"` 11 ID int64 `json:"id"`
12 Size int64 `json:"size"` 12 Size int64 `json:"size"`
13 Unit string `json:"unit"` 13 Unit string `json:"unit"`
14} 14}
diff --git a/api/internal/models/statistics.go b/api/internal/models/statistics.go
index 457e6a0..7dceb3a 100644
--- a/api/internal/models/statistics.go
+++ b/api/internal/models/statistics.go
@@ -3,7 +3,7 @@ package models
3import "time" 3import "time"
4 4
5type Statistic struct { 5type Statistic struct {
6 ID int64 `json:"-"` 6 ID int64 `json:"id"`
7 Date time.Time `json:"date"` 7 Date time.Time `json:"date"`
8 User User `json:"user"` 8 User User `json:"user"`
9 Quantity int `json:"quantity"` 9 Quantity int `json:"quantity"`
diff --git a/api/internal/models/user.go b/api/internal/models/user.go
index 2a3e6fd..ca5daa4 100644
--- a/api/internal/models/user.go
+++ b/api/internal/models/user.go
@@ -3,7 +3,7 @@ package models
3import "github.com/google/uuid" 3import "github.com/google/uuid"
4 4
5type User struct { 5type User struct {
6 ID int64 `json:"-"` 6 ID int64 `json:"id"`
7 Name string `json:"name"` 7 Name string `json:"name"`
8 UUID uuid.UUID `json:"uuid"` 8 UUID uuid.UUID `json:"uuid"`
9 Password string `json:"-"` 9 Password string `json:"-"`
diff --git a/api/internal/router/router.go b/api/internal/router/router.go
index adf96d0..3c86b8c 100644
--- a/api/internal/router/router.go
+++ b/api/internal/router/router.go
@@ -17,13 +17,14 @@ func SetupRouter() *gin.Engine {
17 api := r.Group("api/v1") 17 api := r.Group("api/v1")
18 18
19 api.POST("/auth", controllers.AuthHandler) 19 api.POST("/auth", controllers.AuthHandler)
20 api.GET("/sizes", middleware.TokenRequired(), controllers.GetSizes)
21 api.PATCH("/user/preferences", controllers.UpdateUserPreferences)
20 22
21 user := api.Group("/user/:uuid") 23 user := api.Group("/user/:id")
22 user.Use(middleware.TokenRequired()) 24 user.Use(middleware.TokenRequired())
23 { 25 {
24 user.GET("", controllers.GetUser) 26 user.GET("", controllers.GetUser)
25 user.GET("preferences", controllers.GetUserPreferences) 27 user.GET("preferences", controllers.GetUserPreferences)
26 user.PATCH("preferences", controllers.UpdateUserPreferences)
27 } 28 }
28 29
29 stats := api.Group("/stats") 30 stats := api.Group("/stats")
diff --git a/fe/src/app.css b/fe/src/app.css
index de19b52..c24c713 100644
--- a/fe/src/app.css
+++ b/fe/src/app.css
@@ -109,6 +109,10 @@ button:focus-visible {
109 padding: 1em; 109 padding: 1em;
110} 110}
111 111
112.form.input.group input[type=color] {
113 padding: 0;
114}
115
112.form button[type=submit] { 116.form button[type=submit] {
113 align-self: flex-end; 117 align-self: flex-end;
114 background: var(--submit); 118 background: var(--submit);
diff --git a/fe/src/http.ts b/fe/src/http.ts
index cc5a906..3b2a4f0 100644
--- a/fe/src/http.ts
+++ b/fe/src/http.ts
@@ -1,60 +1,92 @@
1export default class HttpClient { 1let instance;
2 private static instance: HttpClient; 2const baseUrl = import.meta.env?.VITE_API_BASE_URL ?? "http://localhost:8080/api/v1";
3
4class HttpClient {
3 baseURL: string; 5 baseURL: string;
4 6 commonHeaders: Headers;
5 private constructor(baseURL: string) { 7
8 constructor(baseURL: string) {
6 this.baseURL = baseURL; 9 this.baseURL = baseURL;
10 this.commonHeaders = new Headers({
11 "Content-Type": "application/json"
12 })
13 if (instance) {
14 throw new Error("New instance cannot be created!");
15 }
16
17 instance = this;
7 } 18 }
8 19
9 private getURL(endpoint: string): URL { 20 private getURL(endpoint: string): URL {
10 return new URL(endpoint, this.baseURL) 21 return new URL(endpoint, this.baseURL);
11 } 22 }
12 23
13 public static getInstance(): HttpClient { 24 private token(): string | null {
14 if (!HttpClient.instance) { 25 return localStorage.getItem('token');
15 const baseUrl = import.meta.env?.VITE_API_BASE_URL ?? 'http://localhost:8080/api/v1'; 26 }
16 HttpClient.instance = new HttpClient(baseUrl);
17 }
18 27
19 return HttpClient.instance; 28 private async makeRequest(request: Request): Promise<Response> {
29 return fetch(request)
20 } 30 }
21 31
22 async get({ endpoint }: IHttpParameters): Promise<Response> { 32 async get({ endpoint, headers }: IHttpParameters): Promise<Response> {
23 const url = this.getURL(endpoint); 33 const url: URL = this.getURL(endpoint);
24 const response = await fetch(url, { 34 headers = Object.assign<Headers, Headers>(headers, this.commonHeaders);
25 method: 'GET', 35 const request: Request = new Request(url, {
26 headers: headers, 36 method: "GET",
37 headers
27 }); 38 });
28 return response.json(); 39
40 return this.makeRequest(request);
29 } 41 }
30 42
31 async post({ endpoint }: IHttpParameters): Promise<Response> { 43 async post({ endpoint, authenticated, body, headers }: IHttpParameters): Promise<Response> {
32 const url = this.getURL(endpoint); 44 const url = this.getURL(endpoint);
33 const response = await fetch(url, { 45
34 method: 'POST', 46 if (authenticated) {
47 const token: string | null = this.token();
48 headers.append('Authorization', `Bearer ${token}`);
49 }
50
51 const request: Request = new Request(url, {
52 method: "POST",
35 body: JSON.stringify(body), 53 body: JSON.stringify(body),
36 headers: headers, 54 headers
37 }); 55 })
38 return response.json(); 56
57 return this.makeRequest(request);
39 } 58 }
40 59
41 async patch({ endpoint, authenticated, headers }: IHttpParameters): Promise<Response> { 60 async patch({ endpoint, authenticated, headers }: IHttpParameters): Promise<Response> {
42 const url = this.getURL(endpoint); 61 const url = this.getURL(endpoint);
43 if (authenticated) { 62 if (authenticated) {
44 63
45 } 64 }
46 const response: Response = await fetch(url) 65 const response: Response = await fetch(url, {
66 method: "PATCH",
67 headers
68 });
47 } 69 }
48 70
49 async delete({ endpoint, authenticated }: IHttpParameters): Promise<Response> { 71 async delete({ endpoint, authenticated, headers }: IHttpParameters): Promise<Response> {
50 const url = this.getURL(endpoint); 72 const url = this.getURL(endpoint);
51 if (authenticated) { } 73 if (authenticated) {
52 const response = await fetch() 74
75 }
76 const response: Response = await fetch(url, {
77 method: "DELETE",
78 headers
79 })
53 } 80 }
54} 81}
55 82
56interface IHttpParameters { 83interface IHttpParameters {
57 endpoint: string; 84 endpoint: string;
85 body: Record<string, any>;
58 authenticated: boolean; 86 authenticated: boolean;
59 headers: Headers 87 headers: Headers;
60} 88}
89
90let http: Readonly<HttpClient> = Object.freeze(new HttpClient(baseUrl));
91
92export default http;
diff --git a/fe/src/lib/Card.svelte b/fe/src/lib/Card.svelte
index d7cd900..cd1e02c 100644
--- a/fe/src/lib/Card.svelte
+++ b/fe/src/lib/Card.svelte
@@ -1,6 +1,5 @@
1<script lang="ts"> 1<script lang="ts">
2 export let title = ""; 2 export let title = "";
3 export let height: number | undefined = undefined;
4</script> 3</script>
5 4
6<div class="card"> 5<div class="card">
diff --git a/fe/src/lib/Chart.svelte b/fe/src/lib/Chart.svelte
new file mode 100644
index 0000000..b19d932
--- /dev/null
+++ b/fe/src/lib/Chart.svelte
@@ -0,0 +1,63 @@
1<script lang="ts">
2import { onDestroy } from "svelte";
3import ChartJS from "chart.js/auto";
4
5export let data;
6export let labels;
7export let type = 'bar';
8
9let ref: HTMLCanvasElement;
10let chart
11
12function setupChart(result) {
13 [labels, data] = result;
14 chart = new ChartJS(ref, {
15 type,
16 data: {
17 labels,
18 datasets: [
19 {
20 label: "Totals",
21 data,
22 backgroundColor: "rgba(255, 192, 192, 0.2)"
23 }
24 ]
25 },
26 options: {
27 responsive: true,
28 maintainAspectRatio: false,
29 scales: {
30 y: {
31 suggestedMax: 30,
32 beginAtZero: true,
33 ticks: {
34 autoSkip: true,
35 stepSize: 5
36 }
37 }
38 },
39 plugins: {
40 legend: {
41 display: false
42 },
43 title: {
44 display: true,
45 text: "Weekly Breakdown"
46 },
47 subtitle: {
48 display: true,
49 text: "Water consumption over the last week",
50 padding: {bottom: 10}
51 }
52 }
53 }
54 });
55
56 onDestroy(() => {
57 if (chart) chart.destroy();
58 chart = null;
59 })
60}
61</script>
62
63<canvas bind:this={ref} /> \ No newline at end of file
diff --git a/fe/src/lib/DataView.svelte b/fe/src/lib/DataView.svelte
index 0a6b81b..5e81a5a 100644
--- a/fe/src/lib/DataView.svelte
+++ b/fe/src/lib/DataView.svelte
@@ -1,10 +1,11 @@
1<script lang="ts"> 1<script lang="ts">
2 import { onDestroy, onMount } from "svelte"; 2 import { onDestroy, onMount } from "svelte";
3 import HttpClient from "../http"; 3 import http from "../http";
4 import { token } from "../stores/auth"; 4 import { token } from "../stores/auth";
5 import { addFormOpen } from "../stores/forms"; 5 import { addFormOpen } from "../stores/forms";
6 import Table from "./Table.svelte"; 6 import Table from "./Table.svelte";
7 import Chart from "chart.js/auto"; 7 import ChartJS from "chart.js/auto";
8 import Chart from './Chart.svelte'
8 import Card from "./Card.svelte"; 9 import Card from "./Card.svelte";
9 import Column from "./Column.svelte"; 10 import Column from "./Column.svelte";
10 import AddForm from "./forms/AddForm.svelte"; 11 import AddForm from "./forms/AddForm.svelte";
@@ -46,8 +47,8 @@
46 47
47 if (res.ok) { 48 if (res.ok) {
48 const json = await res.json(); 49 const json = await res.json();
49 let labels = json.map(d => d.name); 50 let labels = json.map((d: any) => d.name);
50 let data = json.map(d => d.total); 51 let data = json.map((d: any) => d.total);
51 return [labels, data]; 52 return [labels, data];
52 } else { 53 } else {
53 throw new Error("There was a problem with your request"); 54 throw new Error("There was a problem with your request");
@@ -65,8 +66,8 @@
65 66
66 if (res.ok) { 67 if (res.ok) {
67 const json = await res.json(); 68 const json = await res.json();
68 let labels = json.map(d => d.date); 69 let labels = json.map((d: any) => d.date);
69 let data = json.map(d => d.total); 70 let data = json.map((d: any) => d.total);
70 return [labels, data]; 71 return [labels, data];
71 } else { 72 } else {
72 throw new Error("There was a problem with your request"); 73 throw new Error("There was a problem with your request");
@@ -84,9 +85,9 @@
84 fetchDailyUserStatistics().then(updateDailyUserTotalsChart).catch(err => console.error(err)); 85 fetchDailyUserStatistics().then(updateDailyUserTotalsChart).catch(err => console.error(err));
85 } 86 }
86 87
87 function setupWeeklyTotalsChart(result) { 88 function setupWeeklyTotalsChart(result: any) {
88 [lastSevenDays, lastSevenDaysData] = result; 89 [lastSevenDays, lastSevenDaysData] = result;
89 lineChart = new Chart(lineCanvasRef, { 90 lineChart = new ChartJS(lineCanvasRef, {
90 type: "line", 91 type: "line",
91 data: { 92 data: {
92 labels: lastSevenDays, 93 labels: lastSevenDays,
@@ -129,10 +130,10 @@
129 }); 130 });
130 } 131 }
131 132
132 function setupDailyUserTotalsChart(result) { 133 function setupDailyUserTotalsChart(result: any) {
133 [userTotalsLabels, userTotalsData] = result; 134 [userTotalsLabels, userTotalsData] = result;
134 135
135 barChart = new Chart(barCanvasRef, { 136 barChart = new ChartJS(barCanvasRef, {
136 type: "bar", 137 type: "bar",
137 data: { 138 data: {
138 labels: userTotalsLabels, 139 labels: userTotalsLabels,
@@ -177,13 +178,13 @@
177 }); 178 });
178 } 179 }
179 180
180 function updateWeeklyTotalsChart(result) { 181 function updateWeeklyTotalsChart(result: any) {
181 [, lastSevenDaysData] = result; 182 [, lastSevenDaysData] = result;
182 lineChart.data.datasets[0].data = lastSevenDaysData; 183 lineChart.data.datasets[0].data = lastSevenDaysData;
183 lineChart.update(); 184 lineChart.update();
184 } 185 }
185 186
186 function updateDailyUserTotalsChart(result) { 187 function updateDailyUserTotalsChart(result: any) {
187 [, userTotalsData] = result; 188 [, userTotalsData] = result;
188 barChart.data.datasets[0].data = userTotalsData; 189 barChart.data.datasets[0].data = userTotalsData;
189 barChart.update(); 190 barChart.update();
@@ -205,6 +206,7 @@
205 206
206<Column --width="500px"> 207<Column --width="500px">
207 <Card --height="300px"> 208 <Card --height="300px">
209 <!--<Chart />-->
208 <canvas bind:this={barCanvasRef} /> 210 <canvas bind:this={barCanvasRef} />
209 </Card> 211 </Card>
210 <Card --height="300px"> 212 <Card --height="300px">
@@ -223,17 +225,3 @@
223 </Card> 225 </Card>
224</Column> 226</Column>
225<!-- <Chart /> --> 227<!-- <Chart /> -->
226
227
228<style>
229 dialog {
230 background: red;
231 box-shadow: 0 20px 5em 10px rgba(0, 0, 0, 0.8);
232 }
233
234 dialog::backdrop {
235 padding: 20px;
236 box-shadow: 20px 20px rgba(0, 0, 0, 0.8);
237 background-color: red;
238 }
239</style>
diff --git a/fe/src/lib/Layout.svelte b/fe/src/lib/Layout.svelte
index f208f34..2728dd3 100644
--- a/fe/src/lib/Layout.svelte
+++ b/fe/src/lib/Layout.svelte
@@ -1,9 +1,13 @@
1<script> 1<script>
2 import { authenticated, token } from "../stores/auth"; 2 import { authenticated, token, user, preferences } from "../stores/auth";
3 import PreferencesForm from "./PreferencesForm.svelte"; 3 import PreferencesForm from "./PreferencesForm.svelte";
4 import { addFormOpen } from "../stores/forms"; 4 import { addFormOpen } from "../stores/forms";
5 5
6 const logout = () => token.unauthenticate(); 6 const logout = () => {
7 preferences.reset();
8 user.reset();
9 token.unauthenticate();
10 }
7 let preferenceFormOpen = false; 11 let preferenceFormOpen = false;
8 12
9 function showPreferencesDialog() { 13 function showPreferencesDialog() {
diff --git a/fe/src/lib/LoginForm.svelte b/fe/src/lib/LoginForm.svelte
index bf6d9ad..8c3c288 100644
--- a/fe/src/lib/LoginForm.svelte
+++ b/fe/src/lib/LoginForm.svelte
@@ -47,7 +47,7 @@
47 preferences: userPreferences, 47 preferences: userPreferences,
48 } = await response.json(); 48 } = await response.json();
49 user.setUser(userData); 49 user.setUser(userData);
50 preferences.set(userPreferences); 50 preferences.setPreference(userPreferences);
51 token.authenticate(apiToken); 51 token.authenticate(apiToken);
52 } 52 }
53 53
diff --git a/fe/src/lib/PreferencesForm.svelte b/fe/src/lib/PreferencesForm.svelte
index 95e04c1..875393c 100644
--- a/fe/src/lib/PreferencesForm.svelte
+++ b/fe/src/lib/PreferencesForm.svelte
@@ -1,51 +1,118 @@
1<script lang="ts"> 1<script lang="ts">
2 import { preferences } from '../stores/auth'; 2 import { user, preferences, token } from "../stores/auth";
3 import type { Size, Preference } from '../types'; 3 import { createEventDispatcher, onDestroy, onMount } from "svelte";
4 import { createEventDispatcher } from 'svelte'; 4 import type { User } from "../types";
5 export let open: boolean; 5
6 6 export let open: boolean;
7 let preference: Preference = { 7
8 color: "#00FF00", 8 let sizes: Array<any>;
9 size: { 9 let selectedSize: number = 1;
10 size: 8, 10 let color: string = "#000000";
11 unit: 'oz' 11
12 } 12 const dispatch = createEventDispatcher();
13 } 13
14 14 const unsubscribe = preferences.subscribe(
15 const dispatch = createEventDispatcher(); 15 (value: any) => {
16 16 console.log('update value: ', value);
17 preferences.subscribe((value) => { 17 color = value.color;
18 preference = value; 18 selectedSize = value.size_id;
19 },
20 );
21
22 function closeDialog() {
23 dispatch("close");
24 }
25
26 async function updateUserPreferences() {
27 const res = await fetch("http://localhost:8080/api/v1/user/preferences", {
28 method: "PATCH",
29 headers: {
30 Authorization: `Bearer ${$token}`,
31 },
32 body: JSON.stringify($preferences),
19 }); 33 });
34 }
20 35
21 function onPreferencesSave(): void { 36 async function getUserPreferences() {
22 preferences.set(preference); 37 const res = await fetch(
23 dispatch('close') 38 `http://localhost:8080/api/v1/user/${($user as User)!.id}/preferences`,
24 } 39 {
40 method: "GET",
41 headers: {
42 Authorization: `Bearer ${$token}`,
43 },
44 },
45 );
46 const updatePreferences = await res.json();
47 preferences.set(updatePreferences);
48 }
25 49
50 async function onPreferencesSave(): Promise<void> {
51 preferences.update((value) => ({
52 ...value!,
53 size_id: selectedSize,
54 color: color,
55 }));
56
57 await updateUserPreferences();
58 await getUserPreferences();
59
60 dispatch("close");
61 }
62
63 onMount(() => {
64 fetch("http://localhost:8080/api/v1/sizes", {
65 method: "GET",
66 headers: {
67 Authorization: `Bearer ${$token}`,
68 },
69 })
70 .then((res) => res.json())
71 .then((val) => (sizes = val));
72 });
73
74 onDestroy(() => {
75 unsubscribe();
76 });
26</script> 77</script>
78
27<dialog {open} on:submit|preventDefault={onPreferencesSave}> 79<dialog {open} on:submit|preventDefault={onPreferencesSave}>
28 <h2>User Preferences</h2> 80 <h2>User Preferences</h2>
29 <form method="dialog"> 81 <form method="dialog">
30 <div class="form input group"> 82 <div class="form input group">
31 <label for="color">Color</label> 83 <label for="color">Color</label>
32 <input id="color" name="color" type="color" bind:value={preference.color}/> 84 <input
33 </div> 85 id="color"
34 <div class="form input group"> 86 name="color"
35 <label for="size">Bottle Size</label> 87 type="color"
36 <input id="size" name="size" type="number" min="8" max="48" step="8" bind:value={preference.size.size}/> 88 bind:value={color}
37 </div> 89 />
38 <button type="submit">Save</button> 90 </div>
39 </form> 91 <div class="form input group">
92 <label for="size">Bottle Size</label>
93 <select
94 bind:value={selectedSize}
95 >
96 {#if sizes}
97 {#each sizes as size}
98 <option value={size.id}>{size.size} {size.unit}</option>
99 {/each}
100 {/if}
101 </select>
102 </div>
103 <button on:click={closeDialog}>Cancel</button>
104 <button type="submit">Save</button>
105 </form>
40</dialog> 106</dialog>
107
41<style> 108<style>
42dialog { 109 dialog {
43 background: white; 110 background: white;
44 color: black; 111 color: black;
45} 112 }
46 113
47input[type="color"] { 114 input[type="color"] {
48 width: 100%; 115 width: 4em;
49 height: 100%; 116 height: 4em;
50} 117 }
51</style> 118</style>
diff --git a/fe/src/lib/errors.ts b/fe/src/lib/errors.ts
index d44bec5..81f7145 100644
--- a/fe/src/lib/errors.ts
+++ b/fe/src/lib/errors.ts
@@ -1,7 +1,5 @@
1export class UnauthorizedError extends Error { 1export class UnauthorizedError extends Error {
2 constructor(message?: string, options?: ErrorOptions) { 2 constructor(message?: string) {
3 super(message, options); 3 super(message);
4 } 4 }
5} 5}
6
7
diff --git a/fe/src/lib/utils.ts b/fe/src/lib/utils.ts
index 22d4e9a..e78556c 100644
--- a/fe/src/lib/utils.ts
+++ b/fe/src/lib/utils.ts
@@ -1,5 +1,5 @@
1export function processFormInput(form: HTMLFormElement) { 1export function processFormInput(form: HTMLFormElement) {
2 const formData = new FormData(form); 2 const formData: FormData = new FormData(form);
3 const data: Record<string, any> = {}; 3 const data: Record<string, any> = {};
4 for (let field of formData) { 4 for (let field of formData) {
5 const [key, value] = field; 5 const [key, value] = field;
diff --git a/fe/src/stores/auth.ts b/fe/src/stores/auth.ts
index 0efc80b..63f027e 100644
--- a/fe/src/stores/auth.ts
+++ b/fe/src/stores/auth.ts
@@ -1,100 +1,87 @@
1import type { Invalidator, Subscriber, Unsubscriber } from 'svelte/store'; 1import type { Preference, TokenStore, Nullable, UserStore, User, PreferenceStore } from "../types";
2import type { Preference } from '../types'; 2import { writable, derived } from "svelte/store";
3import { writable, derived } from 'svelte/store';
4 3
5type Nullable<T> = T | null;
6
7interface User {
8 uuid: string;
9 username: string;
10}
11
12interface TokenStore {
13 subscribe: (run: Subscriber<Nullable<string>>, invalidate?: Invalidator<Nullable<string>>) => Unsubscriber,
14 authenticate: (newToken: string) => void,
15 unauthenticate: () => void
16}
17
18
19interface UserStore {
20 subscribe: (run: Subscriber<Nullable<User>>, invalidate?: Invalidator<Nullable<User>>) => Unsubscriber,
21 setUser: (user: User) => void,
22 reset: () => void
23}
24
25interface PreferenceStore {
26 subscribe: (run: Subscriber<Preference>, invalidate?: Invalidator<Preference>) => Unsubscriber,
27 set: (this: void, value: Preference) => void
28}
29 4
30function createTokenStore(): TokenStore { 5function createTokenStore(): TokenStore {
31 const storedToken = localStorage.getItem("token"); 6 const storedToken = localStorage.getItem("token");
32 const { subscribe, set } = writable<string | null>(storedToken); 7 const { subscribe, set } = writable<string | null>(storedToken);
33 8
34 function authenticate(newToken: string): void { 9 function authenticate(newToken: string): void {
35 try { 10 try {
36 localStorage.setItem("token", newToken); 11 localStorage.setItem("token", newToken);
37 set(newToken); 12 set(newToken);
38 } catch (e) { 13 } catch (e) {
39 console.error('error', e); 14 console.error("error", e);
15 }
16 }
17
18 function unauthenticate(): void {
19 localStorage.removeItem("token");
20 set(null);
40 } 21 }
41 } 22
42 23 return {
43 function unauthenticate(): void { 24 subscribe,
44 localStorage.removeItem("token"); 25 authenticate,
45 set(null); 26 unauthenticate
46 } 27 };
47
48 return {
49 subscribe,
50 authenticate,
51 unauthenticate
52 };
53} 28}
54 29
55function onTokenChange($token: Nullable<string>): boolean { 30function onTokenChange($token: Nullable<string>): boolean {
56 return $token ? true : false; 31 return $token ? true : false;
57} 32}
58 33
59function createUserStore(): UserStore { 34function createUserStore(): UserStore {
60 const user = localStorage.getItem('user'); 35 const user = localStorage.getItem("user");
61 const userObj: Nullable<User> = user ? JSON.parse(user) : null; 36 const userObj: Nullable<User> = user ? JSON.parse(user) : null;
62 const { subscribe, set } = writable<User | null>(userObj); 37 const { subscribe, set } = writable<User | null>(userObj);
63 38
64 const setUser = (user: User) => { 39 const setUser = (user: User) => {
65 localStorage.setItem('user', JSON.stringify(user)); 40 localStorage.setItem("user", JSON.stringify(user));
66 set(user); 41 set(user);
67 } 42 };
68 43
69 const reset = () => { 44 const reset = () => {
70 localStorage.removeItem('user'); 45 localStorage.removeItem("user");
71 set(null); 46 set(null);
72 } 47 };
73 48
74 return { 49 return {
75 subscribe, 50 subscribe,
76 setUser, 51 setUser,
77 reset 52 reset
78 } 53 };
79} 54}
80 55
81 56
82function createPreferenceStore(): PreferenceStore { 57function createPreferenceStore(): PreferenceStore {
83 const preferences = localStorage.getItem('preferences'); 58 const preferences = localStorage.getItem("preferences");
84 const preferenceObj: Preference = preferences ? JSON.parse(preferences) : { 59 const preferenceObj: Preference = preferences ? JSON.parse(preferences) : {
85 color: "#FF0000", 60 id: 0,
86 size: { 61 color: "#FF0000",
87 size: 16, 62 size_id: 0,
88 unit: 'oz' 63 user_id: 0
89 } 64 };
90 }; 65
91 66 const { subscribe, set, update } = writable<Nullable<Preference>>(preferenceObj);
92 const { subscribe, set } = writable<Preference>(preferenceObj); 67
93 68 const setPreference = (preference: Preference) => {
94 return { 69 localStorage.setItem("preference", JSON.stringify(preference));
95 subscribe, 70 set(preference);
96 set 71 };
97 } 72
73 const reset = () => {
74 localStorage.removeItem("preference");
75 set(null);
76 };
77
78 return {
79 set,
80 subscribe,
81 reset,
82 update,
83 setPreference,
84 };
98} 85}
99 86
100export const token = createTokenStore(); 87export const token = createTokenStore();
diff --git a/fe/src/types.ts b/fe/src/types.ts
index 526e7eb..c8f2f00 100644
--- a/fe/src/types.ts
+++ b/fe/src/types.ts
@@ -1,14 +1,19 @@
1export interface Size { 1import type { Invalidator, Subscriber, Unsubscriber, Updater } from "svelte/store";
2 size: number;
3 unit: string;
4}
5 2
6export interface Preference { 3export interface Preference {
4 id: number;
7 color: string; 5 color: string;
8 size: Size; 6 size_id: number;
7 user_id: number;
8}
9
10export interface Size {
11 size: number;
12 unit: string;
9} 13}
10 14
11export interface User { 15export interface User {
16 id: number;
12 name: string; 17 name: string;
13 uuid: string; 18 uuid: string;
14} 19}
@@ -17,4 +22,32 @@ export interface Statistic {
17 user_id: string; 22 user_id: string;
18 date: string; 23 date: string;
19 quantity: number; 24 quantity: number;
20} \ No newline at end of file 25}
26
27export type Nullable<T> = T | null;
28
29export interface User {
30 uuid: string;
31 username: string;
32}
33
34export interface TokenStore {
35 subscribe: (run: Subscriber<Nullable<string>>, invalidate?: Invalidator<Nullable<string>>) => Unsubscriber,
36 authenticate: (newToken: string) => void,
37 unauthenticate: () => void
38}
39
40
41export interface UserStore {
42 subscribe: (run: Subscriber<Nullable<User>>, invalidate?: Invalidator<Nullable<User>>) => Unsubscriber,
43 setUser: (user: User) => void,
44 reset: () => void
45}
46
47export interface PreferenceStore {
48 set: (this: void, value: Preference) => void;
49 subscribe: (this: void, run: Subscriber<Nullable<Preference>>, invalidate?: Invalidator<Nullable<Preference>>) => Unsubscriber;
50 reset: () => void;
51 update: (this: void, updater: Updater<Nullable<Preference>>) => void;
52 setPreference: (user: Preference) => void;
53}