diff options
author | Doog <157747121+doogongithub@users.noreply.github.com> | 2024-02-24 20:08:35 -0500 |
---|---|---|
committer | Doog <157747121+doogongithub@users.noreply.github.com> | 2024-02-24 20:08:35 -0500 |
commit | e37c73e33a4aaf7fb8d25b5af03627f20bcda19f (patch) | |
tree | 277a534e826325e25f881e61e322b4e2e7ec94f9 /fe | |
parent | 3eafb413a48cde60dea8a7355ee621c6acca952f (diff) |
add gitignore
Diffstat (limited to 'fe')
-rw-r--r-- | fe/src/App.svelte | 147 | ||||
-rw-r--r-- | fe/src/app.css | 2 | ||||
-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 | ||||
-rw-r--r-- | fe/src/stores/auth.ts | 48 | ||||
-rw-r--r-- | fe/svelte.config.js | 1 |
8 files changed, 301 insertions, 146 deletions
diff --git a/fe/src/App.svelte b/fe/src/App.svelte index cc4e594..8811c52 100644 --- a/fe/src/App.svelte +++ b/fe/src/App.svelte | |||
@@ -1,146 +1,19 @@ | |||
1 | <script lang="ts"> | 1 | <script lang="ts"> |
2 | import {onMount} from 'svelte'; | 2 | import { onMount, onDestroy } from 'svelte'; |
3 | import svelteLogo from './assets/svelte.svg' | 3 | import Layout from './lib/Layout.svelte' |
4 | import viteLogo from '/vite.svg' | 4 | import LoginForm from './lib/LoginForm.svelte'; |
5 | import Counter from './lib/Counter.svelte' | 5 | import DataView from './lib/DataView.svelte'; |
6 | import Table from './lib/Table.svelte' | 6 | import { authenticated } from './stores/auth'; |
7 | import Card from './lib/Card.svelte' | ||
8 | import { UnauthorizedError } from './lib/errors'; | ||
9 | |||
10 | let data; | ||
11 | let error; | ||
12 | |||
13 | let user = { | ||
14 | username: '', | ||
15 | password: '' | ||
16 | } | ||
17 | |||
18 | interface CredentialObject { | ||
19 | username: string; | ||
20 | password: string; | ||
21 | } | ||
22 | |||
23 | function sleep(ms) { | ||
24 | return new Promise(resolve => setTimeout(resolve, ms)); | ||
25 | } | ||
26 | |||
27 | async function getData() { | ||
28 | const res = await fetch('http://localhost:8080/api/v1/stats/'); | ||
29 | if (res.ok) { | ||
30 | await sleep(3000); | ||
31 | return await res.json(); | ||
32 | } else { | ||
33 | throw new Error('There was a problem with your request'); | ||
34 | } | ||
35 | } | ||
36 | |||
37 | function handleClick () { | ||
38 | data = getData(); | ||
39 | } | ||
40 | |||
41 | let authenticated: boolean = false; | ||
42 | |||
43 | function prepareCredentials ({ username, password }: CredentialObject): string { | ||
44 | return btoa(`${username}:${password}`); | ||
45 | } | ||
46 | |||
47 | |||
48 | async function onSubmit(e) { | ||
49 | if (!user.username || !user.password) { | ||
50 | error = 'please enter your username and password'; | ||
51 | return; | ||
52 | } | ||
53 | const auth = prepareCredentials(user); | ||
54 | |||
55 | const response = await fetch('http://localhost:8080/api/v1/auth', { | ||
56 | method: 'POST', | ||
57 | headers: { | ||
58 | 'Authorization': `Basic ${auth}`, | ||
59 | }, | ||
60 | }); | ||
61 | |||
62 | if (response.status === 401) { | ||
63 | error = "Your username or password is wrong"; | ||
64 | return; | ||
65 | } | ||
66 | |||
67 | if (response.ok) { | ||
68 | const { token } = await response.json(); | ||
69 | console.log(token); | ||
70 | localStorage.user = JSON.stringify(user); | ||
71 | localStorage.token = token; | ||
72 | authenticated = true; | ||
73 | } | ||
74 | |||
75 | |||
76 | error = null; | ||
77 | } | ||
78 | |||
79 | function logout() { | ||
80 | localStorage.removeItem("user"); | ||
81 | localStorage.removeItem("token"); | ||
82 | authenticated = false; | ||
83 | } | ||
84 | |||
85 | |||
86 | onMount(() => { | ||
87 | if (localStorage.token) { | ||
88 | authenticated = true; | ||
89 | } | ||
90 | }); | ||
91 | </script> | 7 | </script> |
92 | 8 | ||
93 | <main> | 9 | <main> |
94 | {#if !authenticated} | 10 | <Layout> |
95 | <Card> | 11 | {#if !$authenticated} |
96 | <form class="form" on:submit|preventDefault={onSubmit}> | 12 | <LoginForm /> |
97 | <div class='form input group'> | ||
98 | <label for="username">Username</label> | ||
99 | <input bind:value={user.username} id="username" name='username' type="text" /> | ||
100 | </div> | ||
101 | <div class='form input group'> | ||
102 | <label for="password">Password</label> | ||
103 | <input bind:value={user.password} id="password" name='password' type="password" /> | ||
104 | </div> | ||
105 | {#if error} | ||
106 | <p class="error">{error}</p> | ||
107 | {/if} | ||
108 | <button type="submit">Log in</button> | ||
109 | </form> | ||
110 | </Card> | ||
111 | {:else} | 13 | {:else} |
112 | <div> | 14 | <DataView /> |
113 | <button on:click={logout}>Logout</button> | ||
114 | </div> | ||
115 | <div> | ||
116 | <a href="https://vitejs.dev" target="_blank" rel="noreferrer"> | ||
117 | <img src={viteLogo} class="logo" alt="Vite Logo" /> | ||
118 | </a> | ||
119 | <a href="https://svelte.dev" target="_blank" rel="noreferrer"> | ||
120 | <img src={svelteLogo} class="logo svelte" alt="Svelte Logo" /> | ||
121 | </a> | ||
122 | </div> | ||
123 | |||
124 | <button on:click={handleClick}> | ||
125 | Get Data | ||
126 | </button> | ||
127 | |||
128 | {#await data} | ||
129 | <p>...fetching</p> | ||
130 | {:then data} | ||
131 | {#if data} | ||
132 | <p>Status</p> | ||
133 | <p>{data.status}</p> | ||
134 | <Table /> | ||
135 | <Table nofooter title="No Footer"/> | ||
136 | <Table noheader title="No Header"/> | ||
137 | {:else} | ||
138 | <p>No data yet</p> | ||
139 | {/if} | ||
140 | {:catch errror} | ||
141 | <p>{error.message}</p> | ||
142 | {/await} | ||
143 | {/if} | 15 | {/if} |
16 | </Layout> | ||
144 | </main> | 17 | </main> |
145 | 18 | ||
146 | <style> | 19 | <style> |
diff --git a/fe/src/app.css b/fe/src/app.css index 4768cf6..0d5fa90 100644 --- a/fe/src/app.css +++ b/fe/src/app.css | |||
@@ -42,9 +42,9 @@ h1 { | |||
42 | } | 42 | } |
43 | 43 | ||
44 | #app { | 44 | #app { |
45 | flex-grow: 2; | ||
45 | max-width: 1280px; | 46 | max-width: 1280px; |
46 | margin: 0 auto; | 47 | margin: 0 auto; |
47 | padding: 2rem; | ||
48 | } | 48 | } |
49 | 49 | ||
50 | button { | 50 | button { |
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> |
diff --git a/fe/src/stores/auth.ts b/fe/src/stores/auth.ts new file mode 100644 index 0000000..7e70cda --- /dev/null +++ b/fe/src/stores/auth.ts | |||
@@ -0,0 +1,48 @@ | |||
1 | import type { Invalidator, Subscriber, Unsubscriber } from 'svelte/store'; | ||
2 | import { writable, derived } from 'svelte/store'; | ||
3 | |||
4 | type Nullable<T> = T | null; | ||
5 | |||
6 | interface User { | ||
7 | uuid: string; | ||
8 | username: string; | ||
9 | } | ||
10 | |||
11 | interface TokenStore { | ||
12 | subscribe: (run: Subscriber<Nullable<string>>, invalidate: Invalidator<Nullable<string>>) => Unsubscriber, | ||
13 | authenticate: (newToken: string) => void, | ||
14 | unauthenticate: () => void | ||
15 | } | ||
16 | |||
17 | function createTokenStore(): TokenStore { | ||
18 | const storedToken = localStorage.getItem("token"); | ||
19 | const { subscribe, set } = writable<string | null>(storedToken); | ||
20 | |||
21 | function authenticate(newToken: string): void { | ||
22 | try { | ||
23 | localStorage.setItem("token", newToken); | ||
24 | set(newToken); | ||
25 | } catch (e) { | ||
26 | console.error('error', e); | ||
27 | } | ||
28 | } | ||
29 | |||
30 | function unauthenticate(): void { | ||
31 | localStorage.removeItem("token"); | ||
32 | set(null); | ||
33 | } | ||
34 | |||
35 | return { | ||
36 | subscribe, | ||
37 | authenticate, | ||
38 | unauthenticate | ||
39 | }; | ||
40 | } | ||
41 | |||
42 | function onTokenChange ($token: Nullable<string>): boolean { | ||
43 | return $token ? true : false; | ||
44 | } | ||
45 | |||
46 | export const token = createTokenStore(); | ||
47 | export const authenticated = derived(token, onTokenChange); | ||
48 | export const user = writable<User | null>(null); | ||
diff --git a/fe/svelte.config.js b/fe/svelte.config.js index b0683fd..b29bf40 100644 --- a/fe/svelte.config.js +++ b/fe/svelte.config.js | |||
@@ -4,4 +4,5 @@ export default { | |||
4 | // Consult https://svelte.dev/docs#compile-time-svelte-preprocess | 4 | // Consult https://svelte.dev/docs#compile-time-svelte-preprocess |
5 | // for more information about preprocessors | 5 | // for more information about preprocessors |
6 | preprocess: vitePreprocess(), | 6 | preprocess: vitePreprocess(), |
7 | dev: true | ||
7 | } | 8 | } |