diff options
Diffstat (limited to 'fe/src/lib')
| -rw-r--r-- | fe/src/lib/DataView.svelte | 67 | ||||
| -rw-r--r-- | fe/src/lib/Layout.svelte | 57 | ||||
| -rw-r--r-- | fe/src/lib/LoginForm.svelte | 64 | ||||
| -rw-r--r-- | fe/src/lib/Table.svelte | 61 |
4 files changed, 241 insertions, 8 deletions
diff --git a/fe/src/lib/DataView.svelte b/fe/src/lib/DataView.svelte new file mode 100644 index 0000000..cd7b042 --- /dev/null +++ b/fe/src/lib/DataView.svelte | |||
| @@ -0,0 +1,67 @@ | |||
| 1 | <script lang='ts'> | ||
| 2 | import { onMount } from 'svelte'; | ||
| 3 | import { token } from '../stores/auth' | ||
| 4 | import Table from './Table.svelte'; | ||
| 5 | |||
| 6 | let json; | ||
| 7 | let showAddForm: boolean = false; | ||
| 8 | |||
| 9 | async function fetchData() { | ||
| 10 | const res = await fetch('http://localhost:8080/api/v1/stats/', { | ||
| 11 | method: "GET", | ||
| 12 | headers: { | ||
| 13 | 'Authorization': `Bearer ${$token}` | ||
| 14 | } | ||
| 15 | }); | ||
| 16 | if (res.ok) { | ||
| 17 | json = res.json(); | ||
| 18 | } else { | ||
| 19 | throw new Error('There was a problem with your request'); | ||
| 20 | } | ||
| 21 | } | ||
| 22 | |||
| 23 | async function submitStat() { | ||
| 24 | const response = await fetch('http://localhost:8080/api/v1/stats/', { | ||
| 25 | method: "POST", | ||
| 26 | headers: { | ||
| 27 | 'Authorization': `Bearer ${$token}` | ||
| 28 | }, | ||
| 29 | body: JSON.stringify({ | ||
| 30 | date: new Date, | ||
| 31 | user_id: 1, | ||
| 32 | quantity: 3 | ||
| 33 | }) | ||
| 34 | }); | ||
| 35 | fetchData(); | ||
| 36 | } | ||
| 37 | |||
| 38 | function handleClick() { | ||
| 39 | showAddForm = true; | ||
| 40 | } | ||
| 41 | |||
| 42 | function handleAddDialogSubmit (e) { | ||
| 43 | console.log(e.keyCode) | ||
| 44 | } | ||
| 45 | |||
| 46 | onMount(() => { | ||
| 47 | fetchData(); | ||
| 48 | }); | ||
| 49 | |||
| 50 | </script> | ||
| 51 | <div> | ||
| 52 | <button on:click={submitStat}>Add Stat Test</button> | ||
| 53 | <dialog open={showAddForm} on:submit={handleAddDialogSubmit}> | ||
| 54 | <form method="dialog"> | ||
| 55 | <input name="date" type="date" /> | ||
| 56 | <input name="quantity" type="number" min="0" autocomplete="off"/> | ||
| 57 | <button type="submit">Submit</button> | ||
| 58 | </form> | ||
| 59 | </dialog> | ||
| 60 | <button on:click={handleClick}>Add</button> | ||
| 61 | {#await json then data} | ||
| 62 | <Table {data} nofooter /> | ||
| 63 | {:catch error} | ||
| 64 | <p>{error}</p> | ||
| 65 | {/await} | ||
| 66 | <!-- <Chart /> --> | ||
| 67 | </div> | ||
diff --git a/fe/src/lib/Layout.svelte b/fe/src/lib/Layout.svelte new file mode 100644 index 0000000..f349632 --- /dev/null +++ b/fe/src/lib/Layout.svelte | |||
| @@ -0,0 +1,57 @@ | |||
| 1 | <script> | ||
| 2 | import { authenticated, token } from '../stores/auth'; | ||
| 3 | |||
| 4 | const logout = () => token.unauthenticate(); | ||
| 5 | |||
| 6 | function showSettingsDialog() { | ||
| 7 | console.log('show settings'); | ||
| 8 | } | ||
| 9 | |||
| 10 | </script> | ||
| 11 | |||
| 12 | <div class="layout"> | ||
| 13 | {#if $authenticated} | ||
| 14 | <nav> | ||
| 15 | <div> | ||
| 16 | <h1>Water</h1> | ||
| 17 | </div> | ||
| 18 | <div> | ||
| 19 | <button on:click={showSettingsDialog}>Settings</button> | ||
| 20 | <button on:click={logout}>Logout</button> | ||
| 21 | </div> | ||
| 22 | </nav> | ||
| 23 | {/if} | ||
| 24 | <div id="content"> | ||
| 25 | <slot /> | ||
| 26 | </div> | ||
| 27 | </div> | ||
| 28 | |||
| 29 | <style> | ||
| 30 | .layout { | ||
| 31 | height: 100vh; | ||
| 32 | } | ||
| 33 | nav { | ||
| 34 | display: flex; | ||
| 35 | flex-direction: row; | ||
| 36 | align-items: center; | ||
| 37 | justify-content: space-between; | ||
| 38 | height: 64px; | ||
| 39 | padding: 0 2em; | ||
| 40 | } | ||
| 41 | |||
| 42 | nav div { | ||
| 43 | width: fit-content; | ||
| 44 | } | ||
| 45 | |||
| 46 | nav div h1 { | ||
| 47 | font-size: 1.75em; | ||
| 48 | } | ||
| 49 | |||
| 50 | #content { | ||
| 51 | display: flex; | ||
| 52 | flex-direction: column; | ||
| 53 | justify-content: center; | ||
| 54 | align-items: center; | ||
| 55 | padding: 3em 0; | ||
| 56 | } | ||
| 57 | </style> | ||
diff --git a/fe/src/lib/LoginForm.svelte b/fe/src/lib/LoginForm.svelte new file mode 100644 index 0000000..22c0faf --- /dev/null +++ b/fe/src/lib/LoginForm.svelte | |||
| @@ -0,0 +1,64 @@ | |||
| 1 | <script lang='ts'> | ||
| 2 | import { token } from '../stores/auth'; | ||
| 3 | import Card from './Card.svelte'; | ||
| 4 | |||
| 5 | let user = { | ||
| 6 | username: '', | ||
| 7 | password: '' | ||
| 8 | } | ||
| 9 | |||
| 10 | let error; | ||
| 11 | |||
| 12 | interface CredentialObject { | ||
| 13 | username: string; | ||
| 14 | password: string; | ||
| 15 | } | ||
| 16 | |||
| 17 | function prepareCredentials ({ username, password }: CredentialObject): string { | ||
| 18 | return btoa(`${username}:${password}`); | ||
| 19 | } | ||
| 20 | |||
| 21 | async function onSubmit (e) { | ||
| 22 | if (!user.username || !user.password) { | ||
| 23 | error = 'please enter your username and password'; | ||
| 24 | return; | ||
| 25 | } | ||
| 26 | const auth = prepareCredentials(user); | ||
| 27 | |||
| 28 | const response = await fetch('http://localhost:8080/api/v1/auth', { | ||
| 29 | method: 'POST', | ||
| 30 | headers: { | ||
| 31 | 'Authorization': `Basic ${auth}`, | ||
| 32 | }, | ||
| 33 | }); | ||
| 34 | |||
| 35 | if (response.status === 401) { | ||
| 36 | error = "Your username or password is wrong"; | ||
| 37 | return; | ||
| 38 | } | ||
| 39 | |||
| 40 | if (response.ok) { | ||
| 41 | const { token: apiToken } = await response.json(); | ||
| 42 | token.authenticate(apiToken); | ||
| 43 | } | ||
| 44 | |||
| 45 | error = null; | ||
| 46 | } | ||
| 47 | </script> | ||
| 48 | |||
| 49 | <Card> | ||
| 50 | <form class="form" on:submit|preventDefault={onSubmit}> | ||
| 51 | <div class='form input group'> | ||
| 52 | <label for="username">Username</label> | ||
| 53 | <input bind:value={user.username} id="username" name='username' type="text" autocomplete="username" /> | ||
| 54 | </div> | ||
| 55 | <div class='form input group'> | ||
| 56 | <label for="password">Password</label> | ||
| 57 | <input bind:value={user.password} id="password" name='password' type="password" autocomplete="current-password"/> | ||
| 58 | </div> | ||
| 59 | {#if error} | ||
| 60 | <p class="error">{error}</p> | ||
| 61 | {/if} | ||
| 62 | <button type="submit">Log in</button> | ||
| 63 | </form> | ||
| 64 | </Card> | ||
diff --git a/fe/src/lib/Table.svelte b/fe/src/lib/Table.svelte index 2df9f8c..5572280 100644 --- a/fe/src/lib/Table.svelte +++ b/fe/src/lib/Table.svelte | |||
| @@ -1,8 +1,38 @@ | |||
| 1 | <script lang="ts"> | 1 | <script lang="ts"> |
| 2 | export let data; | 2 | import {afterUpdate} from 'svelte'; |
| 3 | export let nofooter: boolean = false; | 3 | export let data: Array<any> | undefined = undefined; |
| 4 | export let noheader: boolean = false; | 4 | export let nofooter: boolean = false; |
| 5 | export let title: string; | 5 | export let noheader: boolean = false; |
| 6 | export let omit: string[] = ['id']; | ||
| 7 | export let title: string | undefined = undefined; | ||
| 8 | |||
| 9 | function getDataKeys(data: any[]): string[] { | ||
| 10 | if (!data || data.length === 0) return []; | ||
| 11 | return Object.keys(data[0]).map(k => k.split('_').join(' ')).filter(k => !omit.includes(k)); | ||
| 12 | } | ||
| 13 | |||
| 14 | function getRow(row: Record<string, any>): Array<any> { | ||
| 15 | return Object.entries(row).filter(r => !omit.includes(r[0])); | ||
| 16 | } | ||
| 17 | |||
| 18 | const formatter = new Intl.DateTimeFormat('en', { | ||
| 19 | year: 'numeric', | ||
| 20 | month: 'numeric', | ||
| 21 | day: 'numeric', | ||
| 22 | hour: 'numeric', | ||
| 23 | minute: '2-digit', | ||
| 24 | second: '2-digit', | ||
| 25 | timeZone: "America/New_York" | ||
| 26 | }); | ||
| 27 | |||
| 28 | function formatDatum([key, value]: any[]) { | ||
| 29 | if (key === 'date') { | ||
| 30 | const parsedDate = new Date(value); | ||
| 31 | return formatter.format(parsedDate); | ||
| 32 | } | ||
| 33 | return value; | ||
| 34 | } | ||
| 35 | |||
| 6 | </script> | 36 | </script> |
| 7 | <table> | 37 | <table> |
| 8 | {#if title} | 38 | {#if title} |
| @@ -11,16 +41,27 @@ | |||
| 11 | {#if !noheader} | 41 | {#if !noheader} |
| 12 | <thead> | 42 | <thead> |
| 13 | <tr> | 43 | <tr> |
| 14 | <th> | 44 | {#each getDataKeys(data) as header} |
| 15 | Data Header | 45 | <th>{header}</th> |
| 16 | </th> | 46 | {/each} |
| 17 | </tr> | 47 | </tr> |
| 18 | </thead> | 48 | </thead> |
| 19 | {/if} | 49 | {/if} |
| 20 | <tbody> | 50 | <tbody> |
| 51 | {#if data} | ||
| 52 | {#each data as row} | ||
| 21 | <tr> | 53 | <tr> |
| 22 | <td>Data</td> | 54 | {#each getRow(row) as datum} |
| 55 | |||
| 56 | <td>{formatDatum(datum)}</td> | ||
| 57 | {/each} | ||
| 23 | </tr> | 58 | </tr> |
| 59 | {/each} | ||
| 60 | {:else} | ||
| 61 | <tr> | ||
| 62 | There is not data. | ||
| 63 | </tr> | ||
| 64 | {/if} | ||
| 24 | </tbody> | 65 | </tbody> |
| 25 | {#if !nofooter} | 66 | {#if !nofooter} |
| 26 | <slot name="footer"> | 67 | <slot name="footer"> |
| @@ -38,4 +79,8 @@ table { | |||
| 38 | margin: 8px; | 79 | margin: 8px; |
| 39 | border: solid 1px black; | 80 | border: solid 1px black; |
| 40 | } | 81 | } |
| 82 | |||
| 83 | th { | ||
| 84 | text-transform: capitalize; | ||
| 85 | } | ||
| 41 | </style> | 86 | </style> |
