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 | ||
