diff options
author | Zach Berwaldt <zberwaldt@tutamail.com> | 2024-03-15 18:49:43 -0400 |
---|---|---|
committer | Zach Berwaldt <zberwaldt@tutamail.com> | 2024-03-15 18:49:43 -0400 |
commit | 9cae9c1d2a0b4f7fa72f3075541b9ffafe1a7275 (patch) | |
tree | 960fa4f96a1328861a06d97180da8601af6855da | |
parent | 8fab2d03bce82e4dee798ebffb1e93c557f62a4b (diff) |
Add routes for preference, clean up and add types
-rw-r--r-- | api/internal/controllers/auth.go | 18 | ||||
-rw-r--r-- | api/internal/controllers/preferences.go | 45 | ||||
-rw-r--r-- | api/internal/controllers/user.go | 59 | ||||
-rw-r--r-- | api/internal/middleware/middleware.go | 7 | ||||
-rw-r--r-- | api/internal/models/auth.go | 2 | ||||
-rw-r--r-- | api/internal/models/preferences.go | 8 | ||||
-rw-r--r-- | api/internal/models/statistics.go | 2 | ||||
-rw-r--r-- | api/internal/models/user.go | 2 | ||||
-rw-r--r-- | api/internal/router/router.go | 5 | ||||
-rw-r--r-- | fe/src/app.css | 4 | ||||
-rw-r--r-- | fe/src/http.ts | 96 | ||||
-rw-r--r-- | fe/src/lib/Card.svelte | 1 | ||||
-rw-r--r-- | fe/src/lib/Chart.svelte | 63 | ||||
-rw-r--r-- | fe/src/lib/DataView.svelte | 40 | ||||
-rw-r--r-- | fe/src/lib/Layout.svelte | 8 | ||||
-rw-r--r-- | fe/src/lib/LoginForm.svelte | 2 | ||||
-rw-r--r-- | fe/src/lib/PreferencesForm.svelte | 145 | ||||
-rw-r--r-- | fe/src/lib/errors.ts | 6 | ||||
-rw-r--r-- | fe/src/lib/utils.ts | 2 | ||||
-rw-r--r-- | fe/src/stores/auth.ts | 153 | ||||
-rw-r--r-- | fe/src/types.ts | 45 |
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 @@ | |||
1 | package controllers | ||
2 | |||
3 | import ( | ||
4 | "github.com/gin-gonic/gin" | ||
5 | "net/http" | ||
6 | "database/sql" | ||
7 | "water/api/internal/database" | ||
8 | "water/api/internal/models" | ||
9 | ) | ||
10 | |||
11 | func 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 @@ | |||
1 | package controllers | 1 | package controllers |
2 | 2 | ||
3 | import ( | 3 | import ( |
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 | ||
8 | func GetUser(c *gin.Context) { | 14 | func 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 | } |
11 | func GetUserPreferences(c *gin.Context) { | 17 | func 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 | ||
15 | func UpdateUserPreferences(c *gin.Context) { | 43 | func 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 | ||
3 | import ( | 3 | import ( |
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 | ||
11 | func TokenRequired() gin.HandlerFunc { | 12 | func 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 | |||
3 | import "time" | 3 | import "time" |
4 | 4 | ||
5 | type Token struct { | 5 | type 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 @@ | |||
1 | package models | 1 | package models |
2 | 2 | ||
3 | type Preference struct { | 3 | type 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 | ||
10 | type Size struct { | 10 | type 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 | |||
3 | import "time" | 3 | import "time" |
4 | 4 | ||
5 | type Statistic struct { | 5 | type 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 | |||
3 | import "github.com/google/uuid" | 3 | import "github.com/google/uuid" |
4 | 4 | ||
5 | type User struct { | 5 | type 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 @@ | |||
1 | export default class HttpClient { | 1 | let instance; |
2 | private static instance: HttpClient; | 2 | const baseUrl = import.meta.env?.VITE_API_BASE_URL ?? "http://localhost:8080/api/v1"; |
3 | |||
4 | class 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 | ||
56 | interface IHttpParameters { | 83 | interface 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 | |||
90 | let http: Readonly<HttpClient> = Object.freeze(new HttpClient(baseUrl)); | ||
91 | |||
92 | export 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"> | ||
2 | import { onDestroy } from "svelte"; | ||
3 | import ChartJS from "chart.js/auto"; | ||
4 | |||
5 | export let data; | ||
6 | export let labels; | ||
7 | export let type = 'bar'; | ||
8 | |||
9 | let ref: HTMLCanvasElement; | ||
10 | let chart | ||
11 | |||
12 | function 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> |
42 | dialog { | 109 | dialog { |
43 | background: white; | 110 | background: white; |
44 | color: black; | 111 | color: black; |
45 | } | 112 | } |
46 | 113 | ||
47 | input[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 @@ | |||
1 | export class UnauthorizedError extends Error { | 1 | export 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 @@ | |||
1 | export function processFormInput(form: HTMLFormElement) { | 1 | export 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 @@ | |||
1 | import type { Invalidator, Subscriber, Unsubscriber } from 'svelte/store'; | 1 | import type { Preference, TokenStore, Nullable, UserStore, User, PreferenceStore } from "../types"; |
2 | import type { Preference } from '../types'; | 2 | import { writable, derived } from "svelte/store"; |
3 | import { writable, derived } from 'svelte/store'; | ||
4 | 3 | ||
5 | type Nullable<T> = T | null; | ||
6 | |||
7 | interface User { | ||
8 | uuid: string; | ||
9 | username: string; | ||
10 | } | ||
11 | |||
12 | interface TokenStore { | ||
13 | subscribe: (run: Subscriber<Nullable<string>>, invalidate?: Invalidator<Nullable<string>>) => Unsubscriber, | ||
14 | authenticate: (newToken: string) => void, | ||
15 | unauthenticate: () => void | ||
16 | } | ||
17 | |||
18 | |||
19 | interface UserStore { | ||
20 | subscribe: (run: Subscriber<Nullable<User>>, invalidate?: Invalidator<Nullable<User>>) => Unsubscriber, | ||
21 | setUser: (user: User) => void, | ||
22 | reset: () => void | ||
23 | } | ||
24 | |||
25 | interface PreferenceStore { | ||
26 | subscribe: (run: Subscriber<Preference>, invalidate?: Invalidator<Preference>) => Unsubscriber, | ||
27 | set: (this: void, value: Preference) => void | ||
28 | } | ||
29 | 4 | ||
30 | function createTokenStore(): TokenStore { | 5 | function 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 | ||
55 | function onTokenChange($token: Nullable<string>): boolean { | 30 | function onTokenChange($token: Nullable<string>): boolean { |
56 | return $token ? true : false; | 31 | return $token ? true : false; |
57 | } | 32 | } |
58 | 33 | ||
59 | function createUserStore(): UserStore { | 34 | function 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 | ||
82 | function createPreferenceStore(): PreferenceStore { | 57 | function 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 | ||
100 | export const token = createTokenStore(); | 87 | export 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 @@ | |||
1 | export interface Size { | 1 | import type { Invalidator, Subscriber, Unsubscriber, Updater } from "svelte/store"; |
2 | size: number; | ||
3 | unit: string; | ||
4 | } | ||
5 | 2 | ||
6 | export interface Preference { | 3 | export interface Preference { |
4 | id: number; | ||
7 | color: string; | 5 | color: string; |
8 | size: Size; | 6 | size_id: number; |
7 | user_id: number; | ||
8 | } | ||
9 | |||
10 | export interface Size { | ||
11 | size: number; | ||
12 | unit: string; | ||
9 | } | 13 | } |
10 | 14 | ||
11 | export interface User { | 15 | export 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 | |||
27 | export type Nullable<T> = T | null; | ||
28 | |||
29 | export interface User { | ||
30 | uuid: string; | ||
31 | username: string; | ||
32 | } | ||
33 | |||
34 | export interface TokenStore { | ||
35 | subscribe: (run: Subscriber<Nullable<string>>, invalidate?: Invalidator<Nullable<string>>) => Unsubscriber, | ||
36 | authenticate: (newToken: string) => void, | ||
37 | unauthenticate: () => void | ||
38 | } | ||
39 | |||
40 | |||
41 | export interface UserStore { | ||
42 | subscribe: (run: Subscriber<Nullable<User>>, invalidate?: Invalidator<Nullable<User>>) => Unsubscriber, | ||
43 | setUser: (user: User) => void, | ||
44 | reset: () => void | ||
45 | } | ||
46 | |||
47 | export 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 | } | ||