diff options
author | Zach Berwaldt <zberwaldt@tutamail.com> | 2024-03-01 18:17:42 -0500 |
---|---|---|
committer | Zach Berwaldt <zberwaldt@tutamail.com> | 2024-03-01 18:17:42 -0500 |
commit | afeffe31bd7d0f8333627a972e1d32e64a325b5b (patch) | |
tree | 6efd6548755e49e379a0a390c96efcf9f7a65eeb | |
parent | 9f9a33cbf55d38987a66b709284d2bb4ffea0fe9 (diff) |
reformat fe
-rw-r--r-- | .gitignore | 2 | ||||
-rw-r--r-- | db/water.sqlite3 | bin | 36864 -> 0 bytes | |||
-rw-r--r-- | fe/.prettierrc | 4 | ||||
-rw-r--r-- | fe/src/App.svelte | 42 | ||||
-rw-r--r-- | fe/src/lib/Card.svelte | 14 | ||||
-rw-r--r-- | fe/src/lib/Counter.svelte | 10 | ||||
-rw-r--r-- | fe/src/lib/DataView.svelte | 177 | ||||
-rw-r--r-- | fe/src/lib/Layout.svelte | 77 | ||||
-rw-r--r-- | fe/src/lib/LoginForm.svelte | 105 | ||||
-rw-r--r-- | fe/src/lib/PreferencesForm.svelte | 32 | ||||
-rw-r--r-- | fe/src/lib/Table.svelte | 138 | ||||
-rw-r--r-- | fe/src/lib/errors.ts | 6 | ||||
-rw-r--r-- | fe/src/lib/utils.ts | 16 | ||||
-rw-r--r-- | fe/src/main.ts | 2 | ||||
-rw-r--r-- | fe/src/stores/auth.ts | 132 | ||||
-rw-r--r-- | fe/src/types.ts | 18 |
16 files changed, 389 insertions, 386 deletions
@@ -46,4 +46,4 @@ node_modules/ | |||
46 | .env.production.local | 46 | .env.production.local |
47 | .env.local | 47 | .env.local |
48 | 48 | ||
49 | 49 | *.sqlite3 | |
diff --git a/db/water.sqlite3 b/db/water.sqlite3 deleted file mode 100644 index 716c5a4..0000000 --- a/db/water.sqlite3 +++ /dev/null | |||
Binary files differ | |||
diff --git a/fe/.prettierrc b/fe/.prettierrc new file mode 100644 index 0000000..222861c --- /dev/null +++ b/fe/.prettierrc | |||
@@ -0,0 +1,4 @@ | |||
1 | { | ||
2 | "tabWidth": 2, | ||
3 | "useTabs": false | ||
4 | } | ||
diff --git a/fe/src/App.svelte b/fe/src/App.svelte index 8811c52..25d53dc 100644 --- a/fe/src/App.svelte +++ b/fe/src/App.svelte | |||
@@ -1,40 +1,16 @@ | |||
1 | <script lang="ts"> | 1 | <script lang="ts"> |
2 | import { onMount, onDestroy } from 'svelte'; | 2 | import Layout from "./lib/Layout.svelte"; |
3 | import Layout from './lib/Layout.svelte' | 3 | import LoginForm from "./lib/LoginForm.svelte"; |
4 | import LoginForm from './lib/LoginForm.svelte'; | 4 | import DataView from "./lib/DataView.svelte"; |
5 | import DataView from './lib/DataView.svelte'; | 5 | import { authenticated } from "./stores/auth"; |
6 | import { authenticated } from './stores/auth'; | ||
7 | </script> | 6 | </script> |
8 | 7 | ||
9 | <main> | 8 | <main> |
10 | <Layout> | 9 | <Layout> |
11 | {#if !$authenticated} | 10 | {#if !$authenticated} |
12 | <LoginForm /> | 11 | <LoginForm /> |
13 | {:else} | 12 | {:else} |
14 | <DataView /> | 13 | <DataView /> |
15 | {/if} | 14 | {/if} |
16 | </Layout> | 15 | </Layout> |
17 | </main> | 16 | </main> |
18 | |||
19 | <style> | ||
20 | .logo { | ||
21 | height: 6em; | ||
22 | padding: 1.5em; | ||
23 | will-change: filter; | ||
24 | transition: filter 300ms; | ||
25 | } | ||
26 | .logo:hover { | ||
27 | filter: drop-shadow(0 0 2em #646cffaa); | ||
28 | } | ||
29 | .logo.svelte:hover { | ||
30 | filter: drop-shadow(0 0 2em #ff3e00aa); | ||
31 | } | ||
32 | .read-the-docs { | ||
33 | color: #888; | ||
34 | } | ||
35 | |||
36 | .error { | ||
37 | font-size: 0.75em; | ||
38 | color: red; | ||
39 | } | ||
40 | </style> | ||
diff --git a/fe/src/lib/Card.svelte b/fe/src/lib/Card.svelte index feb5bcc..0835940 100644 --- a/fe/src/lib/Card.svelte +++ b/fe/src/lib/Card.svelte | |||
@@ -1,16 +1,16 @@ | |||
1 | <script lang="ts"> | 1 | <script lang="ts"> |
2 | export let title; | 2 | export let title = ""; |
3 | </script> | 3 | </script> |
4 | 4 | ||
5 | <div class="card"> | 5 | <div class="card"> |
6 | {#if title} | 6 | {#if title} |
7 | <h2>{title}</h2> | 7 | <h2>{title}</h2> |
8 | {/if} | 8 | {/if} |
9 | <slot /> | 9 | <slot /> |
10 | </div> | 10 | </div> |
11 | 11 | ||
12 | <style> | 12 | <style> |
13 | .card { | 13 | .card { |
14 | background: #fff; | 14 | background: #fff; |
15 | width: 16rem; | 15 | width: 16rem; |
16 | display: flex; | 16 | display: flex; |
@@ -18,5 +18,5 @@ export let title; | |||
18 | align-items: left; | 18 | align-items: left; |
19 | border: solid 2px #00000066; | 19 | border: solid 2px #00000066; |
20 | border-radius: 0.25em; | 20 | border-radius: 0.25em; |
21 | } | 21 | } |
22 | </style> | 22 | </style> |
diff --git a/fe/src/lib/Counter.svelte b/fe/src/lib/Counter.svelte deleted file mode 100644 index 979b4df..0000000 --- a/fe/src/lib/Counter.svelte +++ /dev/null | |||
@@ -1,10 +0,0 @@ | |||
1 | <script lang="ts"> | ||
2 | let count: number = 0 | ||
3 | const increment = () => { | ||
4 | count += 1 | ||
5 | } | ||
6 | </script> | ||
7 | |||
8 | <button on:click={increment}> | ||
9 | count is {count} | ||
10 | </button> | ||
diff --git a/fe/src/lib/DataView.svelte b/fe/src/lib/DataView.svelte index dc8acae..1458c9a 100644 --- a/fe/src/lib/DataView.svelte +++ b/fe/src/lib/DataView.svelte | |||
@@ -1,126 +1,123 @@ | |||
1 | <script lang='ts'> | 1 | <script lang="ts"> |
2 | import { onMount } from 'svelte'; | 2 | import { onMount } from "svelte"; |
3 | import type { Preference } from '../types'; | 3 | import type { Statistic } from "../types"; |
4 | import { token, user, preferences } from '../stores/auth' | 4 | import { token, user } from "../stores/auth"; |
5 | import Table from './Table.svelte'; | 5 | import Table from "./Table.svelte"; |
6 | import PreferencesForm from './PreferencesForm.svelte'; | ||
7 | 6 | ||
8 | const formatter = new Intl.DateTimeFormat( | 7 | let json: Promise<any>; |
9 | 'en', | 8 | let showAddForm: boolean = false; |
10 | { | ||
11 | year: 'numeric', | ||
12 | month: '2-digit', | ||
13 | day: '2-digit' | ||
14 | } | ||
15 | ); | ||
16 | |||
17 | let json; | ||
18 | let showAddForm: boolean = false; | ||
19 | 9 | ||
20 | let statistic: Statistic = newStatistic(); | 10 | let statistic: Statistic = newStatistic(); |
21 | 11 | ||
22 | async function fetchData() { | 12 | async function fetchData() { |
23 | const res = await fetch('http://localhost:8080/api/v1/stats/', { | 13 | const res = await fetch("http://localhost:8080/api/v1/stats/", { |
24 | method: "GET", | 14 | method: "GET", |
25 | headers: { | 15 | headers: { |
26 | 'Authorization': `Bearer ${$token}` | 16 | Authorization: `Bearer ${$token}`, |
27 | } | 17 | }, |
28 | }); | 18 | }); |
29 | if (res.ok) { | 19 | if (res.ok) { |
30 | json = res.json(); | 20 | json = res.json(); |
31 | } else { | 21 | } else { |
32 | throw new Error('There was a problem with your request'); | 22 | throw new Error("There was a problem with your request"); |
33 | } | 23 | } |
34 | } | 24 | } |
35 | 25 | ||
36 | async function submitStat() { | 26 | async function submitStat() { |
37 | const response = await fetch('http://localhost:8080/api/v1/stats/', { | 27 | const response = await fetch("http://localhost:8080/api/v1/stats/", { |
38 | method: "POST", | 28 | method: "POST", |
39 | headers: { | 29 | headers: { |
40 | 'Authorization': `Bearer ${$token}` | 30 | Authorization: `Bearer ${$token}`, |
41 | }, | 31 | }, |
42 | body: JSON.stringify({ | 32 | body: JSON.stringify({ |
43 | date: new Date, | 33 | date: new Date(), |
44 | user_id: 1, | 34 | user_id: 1, |
45 | quantity: 3 | 35 | quantity: 3, |
46 | }) | 36 | }), |
47 | }); | 37 | }); |
48 | fetchData(); | 38 | fetchData(); |
49 | } | 39 | } |
50 | 40 | ||
51 | function handleClick() { | 41 | function handleClick() { |
52 | showAddForm = true; | 42 | showAddForm = true; |
53 | } | 43 | } |
54 | 44 | ||
55 | function handleAddDialogSubmit (e) { | 45 | function handleAddDialogSubmit(e: Event) { |
56 | console.log(statistic); | 46 | console.log(statistic); |
57 | showAddForm = false; | 47 | showAddForm = false; |
58 | } | 48 | } |
59 | 49 | ||
60 | function closeDialog () { | 50 | function closeDialog() { |
61 | showAddForm = false; | 51 | showAddForm = false; |
62 | } | 52 | } |
63 | 53 | ||
64 | function newStatistic (): Statistic { | 54 | function newStatistic(): Statistic { |
65 | let now = new Date(), month, day, year; | 55 | let now = new Date(), |
56 | month, | ||
57 | day, | ||
58 | year; | ||
66 | 59 | ||
67 | month = `${now.getMonth() + 1}`; | 60 | month = `${now.getMonth() + 1}`; |
68 | day = `${now.getDate()}`; | 61 | day = `${now.getDate()}`; |
69 | year = now.getFullYear(); | 62 | year = now.getFullYear(); |
70 | if (month.length < 2) | 63 | if (month.length < 2) month = "0" + month; |
71 | month = '0' + month; | 64 | if (day.length < 2) day = "0" + day; |
72 | if (day.length < 2) | ||
73 | day = '0' + day; | ||
74 | 65 | ||
75 | const date = [year, month, day].join('-'); | 66 | const date = [year, month, day].join("-"); |
76 | 67 | ||
77 | return { | 68 | return { |
78 | user_id: $user.uuid, | 69 | user_id: $user!.uuid, |
79 | date, | 70 | date, |
80 | quantity: 1 | 71 | quantity: 1, |
81 | } | 72 | }; |
82 | } | 73 | } |
83 | 74 | ||
84 | onMount(() => { | 75 | onMount(() => { |
85 | fetchData(); | 76 | fetchData(); |
86 | }); | 77 | }); |
87 | |||
88 | </script> | 78 | </script> |
79 | |||
89 | <div> | 80 | <div> |
90 | <button on:click={submitStat}>Add Stat Test</button> | 81 | <button on:click={submitStat}>Add Stat Test</button> |
91 | <PreferencesForm /> | 82 | <dialog open={showAddForm} on:submit={handleAddDialogSubmit}> |
92 | <dialog open={showAddForm} on:submit={handleAddDialogSubmit}> | 83 | <h2>Add Water</h2> |
93 | <h2>Add Water</h2> | 84 | <form method="dialog"> |
94 | <form method="dialog"> | 85 | <div class="form input group"> |
95 | <div class='form input group'> | 86 | <label for="date">Date:</label> |
96 | <label for="date">Date:</label> | 87 | <input bind:value={statistic.date} id="date" name="date" type="date" /> |
97 | <input bind:value={statistic.date} id="date" name="date" type="date" /> | 88 | </div> |
98 | </div> | 89 | <div class="form input group"> |
99 | <div class='form input group'> | 90 | <label for="quantity">Quantity:</label> |
100 | <label for="quantity">Quantity:</label> | 91 | <input |
101 | <input bind:value={statistic.quantity} id="quantity" name="quantity" type="number" min="0" autocomplete="off"/> | 92 | bind:value={statistic.quantity} |
102 | </div> | 93 | id="quantity" |
103 | <button on:click={closeDialog}>Cancel</button> | 94 | name="quantity" |
104 | <button type="submit">Submit</button> | 95 | type="number" |
105 | </form> | 96 | min="0" |
106 | </dialog> | 97 | autocomplete="off" |
107 | <button on:click={handleClick}>Add</button> | 98 | /> |
108 | {#await json then data} | 99 | </div> |
109 | <Table {data} nofooter /> | 100 | <button on:click={closeDialog}>Cancel</button> |
110 | {:catch error} | 101 | <button type="submit">Submit</button> |
111 | <p>{error}</p> | 102 | </form> |
112 | {/await} | 103 | </dialog> |
113 | <!-- <Chart /> --> | 104 | <button on:click={handleClick}>Add</button> |
105 | {#await json then data} | ||
106 | <Table {data} nofooter /> | ||
107 | {:catch error} | ||
108 | <p>{error}</p> | ||
109 | {/await} | ||
110 | <!-- <Chart /> --> | ||
114 | </div> | 111 | </div> |
115 | 112 | ||
116 | <style> | 113 | <style> |
117 | dialog { | 114 | dialog { |
118 | background: red; | 115 | background: red; |
119 | box-shadow: 0 20px 5em 10px rgba(0,0,0,0.8); | 116 | box-shadow: 0 20px 5em 10px rgba(0, 0, 0, 0.8); |
120 | } | 117 | } |
121 | dialog::backdrop { | 118 | dialog::backdrop { |
122 | padding: 20px; | 119 | padding: 20px; |
123 | box-shadow: 20px 20px rgba(0,0,0,0.8); | 120 | box-shadow: 20px 20px rgba(0, 0, 0, 0.8); |
124 | background-color: red; | 121 | background-color: red; |
125 | } | 122 | } |
126 | </style> | 123 | </style> |
diff --git a/fe/src/lib/Layout.svelte b/fe/src/lib/Layout.svelte index f349632..94ce84d 100644 --- a/fe/src/lib/Layout.svelte +++ b/fe/src/lib/Layout.svelte | |||
@@ -1,57 +1,62 @@ | |||
1 | <script> | 1 | <script> |
2 | import { authenticated, token } from '../stores/auth'; | 2 | import { authenticated, token } from "../stores/auth"; |
3 | 3 | import PreferencesForm from "./PreferencesForm.svelte"; | |
4 | const logout = () => token.unauthenticate(); | 4 | const logout = () => token.unauthenticate(); |
5 | 5 | let open = false; | |
6 | function showSettingsDialog() { | 6 | |
7 | console.log('show settings'); | 7 | function showSettingsDialog() { |
8 | } | 8 | open = true; |
9 | 9 | } | |
10 | |||
11 | function closeDialog() { | ||
12 | open = false; | ||
13 | } | ||
10 | </script> | 14 | </script> |
11 | 15 | ||
12 | <div class="layout"> | 16 | <div class="layout"> |
13 | {#if $authenticated} | 17 | {#if $authenticated} |
14 | <nav> | 18 | <nav> |
15 | <div> | 19 | <div> |
16 | <h1>Water</h1> | 20 | <h1>Water</h1> |
17 | </div> | 21 | </div> |
18 | <div> | 22 | <div> |
19 | <button on:click={showSettingsDialog}>Settings</button> | 23 | <button on:click={showSettingsDialog}>Settings</button> |
20 | <button on:click={logout}>Logout</button> | 24 | <button on:click={logout}>Logout</button> |
21 | </div> | 25 | </div> |
22 | </nav> | 26 | </nav> |
23 | {/if} | 27 | <PreferencesForm {open} on:close={closeDialog} /> |
24 | <div id="content"> | 28 | {/if} |
25 | <slot /> | 29 | <div id="content"> |
26 | </div> | 30 | <slot /> |
31 | </div> | ||
27 | </div> | 32 | </div> |
28 | 33 | ||
29 | <style> | 34 | <style> |
30 | .layout { | 35 | .layout { |
31 | height: 100vh; | 36 | height: 100vh; |
32 | } | 37 | } |
33 | nav { | 38 | nav { |
34 | display: flex; | 39 | display: flex; |
35 | flex-direction: row; | 40 | flex-direction: row; |
36 | align-items: center; | 41 | align-items: center; |
37 | justify-content: space-between; | 42 | justify-content: space-between; |
38 | height: 64px; | 43 | height: 64px; |
39 | padding: 0 2em; | 44 | padding: 0 2em; |
40 | } | 45 | } |
41 | 46 | ||
42 | nav div { | 47 | nav div { |
43 | width: fit-content; | 48 | width: fit-content; |
44 | } | 49 | } |
45 | 50 | ||
46 | nav div h1 { | 51 | nav div h1 { |
47 | font-size: 1.75em; | 52 | font-size: 1.75em; |
48 | } | 53 | } |
49 | 54 | ||
50 | #content { | 55 | #content { |
51 | display: flex; | 56 | display: flex; |
52 | flex-direction: column; | 57 | flex-direction: column; |
53 | justify-content: center; | 58 | justify-content: center; |
54 | align-items: center; | 59 | align-items: center; |
55 | padding: 3em 0; | 60 | padding: 3em 0; |
56 | } | 61 | } |
57 | </style> | 62 | </style> |
diff --git a/fe/src/lib/LoginForm.svelte b/fe/src/lib/LoginForm.svelte index 499a457..bf6d9ad 100644 --- a/fe/src/lib/LoginForm.svelte +++ b/fe/src/lib/LoginForm.svelte | |||
@@ -1,66 +1,85 @@ | |||
1 | <script lang='ts'> | 1 | <script lang="ts"> |
2 | import { token, user, preferences } from '../stores/auth'; | 2 | import { token, user, preferences } from "../stores/auth"; |
3 | import Card from './Card.svelte'; | 3 | import Card from "./Card.svelte"; |
4 | 4 | ||
5 | let credentials: CredentialObject = { | 5 | let credentials: CredentialObject = { |
6 | username: '', | 6 | username: "", |
7 | password: '' | 7 | password: "", |
8 | } | 8 | }; |
9 | 9 | ||
10 | let error; | 10 | let error: string | null = null; |
11 | 11 | ||
12 | interface CredentialObject { | 12 | interface CredentialObject { |
13 | username: string; | 13 | username: string; |
14 | password: string; | 14 | password: string; |
15 | } | 15 | } |
16 | 16 | ||
17 | function prepareCredentials ({ username, password }: CredentialObject): string { | 17 | function prepareCredentials({ |
18 | username, | ||
19 | password, | ||
20 | }: CredentialObject): string { | ||
18 | return btoa(`${username}:${password}`); | 21 | return btoa(`${username}:${password}`); |
19 | } | 22 | } |
20 | 23 | ||
21 | async function onSubmit (e) { | 24 | async function onSubmit(e: Event) { |
22 | if (!credentials.username || !credentials.password) { | 25 | if (!credentials.username || !credentials.password) { |
23 | error = 'please enter your username and password'; | 26 | error = "please enter your username and password"; |
24 | return; | 27 | return; |
25 | } | 28 | } |
26 | const auth = prepareCredentials(credentials); | 29 | const auth = prepareCredentials(credentials); |
27 | 30 | ||
28 | const response = await fetch('http://localhost:8080/api/v1/auth', { | 31 | const response = await fetch("http://localhost:8080/api/v1/auth", { |
29 | method: 'POST', | 32 | method: "POST", |
30 | headers: { | 33 | headers: { |
31 | 'Authorization': `Basic ${auth}`, | 34 | Authorization: `Basic ${auth}`, |
32 | }, | 35 | }, |
33 | }); | 36 | }); |
34 | 37 | ||
35 | if (response.status === 401) { | 38 | if (response.status === 401) { |
36 | error = "Your username or password is wrong"; | 39 | error = "Your username or password is wrong"; |
37 | return; | 40 | return; |
38 | } | 41 | } |
39 | 42 | ||
40 | if (response.ok) { | 43 | if (response.ok) { |
41 | const { token: apiToken, user: userData, preferences: userPreferences } = await response.json(); | 44 | const { |
42 | user.setUser(userData); | 45 | token: apiToken, |
43 | preferences.set(userPreferences); | 46 | user: userData, |
44 | token.authenticate(apiToken); | 47 | preferences: userPreferences, |
48 | } = await response.json(); | ||
49 | user.setUser(userData); | ||
50 | preferences.set(userPreferences); | ||
51 | token.authenticate(apiToken); | ||
45 | } | 52 | } |
46 | 53 | ||
47 | error = null; | 54 | error = null; |
48 | } | 55 | } |
49 | </script> | 56 | </script> |
50 | 57 | ||
51 | <Card> | 58 | <Card> |
52 | <form class="form" on:submit|preventDefault={onSubmit}> | 59 | <form class="form" on:submit|preventDefault={onSubmit}> |
53 | <div class='form input group'> | 60 | <div class="form input group"> |
54 | <label for="username">Username</label> | 61 | <label for="username">Username</label> |
55 | <input bind:value={credentials.username} id="username" name='username' type="text" autocomplete="username" /> | 62 | <input |
56 | </div> | 63 | bind:value={credentials.username} |
57 | <div class='form input group'> | 64 | id="username" |
58 | <label for="password">Password</label> | 65 | name="username" |
59 | <input bind:value={credentials.password} id="password" name='password' type="password" autocomplete="current-password"/> | 66 | type="text" |
60 | </div> | 67 | autocomplete="username" |
61 | {#if error} | 68 | /> |
62 | <p class="error">{error}</p> | 69 | </div> |
63 | {/if} | 70 | <div class="form input group"> |
64 | <button type="submit">Log in</button> | 71 | <label for="password">Password</label> |
65 | </form> | 72 | <input |
73 | bind:value={credentials.password} | ||
74 | id="password" | ||
75 | name="password" | ||
76 | type="password" | ||
77 | autocomplete="current-password" | ||
78 | /> | ||
79 | </div> | ||
80 | {#if error} | ||
81 | <p class="error">{error}</p> | ||
82 | {/if} | ||
83 | <button type="submit">Log in</button> | ||
84 | </form> | ||
66 | </Card> | 85 | </Card> |
diff --git a/fe/src/lib/PreferencesForm.svelte b/fe/src/lib/PreferencesForm.svelte index 781866c..95e04c1 100644 --- a/fe/src/lib/PreferencesForm.svelte +++ b/fe/src/lib/PreferencesForm.svelte | |||
@@ -1,7 +1,8 @@ | |||
1 | <script lang="ts"> | 1 | <script lang="ts"> |
2 | import { preferences } from '../stores/auth'; | 2 | import { preferences } from '../stores/auth'; |
3 | import type { Size, Preference } from '../types'; | 3 | import type { Size, Preference } from '../types'; |
4 | export let open: boolean = true; | 4 | import { createEventDispatcher } from 'svelte'; |
5 | export let open: boolean; | ||
5 | 6 | ||
6 | let preference: Preference = { | 7 | let preference: Preference = { |
7 | color: "#00FF00", | 8 | color: "#00FF00", |
@@ -10,29 +11,29 @@ import type { Size, Preference } from '../types'; | |||
10 | unit: 'oz' | 11 | unit: 'oz' |
11 | } | 12 | } |
12 | } | 13 | } |
14 | |||
15 | const dispatch = createEventDispatcher(); | ||
13 | 16 | ||
14 | preferences.subscribe((value) => { | 17 | preferences.subscribe((value) => { |
15 | preference = value; | 18 | preference = value; |
16 | }); | 19 | }); |
17 | 20 | ||
18 | function onPreferencesSave(): void { | 21 | function onPreferencesSave(): void { |
19 | preferences.set(preferences); | 22 | preferences.set(preference); |
23 | dispatch('close') | ||
20 | } | 24 | } |
25 | |||
21 | </script> | 26 | </script> |
22 | <dialog {open}> | 27 | <dialog {open} on:submit|preventDefault={onPreferencesSave}> |
23 | <h2>User Preferences</h2> | 28 | <h2>User Preferences</h2> |
24 | <form method="dialog"> | 29 | <form method="dialog"> |
25 | <div class="form input group"> | 30 | <div class="form input group"> |
26 | <label>Color</label> | 31 | <label for="color">Color</label> |
27 | <input type="color" bind:value={preference.color}/> | 32 | <input id="color" name="color" type="color" bind:value={preference.color}/> |
28 | </div> | 33 | </div> |
29 | <div class="form input group"> | 34 | <div class="form input group"> |
30 | <label>Bottle Size</label> | 35 | <label for="size">Bottle Size</label> |
31 | <select bind:value={preference.size.size}> | 36 | <input id="size" name="size" type="number" min="8" max="48" step="8" 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 | </div> |
37 | <button type="submit">Save</button> | 38 | <button type="submit">Save</button> |
38 | </form> | 39 | </form> |
@@ -42,4 +43,9 @@ dialog { | |||
42 | background: white; | 43 | background: white; |
43 | color: black; | 44 | color: black; |
44 | } | 45 | } |
46 | |||
47 | input[type="color"] { | ||
48 | width: 100%; | ||
49 | height: 100%; | ||
50 | } | ||
45 | </style> | 51 | </style> |
diff --git a/fe/src/lib/Table.svelte b/fe/src/lib/Table.svelte index 4b81800..3a66e0d 100644 --- a/fe/src/lib/Table.svelte +++ b/fe/src/lib/Table.svelte | |||
@@ -1,105 +1,105 @@ | |||
1 | <script lang="ts"> | 1 | <script lang="ts"> |
2 | import {afterUpdate} from 'svelte'; | 2 | export let data: Array<any> | undefined = undefined; |
3 | export let data: Array<any> | undefined = undefined; | 3 | export let nofooter: boolean = false; |
4 | export let nofooter: boolean = false; | 4 | export let noheader: boolean = false; |
5 | export let noheader: boolean = false; | 5 | export let omit: string[] = ["id"]; |
6 | export let omit: string[] = ['id']; | 6 | export let title: string | undefined = undefined; |
7 | export let title: string | undefined = undefined; | ||
8 | 7 | ||
9 | function getDataKeys(data: any[]): string[] { | 8 | function getDataKeys(data: any[]): string[] { |
10 | if (!data || data.length === 0) return []; | 9 | if (!data || data.length === 0) return []; |
11 | return Object.keys(data[0]).map(k => k.split('_').join(' ')).filter(k => !omit.includes(k)); | 10 | return Object.keys(data[0]) |
12 | } | 11 | .map((k) => k.split("_").join(" ")) |
12 | .filter((k) => !omit.includes(k)); | ||
13 | } | ||
13 | 14 | ||
14 | function getRow(row: Record<string, any>): Array<any> { | 15 | function getRow(row: Record<string, any>): Array<any> { |
15 | return Object.entries(row).filter(r => !omit.includes(r[0])); | 16 | return Object.entries(row).filter((r) => !omit.includes(r[0])); |
16 | } | 17 | } |
17 | 18 | ||
18 | const formatter = new Intl.DateTimeFormat('en', { | 19 | const formatter = new Intl.DateTimeFormat("en", { |
19 | year: 'numeric', | 20 | year: "numeric", |
20 | month: 'numeric', | 21 | month: "numeric", |
21 | day: 'numeric', | 22 | day: "numeric", |
22 | hour: 'numeric', | 23 | hour: "numeric", |
23 | minute: '2-digit', | 24 | minute: "2-digit", |
24 | second: '2-digit', | 25 | second: "2-digit", |
25 | timeZone: "America/New_York" | 26 | timeZone: "America/New_York", |
26 | }); | 27 | }); |
27 | 28 | ||
28 | function formatDatum([key, value]: any[]) { | 29 | function formatDatum([key, value]: any[]) { |
29 | if (key === 'date') { | 30 | if (key === "date") { |
30 | const parsedDate = new Date(value); | 31 | const parsedDate = new Date(value); |
31 | return formatter.format(parsedDate); | 32 | return formatter.format(parsedDate); |
32 | } | 33 | } |
33 | 34 | ||
34 | if (key === 'user') { | 35 | if (key === "user") { |
35 | return value['name']; | 36 | return value["name"]; |
36 | } | 37 | } |
37 | 38 | ||
38 | return value; | 39 | return value; |
39 | } | 40 | } |
40 | |||
41 | </script> | 41 | </script> |
42 | |||
42 | <table> | 43 | <table> |
43 | {#if title} | 44 | {#if title} |
44 | <h2>{title}</h2> | 45 | <h2>{title}</h2> |
45 | {/if} | 46 | {/if} |
46 | {#if !noheader} | 47 | {#if !noheader && data} |
47 | <thead> | 48 | <thead> |
48 | <tr> | 49 | <tr> |
49 | {#each getDataKeys(data) as header} | 50 | {#each getDataKeys(data) as header} |
50 | <th>{header}</th> | 51 | <th>{header}</th> |
51 | {/each} | 52 | {/each} |
52 | </tr> | 53 | </tr> |
53 | </thead> | 54 | </thead> |
54 | {/if} | 55 | {/if} |
55 | <tbody> | 56 | <tbody> |
56 | {#if data} | 57 | {#if data} |
57 | {#each data as row} | 58 | {#each data as row} |
58 | <tr> | 59 | <tr> |
59 | {#each getRow(row) as datum} | 60 | {#each getRow(row) as datum} |
60 | |||
61 | <td>{formatDatum(datum)}</td> | 61 | <td>{formatDatum(datum)}</td> |
62 | {/each} | 62 | {/each} |
63 | </tr> | 63 | </tr> |
64 | {/each} | 64 | {/each} |
65 | {:else} | 65 | {:else} |
66 | <tr> | 66 | <tr> There is not data. </tr> |
67 | There is not data. | 67 | {/if} |
68 | </tr> | 68 | </tbody> |
69 | {/if} | 69 | {#if !nofooter} |
70 | </tbody> | ||
71 | {#if !nofooter} | ||
72 | <slot name="footer"> | 70 | <slot name="footer"> |
73 | <tfoot> | 71 | <tfoot> |
74 | <tr> | 72 | <tr> |
75 | <td>Table Footer</td> | 73 | <td>Table Footer</td> |
76 | </tr> | 74 | </tr> |
77 | </tfoot> | 75 | </tfoot> |
78 | </slot> | 76 | </slot> |
79 | {/if} | 77 | {/if} |
80 | </table> | 78 | </table> |
79 | |||
81 | <style> | 80 | <style> |
82 | table { | 81 | table { |
83 | padding: 16px; | 82 | padding: 16px; |
84 | margin: 8px; | 83 | margin: 8px; |
85 | border: solid 1px black; | 84 | border: solid 1px black; |
86 | border-collapse: collapse; | 85 | border-collapse: collapse; |
87 | } | 86 | } |
88 | 87 | ||
89 | th { | 88 | th { |
90 | text-transform: capitalize; | 89 | text-transform: capitalize; |
91 | } | 90 | } |
92 | 91 | ||
93 | thead tr { | 92 | thead tr { |
94 | background: rgba(0,0,23, 0.34); | 93 | background: rgba(0, 0, 23, 0.34); |
95 | } | 94 | } |
96 | 95 | ||
97 | tbody tr:nth-child(odd) { | 96 | tbody tr:nth-child(odd) { |
98 | background: rgba(0,0,23,0.14); | 97 | background: rgba(0, 0, 23, 0.14); |
99 | } | 98 | } |
100 | 99 | ||
101 | th, td { | 100 | th, |
101 | td { | ||
102 | padding: 1em; | 102 | padding: 1em; |
103 | border: 1px solid rgba(0,0,0, 1); | 103 | border: 1px solid rgba(0, 0, 0, 1); |
104 | } | 104 | } |
105 | </style> | 105 | </style> |
diff --git a/fe/src/lib/errors.ts b/fe/src/lib/errors.ts index 0663d63..d44bec5 100644 --- a/fe/src/lib/errors.ts +++ b/fe/src/lib/errors.ts | |||
@@ -1,7 +1,7 @@ | |||
1 | export class UnauthorizedError extends Error { | 1 | export class UnauthorizedError extends Error { |
2 | constructor (message?: string , options?: ErrorOptions) { | 2 | constructor(message?: string, options?: ErrorOptions) { |
3 | super(message, options); | 3 | super(message, options); |
4 | } | 4 | } |
5 | } | 5 | } |
6 | 6 | ||
7 | 7 | ||
diff --git a/fe/src/lib/utils.ts b/fe/src/lib/utils.ts index c5501ae..22d4e9a 100644 --- a/fe/src/lib/utils.ts +++ b/fe/src/lib/utils.ts | |||
@@ -1,9 +1,9 @@ | |||
1 | export function processFormInput(form) { | 1 | export function processFormInput(form: HTMLFormElement) { |
2 | const formData = new FormData(form); | 2 | const formData = new FormData(form); |
3 | const data = {}; | 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; |
6 | data[key] = value; | 6 | data[key] = value; |
7 | } | 7 | } |
8 | return data; | 8 | return data; |
9 | } | 9 | } |
diff --git a/fe/src/main.ts b/fe/src/main.ts index 8a909a1..ff866d0 100644 --- a/fe/src/main.ts +++ b/fe/src/main.ts | |||
@@ -2,7 +2,7 @@ import './app.css' | |||
2 | import App from './App.svelte' | 2 | import App from './App.svelte' |
3 | 3 | ||
4 | const app = new App({ | 4 | const app = new App({ |
5 | target: document.getElementById('app'), | 5 | target: document.getElementById('app') as HTMLElement, |
6 | }) | 6 | }) |
7 | 7 | ||
8 | export default app | 8 | export default app |
diff --git a/fe/src/stores/auth.ts b/fe/src/stores/auth.ts index 10e6bd3..0efc80b 100644 --- a/fe/src/stores/auth.ts +++ b/fe/src/stores/auth.ts | |||
@@ -5,96 +5,96 @@ import { writable, derived } from 'svelte/store'; | |||
5 | type Nullable<T> = T | null; | 5 | type Nullable<T> = T | null; |
6 | 6 | ||
7 | interface User { | 7 | interface User { |
8 | uuid: string; | 8 | uuid: string; |
9 | username: string; | 9 | username: string; |
10 | } | 10 | } |
11 | 11 | ||
12 | interface TokenStore { | 12 | interface TokenStore { |
13 | subscribe: (run: Subscriber<Nullable<string>>, invalidate: Invalidator<Nullable<string>>) => Unsubscriber, | 13 | subscribe: (run: Subscriber<Nullable<string>>, invalidate?: Invalidator<Nullable<string>>) => Unsubscriber, |
14 | authenticate: (newToken: string) => void, | 14 | authenticate: (newToken: string) => void, |
15 | unauthenticate: () => void | 15 | unauthenticate: () => void |
16 | } | 16 | } |
17 | 17 | ||
18 | 18 | ||
19 | interface UserStore { | 19 | interface UserStore { |
20 | subscribe: (run: Subscriber<Nullable<User>>, invalidate: Invalidator<Nullable<User>>) => Unsubscriber, | 20 | subscribe: (run: Subscriber<Nullable<User>>, invalidate?: Invalidator<Nullable<User>>) => Unsubscriber, |
21 | setUser: (user: User) => void, | 21 | setUser: (user: User) => void, |
22 | reset: () => void | 22 | reset: () => void |
23 | } | 23 | } |
24 | 24 | ||
25 | interface PreferenceStore { | 25 | interface PreferenceStore { |
26 | subscribe: (run: Subscriber<Nullable<Preference>>, invalidate: Invalidator<Nullable<Preference>>) => Unsubscriber, | 26 | subscribe: (run: Subscriber<Preference>, invalidate?: Invalidator<Preference>) => Unsubscriber, |
27 | set: (this: void, value: Nullable<Preference>) => void | 27 | set: (this: void, value: Preference) => void |
28 | } | 28 | } |
29 | 29 | ||
30 | function createTokenStore(): TokenStore { | 30 | function createTokenStore(): TokenStore { |
31 | const storedToken = localStorage.getItem("token"); | 31 | const storedToken = localStorage.getItem("token"); |
32 | const { subscribe, set } = writable<string | null>(storedToken); | 32 | const { subscribe, set } = writable<string | null>(storedToken); |
33 | 33 | ||
34 | function authenticate(newToken: string): void { | 34 | function authenticate(newToken: string): void { |
35 | try { | 35 | try { |
36 | localStorage.setItem("token", newToken); | 36 | localStorage.setItem("token", newToken); |
37 | set(newToken); | 37 | set(newToken); |
38 | } catch (e) { | 38 | } catch (e) { |
39 | console.error('error', e); | 39 | console.error('error', e); |
40 | } | ||
41 | } | 40 | } |
42 | 41 | } | |
43 | function unauthenticate(): void { | 42 | |
44 | localStorage.removeItem("token"); | 43 | function unauthenticate(): void { |
45 | set(null); | 44 | localStorage.removeItem("token"); |
46 | } | 45 | set(null); |
47 | 46 | } | |
48 | return { | 47 | |
49 | subscribe, | 48 | return { |
50 | authenticate, | 49 | subscribe, |
51 | unauthenticate | 50 | authenticate, |
52 | }; | 51 | unauthenticate |
52 | }; | ||
53 | } | 53 | } |
54 | 54 | ||
55 | function onTokenChange ($token: Nullable<string>): boolean { | 55 | function onTokenChange($token: Nullable<string>): boolean { |
56 | return $token ? true : false; | 56 | return $token ? true : false; |
57 | } | 57 | } |
58 | 58 | ||
59 | function createUserStore(): UserStore { | 59 | function createUserStore(): UserStore { |
60 | const user = localStorage.getItem('user'); | 60 | const user = localStorage.getItem('user'); |
61 | const userObj: Nullable<User> = user ? JSON.parse(user) : null; | 61 | const userObj: Nullable<User> = user ? JSON.parse(user) : null; |
62 | const { subscribe, set } = writable<User | null>(userObj); | 62 | const { subscribe, set } = writable<User | null>(userObj); |
63 | 63 | ||
64 | const setUser = (user: User) => { | 64 | const setUser = (user: User) => { |
65 | localStorage.setItem('user', JSON.stringify(user)); | 65 | localStorage.setItem('user', JSON.stringify(user)); |
66 | set(user); | 66 | set(user); |
67 | } | 67 | } |
68 | |||
69 | const reset = () => { | ||
70 | localStorage.removeItem('user'); | ||
71 | set(null); | ||
72 | } | ||
73 | |||
74 | return { | ||
75 | subscribe, | ||
76 | setUser, | ||
77 | reset | ||
78 | } | ||
79 | } | ||
68 | 80 | ||
69 | const reset = () => { | ||
70 | localStorage.removeItem('user'); | ||
71 | set(null); | ||
72 | } | ||
73 | 81 | ||
74 | return { | 82 | function createPreferenceStore(): PreferenceStore { |
75 | subscribe, | 83 | const preferences = localStorage.getItem('preferences'); |
76 | setUser, | 84 | const preferenceObj: Preference = preferences ? JSON.parse(preferences) : { |
77 | reset | 85 | color: "#FF0000", |
86 | size: { | ||
87 | size: 16, | ||
88 | unit: 'oz' | ||
78 | } | 89 | } |
79 | } | 90 | }; |
80 | 91 | ||
92 | const { subscribe, set } = writable<Preference>(preferenceObj); | ||
81 | 93 | ||
82 | function createPreferenceStore(): PreferenceStore { | 94 | return { |
83 | const preferences = localStorage.getItem('preferences'); | 95 | subscribe, |
84 | const preferenceObj: Preference = preferences ? JSON.parse(preferences) : { | 96 | set |
85 | color: "#FF0000", | 97 | } |
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 | } | 98 | } |
99 | 99 | ||
100 | export const token = createTokenStore(); | 100 | export const token = createTokenStore(); |
diff --git a/fe/src/types.ts b/fe/src/types.ts index 03d613d..526e7eb 100644 --- a/fe/src/types.ts +++ b/fe/src/types.ts | |||
@@ -1,14 +1,20 @@ | |||
1 | export interface Size { | 1 | export interface Size { |
2 | size: number; | 2 | size: number; |
3 | unit: string; | 3 | unit: string; |
4 | } | 4 | } |
5 | 5 | ||
6 | export interface Preference { | 6 | export interface Preference { |
7 | color: string; | 7 | color: string; |
8 | size: Size; | 8 | size: Size; |
9 | } | 9 | } |
10 | 10 | ||
11 | export interface User { | 11 | export interface User { |
12 | name: string; | 12 | name: string; |
13 | uuid: string; | 13 | uuid: string; |
14 | } | 14 | } |
15 | |||
16 | export interface Statistic { | ||
17 | user_id: string; | ||
18 | date: string; | ||
19 | quantity: number; | ||
20 | } \ No newline at end of file | ||