aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDoog <157747121+doogongithub@users.noreply.github.com>2024-02-29 20:13:48 -0500
committerDoog <157747121+doogongithub@users.noreply.github.com>2024-02-29 20:13:48 -0500
commit9f9a33cbf55d38987a66b709284d2bb4ffea0fe9 (patch)
tree1e0539e708983ca05bb4e07d22b9ec10b95d2473
parente37c73e33a4aaf7fb8d25b5af03627f20bcda19f (diff)
modify api, build additional FE components, add types
-rw-r--r--api/go.mod3
-rw-r--r--api/go.sum2
-rw-r--r--api/lib/models.go38
-rw-r--r--api/main.go48
-rw-r--r--db/scripts/water_init.sql40
-rw-r--r--db/water.sqlite3bin24576 -> 36864 bytes
-rw-r--r--fe/src/lib/DataView.svelte67
-rw-r--r--fe/src/lib/LoginForm.svelte16
-rw-r--r--fe/src/lib/PreferencesForm.svelte45
-rw-r--r--fe/src/lib/Table.svelte19
-rw-r--r--fe/src/stores/auth.ts57
-rw-r--r--fe/src/types.ts14
12 files changed, 303 insertions, 46 deletions
diff --git a/api/go.mod b/api/go.mod
index 08ad9e1..6a326bc 100644
--- a/api/go.mod
+++ b/api/go.mod
@@ -4,7 +4,9 @@ go 1.18
4 4
5require ( 5require (
6 github.com/gin-gonic/gin v1.9.1 6 github.com/gin-gonic/gin v1.9.1
7 github.com/google/uuid v1.6.0
7 github.com/mattn/go-sqlite3 v1.14.22 8 github.com/mattn/go-sqlite3 v1.14.22
9 golang.org/x/crypto v0.19.0
8) 10)
9 11
10require ( 12require (
@@ -27,7 +29,6 @@ require (
27 github.com/twitchyliquid64/golang-asm v0.15.1 // indirect 29 github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
28 github.com/ugorji/go/codec v1.2.12 // indirect 30 github.com/ugorji/go/codec v1.2.12 // indirect
29 golang.org/x/arch v0.7.0 // indirect 31 golang.org/x/arch v0.7.0 // indirect
30 golang.org/x/crypto v0.19.0 // indirect
31 golang.org/x/net v0.21.0 // indirect 32 golang.org/x/net v0.21.0 // indirect
32 golang.org/x/sys v0.17.0 // indirect 33 golang.org/x/sys v0.17.0 // indirect
33 golang.org/x/text v0.14.0 // indirect 34 golang.org/x/text v0.14.0 // indirect
diff --git a/api/go.sum b/api/go.sum
index 5174feb..115a832 100644
--- a/api/go.sum
+++ b/api/go.sum
@@ -29,6 +29,8 @@ github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
29github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= 29github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
30github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= 30github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
31github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 31github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
32github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
33github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
32github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 34github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
33github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 35github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
34github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= 36github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
diff --git a/api/lib/models.go b/api/lib/models.go
index 92e5703..f959519 100644
--- a/api/lib/models.go
+++ b/api/lib/models.go
@@ -1,23 +1,41 @@
1package models 1package models
2 2
3import "time" 3import (
4 "time"
5 "github.com/google/uuid"
6)
4 7
5type Statistic struct { 8type Statistic struct {
6 ID int64 `json:"id"` 9 ID int64 `json:"-"`
7 Date time.Time `json:"date"` 10 Date time.Time `json:"date"`
8 UserID int64 `json:"user_id"` 11 User User `json:"user"`
9 Quantity int `json:"quantity"` 12 Quantity int `json:"quantity"`
10} 13}
11 14
12type User struct { 15type User struct {
13 ID int64 16 ID int64 `json:"-"`
14 Name string 17 Name string `json:"name"`
18 UUID uuid.UUID `json:"uuid"`
19 Password string `json:"-"`
15} 20}
16 21
17type Token struct { 22type Token struct {
18 ID int64 23 ID int64 `json:"-"`
19 UserID int64 24 UserID int64 `json:"user_id"`
20 Token string 25 Token string `json:"token"`
21 CreatedAt time.Time 26 CreatedAt time.Time `json:"created_at"`
22 ExpiredAt time.Time 27 ExpiredAt time.Time `json:"expired_at"`
28}
29
30type Preference struct {
31 ID int64 `json:"-"`
32 Color string `json:"color"`
33 UserID int64 `json:"-"`
34 Size Size `json:"size"`
35}
36
37type Size struct {
38 ID int64 `json:"-"`
39 Size int64 `json:"size"`
40 Unit string `json:"unit"`
23} 41}
diff --git a/api/main.go b/api/main.go
index 292a5f9..91b7929 100644
--- a/api/main.go
+++ b/api/main.go
@@ -10,6 +10,7 @@ import (
10 10
11 "github.com/gin-gonic/gin" 11 "github.com/gin-gonic/gin"
12 _ "github.com/mattn/go-sqlite3" 12 _ "github.com/mattn/go-sqlite3"
13 "golang.org/x/crypto/bcrypt"
13 "water/api/lib" 14 "water/api/lib"
14) 15)
15 16
@@ -29,6 +30,7 @@ func CORSMiddleware() gin.HandlerFunc {
29 } 30 }
30} 31}
31 32
33// generatToken will g
32func generateToken() string { 34func generateToken() string {
33 token := make([]byte, 32) 35 token := make([]byte, 32)
34 rand.Read(token) 36 rand.Read(token)
@@ -43,6 +45,7 @@ func establishDBConnection() *sql.DB {
43 return db 45 return db
44} 46}
45 47
48
46func checkForTokenInContext(c *gin.Context) (string, error) { 49func checkForTokenInContext(c *gin.Context) (string, error) {
47 authorizationHeader := c.GetHeader("Authorization") 50 authorizationHeader := c.GetHeader("Authorization")
48 if authorizationHeader == "" { 51 if authorizationHeader == "" {
@@ -54,6 +57,7 @@ func checkForTokenInContext(c *gin.Context) (string, error) {
54 if len(parts) != 2 || parts[0] != "Bearer" { 57 if len(parts) != 2 || parts[0] != "Bearer" {
55 return "", errors.New("Invalid Authorization header format") 58 return "", errors.New("Invalid Authorization header format")
56 } 59 }
60
57 61
58 return parts[1], nil 62 return parts[1], nil
59} 63}
@@ -73,15 +77,6 @@ func TokenRequired() gin.HandlerFunc {
73 } 77 }
74} 78}
75 79
76type User struct {
77 Username string
78 Password string
79}
80
81var users = map[string]User{
82 "user1": {"user1", "password1"},
83}
84
85func setupRouter() *gin.Engine { 80func setupRouter() *gin.Engine {
86 // Disable Console Color 81 // Disable Console Color
87 // gin.DisableConsoleColor() 82 // gin.DisableConsoleColor()
@@ -100,16 +95,31 @@ func setupRouter() *gin.Engine {
100 return 95 return
101 } 96 }
102 97
103 user, exists := users[username] 98 db := establishDBConnection()
99 defer db.Close()
100
101 var user models.User
102 var preference models.Preference
103 var size models.Size
104
105 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)
106 if err := row.Scan(&user.Name, &user.UUID, &user.Password, &preference.Color, &size.Size, &size.Unit); err != nil {
107 if err == sql.ErrNoRows {
108 c.AbortWithStatus(http.StatusUnauthorized)
109 return
110 }
111 }
112
113 if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password)); err != nil {
114 c.AbortWithStatus(http.StatusUnauthorized)
115 return
116 }
104 117
105 if !exists || user.Password != password { 118 preference.Size = size
106 c.AbortWithStatus(http.StatusUnauthorized)
107 return
108 }
109 119
110 // Generate a simple API token 120 // Generate a simple API token
111 apiToken := generateToken() 121 apiToken := generateToken()
112 c.JSON(http.StatusOK, gin.H{"token": apiToken}) 122 c.JSON(http.StatusOK, gin.H{"token": apiToken, "user": user, "preferences": preference})
113 }) 123 })
114 124
115 stats := api.Group("stats") 125 stats := api.Group("stats")
@@ -119,7 +129,7 @@ func setupRouter() *gin.Engine {
119 db := establishDBConnection() 129 db := establishDBConnection()
120 defer db.Close() 130 defer db.Close()
121 131
122 rows, err := db.Query("SELECT * FROM statistics"); 132 rows, err := db.Query("SELECT s.date, s.quantity, u.uuid, u.name FROM Statistics s INNER JOIN Users u ON u.id = s.user_id");
123 if err != nil { 133 if err != nil {
124 c.JSON(500, gin.H{"error": err.Error()}) 134 c.JSON(500, gin.H{"error": err.Error()})
125 return 135 return
@@ -129,10 +139,12 @@ func setupRouter() *gin.Engine {
129 var data []models.Statistic 139 var data []models.Statistic
130 for rows.Next() { 140 for rows.Next() {
131 var stat models.Statistic 141 var stat models.Statistic
132 if err := rows.Scan(&stat.ID, &stat.Date, &stat.UserID, &stat.Quantity); err != nil { 142 var user models.User
143 if err := rows.Scan(&stat.Date, &stat.Quantity, &user.UUID, &user.Name); err != nil {
133 c.JSON(500, gin.H{"error": err.Error()}) 144 c.JSON(500, gin.H{"error": err.Error()})
134 return 145 return
135 } 146 }
147 stat.User = user
136 data = append(data, stat) 148 data = append(data, stat)
137 } 149 }
138 150
@@ -150,7 +162,7 @@ func setupRouter() *gin.Engine {
150 db := establishDBConnection() 162 db := establishDBConnection()
151 defer db.Close() 163 defer db.Close()
152 164
153 result, err := db.Exec("INSERT INTO statistics (date, user_id, quantity) values (?, ?, ?)", stat.Date, stat.UserID, stat.Quantity) 165 result, err := db.Exec("INSERT INTO statistics (date, user_id, quantity) values (?, ?, ?)", stat.Date, 1, stat.Quantity)
154 166
155 if err != nil { 167 if err != nil {
156 c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) 168 c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
diff --git a/db/scripts/water_init.sql b/db/scripts/water_init.sql
index 0751c41..6a4de24 100644
--- a/db/scripts/water_init.sql
+++ b/db/scripts/water_init.sql
@@ -1,8 +1,9 @@
1-- user table for users. 1-- user table for users.
2CREATE TABLE IF NOT EXISTS Users ( 2CREATE TABLE IF NOT EXISTS Users (
3 id INTEGER PRIMARY KEY, 3 id INTEGER PRIMARY KEY,
4 name TEXT NOT NULL, 4 password TEXT UNIQUE NOT NULL,
5 UNIQUE(name) 5 uuid TEXT UNIQUE NOT NULL,
6 name TEXT UNIQUE NOT NULL
6); 7);
7 8
8-- statistics table for users to log their consumption 9-- statistics table for users to log their consumption
@@ -19,7 +20,7 @@ CREATE TABLE IF NOT EXISTS Preferences (
19 color TEXT NOT NULL DEFAULT "#000000", 20 color TEXT NOT NULL DEFAULT "#000000",
20 user_id INT NOT NULL, 21 user_id INT NOT NULL,
21 size_id INT NOT NULL DEFAULT 1, 22 size_id INT NOT NULL DEFAULT 1,
22 FOREIGN KEY(user_id) REFERENCES Users(id) ON DELETE CASCADE 23 FOREIGN KEY(user_id) REFERENCES Users(id) ON DELETE CASCADE,
23 FOREIGN KEY(size_id) REFERENCES Sizes(id) 24 FOREIGN KEY(size_id) REFERENCES Sizes(id)
24); 25);
25 26
@@ -30,13 +31,42 @@ CREATE TABLE IF NOT EXISTS Sizes (
30 unit TEXT DEFAULT "oz" 31 unit TEXT DEFAULT "oz"
31); 32);
32 33
34CREATE TABLE IF NOT EXISTS APIToken (
35 id INTEGER PRIMARY KEY,
36 token TEXT NOT NULL,
37 user_id INTEGER NOT NULL,
38 FOREIGN KEY(user_id) REFERENCES Users(id) ON DELETE CASCADE
39);
40
33-- create default sizes for sizes lookup table. 41-- create default sizes for sizes lookup table.
34INSERT OR IGNORE INTO Sizes (id, size) VALUES (1, 8), (2, 16), (3, 24), (4, 32), (5, 40), (6, 48); 42INSERT OR IGNORE INTO Sizes (id, size) VALUES (1, 8), (2, 16), (3, 24), (4, 32), (5, 40), (6, 48);
35 43
36-- create default users. 44-- create default users.
37INSERT OR IGNORE INTO Users (id, name) VALUES (1, 'Parker'), (2, 'Zach'); 45INSERT OR IGNORE INTO Users (name, password, uuid) VALUES (
46 'parker',
47 '$2y$10$2UlKrQJQV5cQOo/8VcFlq.ai3MWf7mA4//knEs2xVnHTeB.RnfN.m',
48 '1aa668f3-7527-4a67-9c24-fdf307542eeb'
49), (
50 'zach',
51 '$2y$10$35UJnLpBj8ulhqN/3G4qKe0GYBOa/YunXit11n7ET6zknZpNeKpRS',
52 'be3fd6b7-cf55-4eb8-92d8-1b745b439f34'
53);
38 54
39-- create default preferences. 55-- create default preferences.
40INSERT OR IGNORE INTO Preferences (id, user_id) VALUES (1, 1), (2, 2); 56INSERT OR IGNORE INTO Preferences (user_id) VALUES (1), (2);
41 57
58CREATE TRIGGER IF NOT EXISTS enforce_size_id
59BEFORE INSERT ON Preferences
60BEGIN
61 SELECT
62 CASE
63 WHEN (
64 SELECT COUNT(*) FROM Sizes WHERE id = new.size_id
65 ) = 0
66 THEN RAISE(ABORT, 'Size does not exist')
67 END;
68END;
42 69
70--
71CREATE VIEW IF NOT EXISTS aggregated_stats AS
72 SELECT u.uuid, SUM(s.quantity * s.size) from Statistics s INNER JOIN Users u ON u.id = s.user_id INNER JOIN Preferences p ON p.user_id = u.id INNER JOIN Size s ON s.id = p.size_id;
diff --git a/db/water.sqlite3 b/db/water.sqlite3
index c800708..716c5a4 100644
--- a/db/water.sqlite3
+++ b/db/water.sqlite3
Binary files differ
diff --git a/fe/src/lib/DataView.svelte b/fe/src/lib/DataView.svelte
index cd7b042..dc8acae 100644
--- a/fe/src/lib/DataView.svelte
+++ b/fe/src/lib/DataView.svelte
@@ -1,11 +1,24 @@
1<script lang='ts'> 1<script lang='ts'>
2import { onMount } from 'svelte'; 2import { onMount } from 'svelte';
3import { token } from '../stores/auth' 3import type { Preference } from '../types';
4import { token, user, preferences } from '../stores/auth'
4import Table from './Table.svelte'; 5import Table from './Table.svelte';
6import PreferencesForm from './PreferencesForm.svelte';
7
8const formatter = new Intl.DateTimeFormat(
9 'en',
10 {
11 year: 'numeric',
12 month: '2-digit',
13 day: '2-digit'
14 }
15);
5 16
6let json; 17let json;
7let showAddForm: boolean = false; 18let showAddForm: boolean = false;
8 19
20let statistic: Statistic = newStatistic();
21
9async function fetchData() { 22async function fetchData() {
10 const res = await fetch('http://localhost:8080/api/v1/stats/', { 23 const res = await fetch('http://localhost:8080/api/v1/stats/', {
11 method: "GET", 24 method: "GET",
@@ -40,7 +53,32 @@ function handleClick() {
40} 53}
41 54
42function handleAddDialogSubmit (e) { 55function handleAddDialogSubmit (e) {
43 console.log(e.keyCode) 56 console.log(statistic);
57 showAddForm = false;
58}
59
60function closeDialog () {
61 showAddForm = false;
62}
63
64function newStatistic (): Statistic {
65 let now = new Date(), month, day, year;
66
67 month = `${now.getMonth() + 1}`;
68 day = `${now.getDate()}`;
69 year = now.getFullYear();
70 if (month.length < 2)
71 month = '0' + month;
72 if (day.length < 2)
73 day = '0' + day;
74
75 const date = [year, month, day].join('-');
76
77 return {
78 user_id: $user.uuid,
79 date,
80 quantity: 1
81 }
44} 82}
45 83
46onMount(() => { 84onMount(() => {
@@ -50,10 +88,19 @@ onMount(() => {
50</script> 88</script>
51<div> 89<div>
52 <button on:click={submitStat}>Add Stat Test</button> 90 <button on:click={submitStat}>Add Stat Test</button>
91 <PreferencesForm />
53 <dialog open={showAddForm} on:submit={handleAddDialogSubmit}> 92 <dialog open={showAddForm} on:submit={handleAddDialogSubmit}>
93 <h2>Add Water</h2>
54 <form method="dialog"> 94 <form method="dialog">
55 <input name="date" type="date" /> 95 <div class='form input group'>
56 <input name="quantity" type="number" min="0" autocomplete="off"/> 96 <label for="date">Date:</label>
97 <input bind:value={statistic.date} id="date" name="date" type="date" />
98 </div>
99 <div class='form input group'>
100 <label for="quantity">Quantity:</label>
101 <input bind:value={statistic.quantity} id="quantity" name="quantity" type="number" min="0" autocomplete="off"/>
102 </div>
103 <button on:click={closeDialog}>Cancel</button>
57 <button type="submit">Submit</button> 104 <button type="submit">Submit</button>
58 </form> 105 </form>
59 </dialog> 106 </dialog>
@@ -65,3 +112,15 @@ onMount(() => {
65 {/await} 112 {/await}
66 <!-- <Chart /> --> 113 <!-- <Chart /> -->
67</div> 114</div>
115
116<style>
117dialog {
118 background: red;
119 box-shadow: 0 20px 5em 10px rgba(0,0,0,0.8);
120}
121dialog::backdrop {
122 padding: 20px;
123 box-shadow: 20px 20px rgba(0,0,0,0.8);
124 background-color: red;
125}
126</style>
diff --git a/fe/src/lib/LoginForm.svelte b/fe/src/lib/LoginForm.svelte
index 22c0faf..499a457 100644
--- a/fe/src/lib/LoginForm.svelte
+++ b/fe/src/lib/LoginForm.svelte
@@ -1,8 +1,8 @@
1<script lang='ts'> 1<script lang='ts'>
2import { token } from '../stores/auth'; 2import { token, user, preferences } from '../stores/auth';
3import Card from './Card.svelte'; 3import Card from './Card.svelte';
4 4
5let user = { 5let credentials: CredentialObject = {
6 username: '', 6 username: '',
7 password: '' 7 password: ''
8} 8}
@@ -19,11 +19,11 @@ function prepareCredentials ({ username, password }: CredentialObject): string {
19} 19}
20 20
21async function onSubmit (e) { 21async function onSubmit (e) {
22 if (!user.username || !user.password) { 22 if (!credentials.username || !credentials.password) {
23 error = 'please enter your username and password'; 23 error = 'please enter your username and password';
24 return; 24 return;
25 } 25 }
26 const auth = prepareCredentials(user); 26 const auth = prepareCredentials(credentials);
27 27
28 const response = await fetch('http://localhost:8080/api/v1/auth', { 28 const response = await fetch('http://localhost:8080/api/v1/auth', {
29 method: 'POST', 29 method: 'POST',
@@ -38,7 +38,9 @@ async function onSubmit (e) {
38 } 38 }
39 39
40 if (response.ok) { 40 if (response.ok) {
41 const { token: apiToken } = await response.json(); 41 const { token: apiToken, user: userData, preferences: userPreferences } = await response.json();
42 user.setUser(userData);
43 preferences.set(userPreferences);
42 token.authenticate(apiToken); 44 token.authenticate(apiToken);
43 } 45 }
44 46
@@ -50,11 +52,11 @@ async function onSubmit (e) {
50 <form class="form" on:submit|preventDefault={onSubmit}> 52 <form class="form" on:submit|preventDefault={onSubmit}>
51 <div class='form input group'> 53 <div class='form input group'>
52 <label for="username">Username</label> 54 <label for="username">Username</label>
53 <input bind:value={user.username} id="username" name='username' type="text" autocomplete="username" /> 55 <input bind:value={credentials.username} id="username" name='username' type="text" autocomplete="username" />
54 </div> 56 </div>
55 <div class='form input group'> 57 <div class='form input group'>
56 <label for="password">Password</label> 58 <label for="password">Password</label>
57 <input bind:value={user.password} id="password" name='password' type="password" autocomplete="current-password"/> 59 <input bind:value={credentials.password} id="password" name='password' type="password" autocomplete="current-password"/>
58 </div> 60 </div>
59 {#if error} 61 {#if error}
60 <p class="error">{error}</p> 62 <p class="error">{error}</p>
diff --git a/fe/src/lib/PreferencesForm.svelte b/fe/src/lib/PreferencesForm.svelte
new file mode 100644
index 0000000..781866c
--- /dev/null
+++ b/fe/src/lib/PreferencesForm.svelte
@@ -0,0 +1,45 @@
1<script lang="ts">
2import { preferences } from '../stores/auth';
3import type { Size, Preference } from '../types';
4 export let open: boolean = true;
5
6 let preference: Preference = {
7 color: "#00FF00",
8 size: {
9 size: 8,
10 unit: 'oz'
11 }
12 }
13
14 preferences.subscribe((value) => {
15 preference = value;
16 });
17
18 function onPreferencesSave(): void {
19 preferences.set(preferences);
20 }
21</script>
22<dialog {open}>
23 <h2>User Preferences</h2>
24 <form method="dialog">
25 <div class="form input group">
26 <label>Color</label>
27 <input type="color" bind:value={preference.color}/>
28 </div>
29 <div class="form input group">
30 <label>Bottle Size</label>
31 <select bind:value={preference.size.size}>
32 {#each [8,16,24,32,40,48] as size}
33 <option>{ size }</option>
34 {/each}
35 </select>
36 </div>
37 <button type="submit">Save</button>
38 </form>
39</dialog>
40<style>
41dialog {
42 background: white;
43 color: black;
44}
45</style>
diff --git a/fe/src/lib/Table.svelte b/fe/src/lib/Table.svelte
index 5572280..4b81800 100644
--- a/fe/src/lib/Table.svelte
+++ b/fe/src/lib/Table.svelte
@@ -30,6 +30,11 @@ function formatDatum([key, value]: any[]) {
30 const parsedDate = new Date(value); 30 const parsedDate = new Date(value);
31 return formatter.format(parsedDate); 31 return formatter.format(parsedDate);
32 } 32 }
33
34 if (key === 'user') {
35 return value['name'];
36 }
37
33 return value; 38 return value;
34} 39}
35 40
@@ -78,9 +83,23 @@ table {
78 padding: 16px; 83 padding: 16px;
79 margin: 8px; 84 margin: 8px;
80 border: solid 1px black; 85 border: solid 1px black;
86 border-collapse: collapse;
81} 87}
82 88
83th { 89th {
84 text-transform: capitalize; 90 text-transform: capitalize;
85} 91}
92
93thead tr {
94 background: rgba(0,0,23, 0.34);
95}
96
97tbody tr:nth-child(odd) {
98 background: rgba(0,0,23,0.14);
99}
100
101th, td {
102 padding: 1em;
103 border: 1px solid rgba(0,0,0, 1);
104}
86</style> 105</style>
diff --git a/fe/src/stores/auth.ts b/fe/src/stores/auth.ts
index 7e70cda..10e6bd3 100644
--- a/fe/src/stores/auth.ts
+++ b/fe/src/stores/auth.ts
@@ -1,4 +1,5 @@
1import type { Invalidator, Subscriber, Unsubscriber } from 'svelte/store'; 1import type { Invalidator, Subscriber, Unsubscriber } from 'svelte/store';
2import type { Preference } from '../types';
2import { writable, derived } from 'svelte/store'; 3import { writable, derived } from 'svelte/store';
3 4
4type Nullable<T> = T | null; 5type Nullable<T> = T | null;
@@ -14,6 +15,18 @@ interface TokenStore {
14 unauthenticate: () => void 15 unauthenticate: () => void
15} 16}
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<Nullable<Preference>>, invalidate: Invalidator<Nullable<Preference>>) => Unsubscriber,
27 set: (this: void, value: Nullable<Preference>) => void
28}
29
17function createTokenStore(): TokenStore { 30function createTokenStore(): TokenStore {
18 const storedToken = localStorage.getItem("token"); 31 const storedToken = localStorage.getItem("token");
19 const { subscribe, set } = writable<string | null>(storedToken); 32 const { subscribe, set } = writable<string | null>(storedToken);
@@ -43,6 +56,48 @@ function onTokenChange ($token: Nullable<string>): boolean {
43 return $token ? true : false; 56 return $token ? true : false;
44} 57}
45 58
59function createUserStore(): UserStore {
60 const user = localStorage.getItem('user');
61 const userObj: Nullable<User> = user ? JSON.parse(user) : null;
62 const { subscribe, set } = writable<User | null>(userObj);
63
64 const setUser = (user: User) => {
65 localStorage.setItem('user', JSON.stringify(user));
66 set(user);
67 }
68
69 const reset = () => {
70 localStorage.removeItem('user');
71 set(null);
72 }
73
74 return {
75 subscribe,
76 setUser,
77 reset
78 }
79}
80
81
82function createPreferenceStore(): PreferenceStore {
83 const preferences = localStorage.getItem('preferences');
84 const preferenceObj: Preference = preferences ? JSON.parse(preferences) : {
85 color: "#FF0000",
86 size: {
87 size: 16,
88 unit: 'oz'
89 }
90 };
91
92 const { subscribe, set } = writable<Nullable<Preference>>(preferenceObj);
93
94 return {
95 subscribe,
96 set
97 }
98}
99
46export const token = createTokenStore(); 100export const token = createTokenStore();
47export const authenticated = derived(token, onTokenChange); 101export const authenticated = derived(token, onTokenChange);
48export const user = writable<User | null>(null); 102export const user = createUserStore();
103export const preferences = createPreferenceStore();
diff --git a/fe/src/types.ts b/fe/src/types.ts
new file mode 100644
index 0000000..03d613d
--- /dev/null
+++ b/fe/src/types.ts
@@ -0,0 +1,14 @@
1export interface Size {
2 size: number;
3 unit: string;
4}
5
6export interface Preference {
7 color: string;
8 size: Size;
9}
10
11export interface User {
12 name: string;
13 uuid: string;
14}