aboutsummaryrefslogtreecommitdiff
path: root/fe
diff options
context:
space:
mode:
authorDoog <157747121+doogongithub@users.noreply.github.com>2024-02-24 20:08:35 -0500
committerDoog <157747121+doogongithub@users.noreply.github.com>2024-02-24 20:08:35 -0500
commite37c73e33a4aaf7fb8d25b5af03627f20bcda19f (patch)
tree277a534e826325e25f881e61e322b4e2e7ec94f9 /fe
parent3eafb413a48cde60dea8a7355ee621c6acca952f (diff)
add gitignore
Diffstat (limited to 'fe')
-rw-r--r--fe/src/App.svelte147
-rw-r--r--fe/src/app.css2
-rw-r--r--fe/src/lib/DataView.svelte67
-rw-r--r--fe/src/lib/Layout.svelte57
-rw-r--r--fe/src/lib/LoginForm.svelte64
-rw-r--r--fe/src/lib/Table.svelte61
-rw-r--r--fe/src/stores/auth.ts48
-rw-r--r--fe/svelte.config.js1
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
50button { 50button {
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'>
2import { onMount } from 'svelte';
3import { token } from '../stores/auth'
4import Table from './Table.svelte';
5
6let json;
7let showAddForm: boolean = false;
8
9async 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
23async 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
38function handleClick() {
39 showAddForm = true;
40}
41
42function handleAddDialogSubmit (e) {
43 console.log(e.keyCode)
44}
45
46onMount(() => {
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>
2import { authenticated, token } from '../stores/auth';
3
4const logout = () => token.unauthenticate();
5
6function 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}
33nav {
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
42nav div {
43 width: fit-content;
44}
45
46nav 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'>
2import { token } from '../stores/auth';
3import Card from './Card.svelte';
4
5let user = {
6 username: '',
7 password: ''
8}
9
10let error;
11
12interface CredentialObject {
13 username: string;
14 password: string;
15}
16
17function prepareCredentials ({ username, password }: CredentialObject): string {
18 return btoa(`${username}:${password}`);
19}
20
21async 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; 2import {afterUpdate} from 'svelte';
3 export let nofooter: boolean = false; 3export let data: Array<any> | undefined = undefined;
4 export let noheader: boolean = false; 4export let nofooter: boolean = false;
5 export let title: string; 5export let noheader: boolean = false;
6export let omit: string[] = ['id'];
7export let title: string | undefined = undefined;
8
9function 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
14function getRow(row: Record<string, any>): Array<any> {
15 return Object.entries(row).filter(r => !omit.includes(r[0]));
16}
17
18const 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
28function 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
83th {
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 @@
1import type { Invalidator, Subscriber, Unsubscriber } from 'svelte/store';
2import { writable, derived } from 'svelte/store';
3
4type Nullable<T> = T | null;
5
6interface User {
7 uuid: string;
8 username: string;
9}
10
11interface TokenStore {
12 subscribe: (run: Subscriber<Nullable<string>>, invalidate: Invalidator<Nullable<string>>) => Unsubscriber,
13 authenticate: (newToken: string) => void,
14 unauthenticate: () => void
15}
16
17function 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
42function onTokenChange ($token: Nullable<string>): boolean {
43 return $token ? true : false;
44}
45
46export const token = createTokenStore();
47export const authenticated = derived(token, onTokenChange);
48export 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}