diff options
| author | Zach Berwaldt <zberwaldt@tutamail.com> | 2024-03-15 18:49:43 -0400 |
|---|---|---|
| committer | Zach Berwaldt <zberwaldt@tutamail.com> | 2024-03-15 18:49:43 -0400 |
| commit | 9cae9c1d2a0b4f7fa72f3075541b9ffafe1a7275 (patch) | |
| tree | 960fa4f96a1328861a06d97180da8601af6855da /fe | |
| parent | 8fab2d03bce82e4dee798ebffb1e93c557f62a4b (diff) | |
Add routes for preference, clean up and add types
Diffstat (limited to 'fe')
| -rw-r--r-- | fe/src/app.css | 4 | ||||
| -rw-r--r-- | fe/src/http.ts | 96 | ||||
| -rw-r--r-- | fe/src/lib/Card.svelte | 1 | ||||
| -rw-r--r-- | fe/src/lib/Chart.svelte | 63 | ||||
| -rw-r--r-- | fe/src/lib/DataView.svelte | 40 | ||||
| -rw-r--r-- | fe/src/lib/Layout.svelte | 8 | ||||
| -rw-r--r-- | fe/src/lib/LoginForm.svelte | 2 | ||||
| -rw-r--r-- | fe/src/lib/PreferencesForm.svelte | 145 | ||||
| -rw-r--r-- | fe/src/lib/errors.ts | 6 | ||||
| -rw-r--r-- | fe/src/lib/utils.ts | 2 | ||||
| -rw-r--r-- | fe/src/stores/auth.ts | 153 | ||||
| -rw-r--r-- | fe/src/types.ts | 45 |
12 files changed, 370 insertions, 195 deletions
diff --git a/fe/src/app.css b/fe/src/app.css index de19b52..c24c713 100644 --- a/fe/src/app.css +++ b/fe/src/app.css | |||
| @@ -109,6 +109,10 @@ button:focus-visible { | |||
| 109 | padding: 1em; | 109 | padding: 1em; |
| 110 | } | 110 | } |
| 111 | 111 | ||
| 112 | .form.input.group input[type=color] { | ||
| 113 | padding: 0; | ||
| 114 | } | ||
| 115 | |||
| 112 | .form button[type=submit] { | 116 | .form button[type=submit] { |
| 113 | align-self: flex-end; | 117 | align-self: flex-end; |
| 114 | background: var(--submit); | 118 | background: var(--submit); |
diff --git a/fe/src/http.ts b/fe/src/http.ts index cc5a906..3b2a4f0 100644 --- a/fe/src/http.ts +++ b/fe/src/http.ts | |||
| @@ -1,60 +1,92 @@ | |||
| 1 | export default class HttpClient { | 1 | let instance; |
| 2 | private static instance: HttpClient; | 2 | const baseUrl = import.meta.env?.VITE_API_BASE_URL ?? "http://localhost:8080/api/v1"; |
| 3 | |||
| 4 | class HttpClient { | ||
| 3 | baseURL: string; | 5 | baseURL: string; |
| 4 | 6 | commonHeaders: Headers; | |
| 5 | private constructor(baseURL: string) { | 7 | |
| 8 | constructor(baseURL: string) { | ||
| 6 | this.baseURL = baseURL; | 9 | this.baseURL = baseURL; |
| 10 | this.commonHeaders = new Headers({ | ||
| 11 | "Content-Type": "application/json" | ||
| 12 | }) | ||
| 13 | if (instance) { | ||
| 14 | throw new Error("New instance cannot be created!"); | ||
| 15 | } | ||
| 16 | |||
| 17 | instance = this; | ||
| 7 | } | 18 | } |
| 8 | 19 | ||
| 9 | private getURL(endpoint: string): URL { | 20 | private getURL(endpoint: string): URL { |
| 10 | return new URL(endpoint, this.baseURL) | 21 | return new URL(endpoint, this.baseURL); |
| 11 | } | 22 | } |
| 12 | 23 | ||
| 13 | public static getInstance(): HttpClient { | 24 | private token(): string | null { |
| 14 | if (!HttpClient.instance) { | 25 | return localStorage.getItem('token'); |
| 15 | const baseUrl = import.meta.env?.VITE_API_BASE_URL ?? 'http://localhost:8080/api/v1'; | 26 | } |
| 16 | HttpClient.instance = new HttpClient(baseUrl); | ||
| 17 | } | ||
| 18 | 27 | ||
| 19 | return HttpClient.instance; | 28 | private async makeRequest(request: Request): Promise<Response> { |
| 29 | return fetch(request) | ||
| 20 | } | 30 | } |
| 21 | 31 | ||
| 22 | async get({ endpoint }: IHttpParameters): Promise<Response> { | 32 | async get({ endpoint, headers }: IHttpParameters): Promise<Response> { |
| 23 | const url = this.getURL(endpoint); | 33 | const url: URL = this.getURL(endpoint); |
| 24 | const response = await fetch(url, { | 34 | headers = Object.assign<Headers, Headers>(headers, this.commonHeaders); |
| 25 | method: 'GET', | 35 | const request: Request = new Request(url, { |
| 26 | headers: headers, | 36 | method: "GET", |
| 37 | headers | ||
| 27 | }); | 38 | }); |
| 28 | return response.json(); | 39 | |
| 40 | return this.makeRequest(request); | ||
| 29 | } | 41 | } |
| 30 | 42 | ||
| 31 | async post({ endpoint }: IHttpParameters): Promise<Response> { | 43 | async post({ endpoint, authenticated, body, headers }: IHttpParameters): Promise<Response> { |
| 32 | const url = this.getURL(endpoint); | 44 | const url = this.getURL(endpoint); |
| 33 | const response = await fetch(url, { | 45 | |
| 34 | method: 'POST', | 46 | if (authenticated) { |
| 47 | const token: string | null = this.token(); | ||
| 48 | headers.append('Authorization', `Bearer ${token}`); | ||
| 49 | } | ||
| 50 | |||
| 51 | const request: Request = new Request(url, { | ||
| 52 | method: "POST", | ||
| 35 | body: JSON.stringify(body), | 53 | body: JSON.stringify(body), |
| 36 | headers: headers, | 54 | headers |
| 37 | }); | 55 | }) |
| 38 | return response.json(); | 56 | |
| 57 | return this.makeRequest(request); | ||
| 39 | } | 58 | } |
| 40 | 59 | ||
| 41 | async patch({ endpoint, authenticated, headers }: IHttpParameters): Promise<Response> { | 60 | async patch({ endpoint, authenticated, headers }: IHttpParameters): Promise<Response> { |
| 42 | const url = this.getURL(endpoint); | 61 | const url = this.getURL(endpoint); |
| 43 | if (authenticated) { | 62 | if (authenticated) { |
| 44 | 63 | ||
| 45 | } | 64 | } |
| 46 | const response: Response = await fetch(url) | 65 | const response: Response = await fetch(url, { |
| 66 | method: "PATCH", | ||
| 67 | headers | ||
| 68 | }); | ||
| 47 | } | 69 | } |
| 48 | 70 | ||
| 49 | async delete({ endpoint, authenticated }: IHttpParameters): Promise<Response> { | 71 | async delete({ endpoint, authenticated, headers }: IHttpParameters): Promise<Response> { |
| 50 | const url = this.getURL(endpoint); | 72 | const url = this.getURL(endpoint); |
| 51 | if (authenticated) { } | 73 | if (authenticated) { |
| 52 | const response = await fetch() | 74 | |
| 75 | } | ||
| 76 | const response: Response = await fetch(url, { | ||
| 77 | method: "DELETE", | ||
| 78 | headers | ||
| 79 | }) | ||
| 53 | } | 80 | } |
| 54 | } | 81 | } |
| 55 | 82 | ||
| 56 | interface IHttpParameters { | 83 | interface IHttpParameters { |
| 57 | endpoint: string; | 84 | endpoint: string; |
| 85 | body: Record<string, any>; | ||
| 58 | authenticated: boolean; | 86 | authenticated: boolean; |
| 59 | headers: Headers | 87 | headers: Headers; |
| 60 | } | 88 | } |
| 89 | |||
| 90 | let http: Readonly<HttpClient> = Object.freeze(new HttpClient(baseUrl)); | ||
| 91 | |||
| 92 | export default http; | ||
diff --git a/fe/src/lib/Card.svelte b/fe/src/lib/Card.svelte index d7cd900..cd1e02c 100644 --- a/fe/src/lib/Card.svelte +++ b/fe/src/lib/Card.svelte | |||
| @@ -1,6 +1,5 @@ | |||
| 1 | <script lang="ts"> | 1 | <script lang="ts"> |
| 2 | export let title = ""; | 2 | export let title = ""; |
| 3 | export let height: number | undefined = undefined; | ||
| 4 | </script> | 3 | </script> |
| 5 | 4 | ||
| 6 | <div class="card"> | 5 | <div class="card"> |
diff --git a/fe/src/lib/Chart.svelte b/fe/src/lib/Chart.svelte new file mode 100644 index 0000000..b19d932 --- /dev/null +++ b/fe/src/lib/Chart.svelte | |||
| @@ -0,0 +1,63 @@ | |||
| 1 | <script lang="ts"> | ||
| 2 | import { onDestroy } from "svelte"; | ||
| 3 | import ChartJS from "chart.js/auto"; | ||
| 4 | |||
| 5 | export let data; | ||
| 6 | export let labels; | ||
| 7 | export let type = 'bar'; | ||
| 8 | |||
| 9 | let ref: HTMLCanvasElement; | ||
| 10 | let chart | ||
| 11 | |||
| 12 | function setupChart(result) { | ||
| 13 | [labels, data] = result; | ||
| 14 | chart = new ChartJS(ref, { | ||
| 15 | type, | ||
| 16 | data: { | ||
| 17 | labels, | ||
| 18 | datasets: [ | ||
| 19 | { | ||
| 20 | label: "Totals", | ||
| 21 | data, | ||
| 22 | backgroundColor: "rgba(255, 192, 192, 0.2)" | ||
| 23 | } | ||
| 24 | ] | ||
| 25 | }, | ||
| 26 | options: { | ||
| 27 | responsive: true, | ||
| 28 | maintainAspectRatio: false, | ||
| 29 | scales: { | ||
| 30 | y: { | ||
| 31 | suggestedMax: 30, | ||
| 32 | beginAtZero: true, | ||
| 33 | ticks: { | ||
| 34 | autoSkip: true, | ||
| 35 | stepSize: 5 | ||
| 36 | } | ||
| 37 | } | ||
| 38 | }, | ||
| 39 | plugins: { | ||
| 40 | legend: { | ||
| 41 | display: false | ||
| 42 | }, | ||
| 43 | title: { | ||
| 44 | display: true, | ||
| 45 | text: "Weekly Breakdown" | ||
| 46 | }, | ||
| 47 | subtitle: { | ||
| 48 | display: true, | ||
| 49 | text: "Water consumption over the last week", | ||
| 50 | padding: {bottom: 10} | ||
| 51 | } | ||
| 52 | } | ||
| 53 | } | ||
| 54 | }); | ||
| 55 | |||
| 56 | onDestroy(() => { | ||
| 57 | if (chart) chart.destroy(); | ||
| 58 | chart = null; | ||
| 59 | }) | ||
| 60 | } | ||
| 61 | </script> | ||
| 62 | |||
| 63 | <canvas bind:this={ref} /> \ No newline at end of file | ||
diff --git a/fe/src/lib/DataView.svelte b/fe/src/lib/DataView.svelte index 0a6b81b..5e81a5a 100644 --- a/fe/src/lib/DataView.svelte +++ b/fe/src/lib/DataView.svelte | |||
| @@ -1,10 +1,11 @@ | |||
| 1 | <script lang="ts"> | 1 | <script lang="ts"> |
| 2 | import { onDestroy, onMount } from "svelte"; | 2 | import { onDestroy, onMount } from "svelte"; |
| 3 | import HttpClient from "../http"; | 3 | import http from "../http"; |
| 4 | import { token } from "../stores/auth"; | 4 | import { token } from "../stores/auth"; |
| 5 | import { addFormOpen } from "../stores/forms"; | 5 | import { addFormOpen } from "../stores/forms"; |
| 6 | import Table from "./Table.svelte"; | 6 | import Table from "./Table.svelte"; |
| 7 | import Chart from "chart.js/auto"; | 7 | import ChartJS from "chart.js/auto"; |
| 8 | import Chart from './Chart.svelte' | ||
| 8 | import Card from "./Card.svelte"; | 9 | import Card from "./Card.svelte"; |
| 9 | import Column from "./Column.svelte"; | 10 | import Column from "./Column.svelte"; |
| 10 | import AddForm from "./forms/AddForm.svelte"; | 11 | import AddForm from "./forms/AddForm.svelte"; |
| @@ -46,8 +47,8 @@ | |||
| 46 | 47 | ||
| 47 | if (res.ok) { | 48 | if (res.ok) { |
| 48 | const json = await res.json(); | 49 | const json = await res.json(); |
| 49 | let labels = json.map(d => d.name); | 50 | let labels = json.map((d: any) => d.name); |
| 50 | let data = json.map(d => d.total); | 51 | let data = json.map((d: any) => d.total); |
| 51 | return [labels, data]; | 52 | return [labels, data]; |
| 52 | } else { | 53 | } else { |
| 53 | throw new Error("There was a problem with your request"); | 54 | throw new Error("There was a problem with your request"); |
| @@ -65,8 +66,8 @@ | |||
| 65 | 66 | ||
| 66 | if (res.ok) { | 67 | if (res.ok) { |
| 67 | const json = await res.json(); | 68 | const json = await res.json(); |
| 68 | let labels = json.map(d => d.date); | 69 | let labels = json.map((d: any) => d.date); |
| 69 | let data = json.map(d => d.total); | 70 | let data = json.map((d: any) => d.total); |
| 70 | return [labels, data]; | 71 | return [labels, data]; |
| 71 | } else { | 72 | } else { |
| 72 | throw new Error("There was a problem with your request"); | 73 | throw new Error("There was a problem with your request"); |
| @@ -84,9 +85,9 @@ | |||
| 84 | fetchDailyUserStatistics().then(updateDailyUserTotalsChart).catch(err => console.error(err)); | 85 | fetchDailyUserStatistics().then(updateDailyUserTotalsChart).catch(err => console.error(err)); |
| 85 | } | 86 | } |
| 86 | 87 | ||
| 87 | function setupWeeklyTotalsChart(result) { | 88 | function setupWeeklyTotalsChart(result: any) { |
| 88 | [lastSevenDays, lastSevenDaysData] = result; | 89 | [lastSevenDays, lastSevenDaysData] = result; |
| 89 | lineChart = new Chart(lineCanvasRef, { | 90 | lineChart = new ChartJS(lineCanvasRef, { |
| 90 | type: "line", | 91 | type: "line", |
| 91 | data: { | 92 | data: { |
| 92 | labels: lastSevenDays, | 93 | labels: lastSevenDays, |
| @@ -129,10 +130,10 @@ | |||
| 129 | }); | 130 | }); |
| 130 | } | 131 | } |
| 131 | 132 | ||
| 132 | function setupDailyUserTotalsChart(result) { | 133 | function setupDailyUserTotalsChart(result: any) { |
| 133 | [userTotalsLabels, userTotalsData] = result; | 134 | [userTotalsLabels, userTotalsData] = result; |
| 134 | 135 | ||
| 135 | barChart = new Chart(barCanvasRef, { | 136 | barChart = new ChartJS(barCanvasRef, { |
| 136 | type: "bar", | 137 | type: "bar", |
| 137 | data: { | 138 | data: { |
| 138 | labels: userTotalsLabels, | 139 | labels: userTotalsLabels, |
| @@ -177,13 +178,13 @@ | |||
| 177 | }); | 178 | }); |
| 178 | } | 179 | } |
| 179 | 180 | ||
| 180 | function updateWeeklyTotalsChart(result) { | 181 | function updateWeeklyTotalsChart(result: any) { |
| 181 | [, lastSevenDaysData] = result; | 182 | [, lastSevenDaysData] = result; |
| 182 | lineChart.data.datasets[0].data = lastSevenDaysData; | 183 | lineChart.data.datasets[0].data = lastSevenDaysData; |
| 183 | lineChart.update(); | 184 | lineChart.update(); |
| 184 | } | 185 | } |
| 185 | 186 | ||
| 186 | function updateDailyUserTotalsChart(result) { | 187 | function updateDailyUserTotalsChart(result: any) { |
| 187 | [, userTotalsData] = result; | 188 | [, userTotalsData] = result; |
| 188 | barChart.data.datasets[0].data = userTotalsData; | 189 | barChart.data.datasets[0].data = userTotalsData; |
| 189 | barChart.update(); | 190 | barChart.update(); |
| @@ -205,6 +206,7 @@ | |||
| 205 | 206 | ||
| 206 | <Column --width="500px"> | 207 | <Column --width="500px"> |
| 207 | <Card --height="300px"> | 208 | <Card --height="300px"> |
| 209 | <!--<Chart />--> | ||
| 208 | <canvas bind:this={barCanvasRef} /> | 210 | <canvas bind:this={barCanvasRef} /> |
| 209 | </Card> | 211 | </Card> |
| 210 | <Card --height="300px"> | 212 | <Card --height="300px"> |
| @@ -223,17 +225,3 @@ | |||
| 223 | </Card> | 225 | </Card> |
| 224 | </Column> | 226 | </Column> |
| 225 | <!-- <Chart /> --> | 227 | <!-- <Chart /> --> |
| 226 | |||
| 227 | |||
| 228 | <style> | ||
| 229 | dialog { | ||
| 230 | background: red; | ||
| 231 | box-shadow: 0 20px 5em 10px rgba(0, 0, 0, 0.8); | ||
| 232 | } | ||
| 233 | |||
| 234 | dialog::backdrop { | ||
| 235 | padding: 20px; | ||
| 236 | box-shadow: 20px 20px rgba(0, 0, 0, 0.8); | ||
| 237 | background-color: red; | ||
| 238 | } | ||
| 239 | </style> | ||
diff --git a/fe/src/lib/Layout.svelte b/fe/src/lib/Layout.svelte index f208f34..2728dd3 100644 --- a/fe/src/lib/Layout.svelte +++ b/fe/src/lib/Layout.svelte | |||
| @@ -1,9 +1,13 @@ | |||
| 1 | <script> | 1 | <script> |
| 2 | import { authenticated, token } from "../stores/auth"; | 2 | import { authenticated, token, user, preferences } from "../stores/auth"; |
| 3 | import PreferencesForm from "./PreferencesForm.svelte"; | 3 | import PreferencesForm from "./PreferencesForm.svelte"; |
| 4 | import { addFormOpen } from "../stores/forms"; | 4 | import { addFormOpen } from "../stores/forms"; |
| 5 | 5 | ||
| 6 | const logout = () => token.unauthenticate(); | 6 | const logout = () => { |
| 7 | preferences.reset(); | ||
| 8 | user.reset(); | ||
| 9 | token.unauthenticate(); | ||
| 10 | } | ||
| 7 | let preferenceFormOpen = false; | 11 | let preferenceFormOpen = false; |
| 8 | 12 | ||
| 9 | function showPreferencesDialog() { | 13 | function showPreferencesDialog() { |
diff --git a/fe/src/lib/LoginForm.svelte b/fe/src/lib/LoginForm.svelte index bf6d9ad..8c3c288 100644 --- a/fe/src/lib/LoginForm.svelte +++ b/fe/src/lib/LoginForm.svelte | |||
| @@ -47,7 +47,7 @@ | |||
| 47 | preferences: userPreferences, | 47 | preferences: userPreferences, |
| 48 | } = await response.json(); | 48 | } = await response.json(); |
| 49 | user.setUser(userData); | 49 | user.setUser(userData); |
| 50 | preferences.set(userPreferences); | 50 | preferences.setPreference(userPreferences); |
| 51 | token.authenticate(apiToken); | 51 | token.authenticate(apiToken); |
| 52 | } | 52 | } |
| 53 | 53 | ||
diff --git a/fe/src/lib/PreferencesForm.svelte b/fe/src/lib/PreferencesForm.svelte index 95e04c1..875393c 100644 --- a/fe/src/lib/PreferencesForm.svelte +++ b/fe/src/lib/PreferencesForm.svelte | |||
| @@ -1,51 +1,118 @@ | |||
| 1 | <script lang="ts"> | 1 | <script lang="ts"> |
| 2 | import { preferences } from '../stores/auth'; | 2 | import { user, preferences, token } from "../stores/auth"; |
| 3 | import type { Size, Preference } from '../types'; | 3 | import { createEventDispatcher, onDestroy, onMount } from "svelte"; |
| 4 | import { createEventDispatcher } from 'svelte'; | 4 | import type { User } from "../types"; |
| 5 | export let open: boolean; | 5 | |
| 6 | 6 | export let open: boolean; | |
| 7 | let preference: Preference = { | 7 | |
| 8 | color: "#00FF00", | 8 | let sizes: Array<any>; |
| 9 | size: { | 9 | let selectedSize: number = 1; |
| 10 | size: 8, | 10 | let color: string = "#000000"; |
| 11 | unit: 'oz' | 11 | |
| 12 | } | 12 | const dispatch = createEventDispatcher(); |
| 13 | } | 13 | |
| 14 | 14 | const unsubscribe = preferences.subscribe( | |
| 15 | const dispatch = createEventDispatcher(); | 15 | (value: any) => { |
| 16 | 16 | console.log('update value: ', value); | |
| 17 | preferences.subscribe((value) => { | 17 | color = value.color; |
| 18 | preference = value; | 18 | selectedSize = value.size_id; |
| 19 | }, | ||
| 20 | ); | ||
| 21 | |||
| 22 | function closeDialog() { | ||
| 23 | dispatch("close"); | ||
| 24 | } | ||
| 25 | |||
| 26 | async function updateUserPreferences() { | ||
| 27 | const res = await fetch("http://localhost:8080/api/v1/user/preferences", { | ||
| 28 | method: "PATCH", | ||
| 29 | headers: { | ||
| 30 | Authorization: `Bearer ${$token}`, | ||
| 31 | }, | ||
| 32 | body: JSON.stringify($preferences), | ||
| 19 | }); | 33 | }); |
| 34 | } | ||
| 20 | 35 | ||
| 21 | function onPreferencesSave(): void { | 36 | async function getUserPreferences() { |
| 22 | preferences.set(preference); | 37 | const res = await fetch( |
| 23 | dispatch('close') | 38 | `http://localhost:8080/api/v1/user/${($user as User)!.id}/preferences`, |
| 24 | } | 39 | { |
| 40 | method: "GET", | ||
| 41 | headers: { | ||
| 42 | Authorization: `Bearer ${$token}`, | ||
| 43 | }, | ||
| 44 | }, | ||
| 45 | ); | ||
| 46 | const updatePreferences = await res.json(); | ||
| 47 | preferences.set(updatePreferences); | ||
| 48 | } | ||
| 25 | 49 | ||
| 50 | async function onPreferencesSave(): Promise<void> { | ||
| 51 | preferences.update((value) => ({ | ||
| 52 | ...value!, | ||
| 53 | size_id: selectedSize, | ||
| 54 | color: color, | ||
| 55 | })); | ||
| 56 | |||
| 57 | await updateUserPreferences(); | ||
| 58 | await getUserPreferences(); | ||
| 59 | |||
| 60 | dispatch("close"); | ||
| 61 | } | ||
| 62 | |||
| 63 | onMount(() => { | ||
| 64 | fetch("http://localhost:8080/api/v1/sizes", { | ||
| 65 | method: "GET", | ||
| 66 | headers: { | ||
| 67 | Authorization: `Bearer ${$token}`, | ||
| 68 | }, | ||
| 69 | }) | ||
| 70 | .then((res) => res.json()) | ||
| 71 | .then((val) => (sizes = val)); | ||
| 72 | }); | ||
| 73 | |||
| 74 | onDestroy(() => { | ||
| 75 | unsubscribe(); | ||
| 76 | }); | ||
| 26 | </script> | 77 | </script> |
| 78 | |||
| 27 | <dialog {open} on:submit|preventDefault={onPreferencesSave}> | 79 | <dialog {open} on:submit|preventDefault={onPreferencesSave}> |
| 28 | <h2>User Preferences</h2> | 80 | <h2>User Preferences</h2> |
| 29 | <form method="dialog"> | 81 | <form method="dialog"> |
| 30 | <div class="form input group"> | 82 | <div class="form input group"> |
| 31 | <label for="color">Color</label> | 83 | <label for="color">Color</label> |
| 32 | <input id="color" name="color" type="color" bind:value={preference.color}/> | 84 | <input |
| 33 | </div> | 85 | id="color" |
| 34 | <div class="form input group"> | 86 | name="color" |
| 35 | <label for="size">Bottle Size</label> | 87 | type="color" |
| 36 | <input id="size" name="size" type="number" min="8" max="48" step="8" bind:value={preference.size.size}/> | 88 | bind:value={color} |
| 37 | </div> | 89 | /> |
| 38 | <button type="submit">Save</button> | 90 | </div> |
| 39 | </form> | 91 | <div class="form input group"> |
| 92 | <label for="size">Bottle Size</label> | ||
| 93 | <select | ||
| 94 | bind:value={selectedSize} | ||
| 95 | > | ||
| 96 | {#if sizes} | ||
| 97 | {#each sizes as size} | ||
| 98 | <option value={size.id}>{size.size} {size.unit}</option> | ||
| 99 | {/each} | ||
| 100 | {/if} | ||
| 101 | </select> | ||
| 102 | </div> | ||
| 103 | <button on:click={closeDialog}>Cancel</button> | ||
| 104 | <button type="submit">Save</button> | ||
| 105 | </form> | ||
| 40 | </dialog> | 106 | </dialog> |
| 107 | |||
| 41 | <style> | 108 | <style> |
| 42 | dialog { | 109 | dialog { |
| 43 | background: white; | 110 | background: white; |
| 44 | color: black; | 111 | color: black; |
| 45 | } | 112 | } |
| 46 | 113 | ||
| 47 | input[type="color"] { | 114 | input[type="color"] { |
| 48 | width: 100%; | 115 | width: 4em; |
| 49 | height: 100%; | 116 | height: 4em; |
| 50 | } | 117 | } |
| 51 | </style> | 118 | </style> |
diff --git a/fe/src/lib/errors.ts b/fe/src/lib/errors.ts index d44bec5..81f7145 100644 --- a/fe/src/lib/errors.ts +++ b/fe/src/lib/errors.ts | |||
| @@ -1,7 +1,5 @@ | |||
| 1 | export class UnauthorizedError extends Error { | 1 | export class UnauthorizedError extends Error { |
| 2 | constructor(message?: string, options?: ErrorOptions) { | 2 | constructor(message?: string) { |
| 3 | super(message, options); | 3 | super(message); |
| 4 | } | 4 | } |
| 5 | } | 5 | } |
| 6 | |||
| 7 | |||
diff --git a/fe/src/lib/utils.ts b/fe/src/lib/utils.ts index 22d4e9a..e78556c 100644 --- a/fe/src/lib/utils.ts +++ b/fe/src/lib/utils.ts | |||
| @@ -1,5 +1,5 @@ | |||
| 1 | export function processFormInput(form: HTMLFormElement) { | 1 | export function processFormInput(form: HTMLFormElement) { |
| 2 | const formData = new FormData(form); | 2 | const formData: FormData = new FormData(form); |
| 3 | const data: Record<string, any> = {}; | 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; |
diff --git a/fe/src/stores/auth.ts b/fe/src/stores/auth.ts index 0efc80b..63f027e 100644 --- a/fe/src/stores/auth.ts +++ b/fe/src/stores/auth.ts | |||
| @@ -1,100 +1,87 @@ | |||
| 1 | import type { Invalidator, Subscriber, Unsubscriber } from 'svelte/store'; | 1 | import type { Preference, TokenStore, Nullable, UserStore, User, PreferenceStore } from "../types"; |
| 2 | import type { Preference } from '../types'; | 2 | import { writable, derived } from "svelte/store"; |
| 3 | import { writable, derived } from 'svelte/store'; | ||
| 4 | 3 | ||
| 5 | type Nullable<T> = T | null; | ||
| 6 | |||
| 7 | interface User { | ||
| 8 | uuid: string; | ||
| 9 | username: string; | ||
| 10 | } | ||
| 11 | |||
| 12 | interface TokenStore { | ||
| 13 | subscribe: (run: Subscriber<Nullable<string>>, invalidate?: Invalidator<Nullable<string>>) => Unsubscriber, | ||
| 14 | authenticate: (newToken: string) => void, | ||
| 15 | unauthenticate: () => void | ||
| 16 | } | ||
| 17 | |||
| 18 | |||
| 19 | interface UserStore { | ||
| 20 | subscribe: (run: Subscriber<Nullable<User>>, invalidate?: Invalidator<Nullable<User>>) => Unsubscriber, | ||
| 21 | setUser: (user: User) => void, | ||
| 22 | reset: () => void | ||
| 23 | } | ||
| 24 | |||
| 25 | interface PreferenceStore { | ||
| 26 | subscribe: (run: Subscriber<Preference>, invalidate?: Invalidator<Preference>) => Unsubscriber, | ||
| 27 | set: (this: void, value: Preference) => void | ||
| 28 | } | ||
| 29 | 4 | ||
| 30 | function createTokenStore(): TokenStore { | 5 | function createTokenStore(): TokenStore { |
| 31 | const storedToken = localStorage.getItem("token"); | 6 | const storedToken = localStorage.getItem("token"); |
| 32 | const { subscribe, set } = writable<string | null>(storedToken); | 7 | const { subscribe, set } = writable<string | null>(storedToken); |
| 33 | 8 | ||
| 34 | function authenticate(newToken: string): void { | 9 | function authenticate(newToken: string): void { |
| 35 | try { | 10 | try { |
| 36 | localStorage.setItem("token", newToken); | 11 | localStorage.setItem("token", newToken); |
| 37 | set(newToken); | 12 | set(newToken); |
| 38 | } catch (e) { | 13 | } catch (e) { |
| 39 | console.error('error', e); | 14 | console.error("error", e); |
| 15 | } | ||
| 16 | } | ||
| 17 | |||
| 18 | function unauthenticate(): void { | ||
| 19 | localStorage.removeItem("token"); | ||
| 20 | set(null); | ||
| 40 | } | 21 | } |
| 41 | } | 22 | |
| 42 | 23 | return { | |
| 43 | function unauthenticate(): void { | 24 | subscribe, |
| 44 | localStorage.removeItem("token"); | 25 | authenticate, |
| 45 | set(null); | 26 | unauthenticate |
| 46 | } | 27 | }; |
| 47 | |||
| 48 | return { | ||
| 49 | subscribe, | ||
| 50 | authenticate, | ||
| 51 | unauthenticate | ||
| 52 | }; | ||
| 53 | } | 28 | } |
| 54 | 29 | ||
| 55 | function onTokenChange($token: Nullable<string>): boolean { | 30 | function onTokenChange($token: Nullable<string>): boolean { |
| 56 | return $token ? true : false; | 31 | return $token ? true : false; |
| 57 | } | 32 | } |
| 58 | 33 | ||
| 59 | function createUserStore(): UserStore { | 34 | function createUserStore(): UserStore { |
| 60 | const user = localStorage.getItem('user'); | 35 | const user = localStorage.getItem("user"); |
| 61 | const userObj: Nullable<User> = user ? JSON.parse(user) : null; | 36 | const userObj: Nullable<User> = user ? JSON.parse(user) : null; |
| 62 | const { subscribe, set } = writable<User | null>(userObj); | 37 | const { subscribe, set } = writable<User | null>(userObj); |
| 63 | 38 | ||
| 64 | const setUser = (user: User) => { | 39 | const setUser = (user: User) => { |
| 65 | localStorage.setItem('user', JSON.stringify(user)); | 40 | localStorage.setItem("user", JSON.stringify(user)); |
| 66 | set(user); | 41 | set(user); |
| 67 | } | 42 | }; |
| 68 | 43 | ||
| 69 | const reset = () => { | 44 | const reset = () => { |
| 70 | localStorage.removeItem('user'); | 45 | localStorage.removeItem("user"); |
| 71 | set(null); | 46 | set(null); |
| 72 | } | 47 | }; |
| 73 | 48 | ||
| 74 | return { | 49 | return { |
| 75 | subscribe, | 50 | subscribe, |
| 76 | setUser, | 51 | setUser, |
| 77 | reset | 52 | reset |
| 78 | } | 53 | }; |
| 79 | } | 54 | } |
| 80 | 55 | ||
| 81 | 56 | ||
| 82 | function createPreferenceStore(): PreferenceStore { | 57 | function createPreferenceStore(): PreferenceStore { |
| 83 | const preferences = localStorage.getItem('preferences'); | 58 | const preferences = localStorage.getItem("preferences"); |
| 84 | const preferenceObj: Preference = preferences ? JSON.parse(preferences) : { | 59 | const preferenceObj: Preference = preferences ? JSON.parse(preferences) : { |
| 85 | color: "#FF0000", | 60 | id: 0, |
| 86 | size: { | 61 | color: "#FF0000", |
| 87 | size: 16, | 62 | size_id: 0, |
| 88 | unit: 'oz' | 63 | user_id: 0 |
| 89 | } | 64 | }; |
| 90 | }; | 65 | |
| 91 | 66 | const { subscribe, set, update } = writable<Nullable<Preference>>(preferenceObj); | |
| 92 | const { subscribe, set } = writable<Preference>(preferenceObj); | 67 | |
| 93 | 68 | const setPreference = (preference: Preference) => { | |
| 94 | return { | 69 | localStorage.setItem("preference", JSON.stringify(preference)); |
| 95 | subscribe, | 70 | set(preference); |
| 96 | set | 71 | }; |
| 97 | } | 72 | |
| 73 | const reset = () => { | ||
| 74 | localStorage.removeItem("preference"); | ||
| 75 | set(null); | ||
| 76 | }; | ||
| 77 | |||
| 78 | return { | ||
| 79 | set, | ||
| 80 | subscribe, | ||
| 81 | reset, | ||
| 82 | update, | ||
| 83 | setPreference, | ||
| 84 | }; | ||
| 98 | } | 85 | } |
| 99 | 86 | ||
| 100 | export const token = createTokenStore(); | 87 | export const token = createTokenStore(); |
diff --git a/fe/src/types.ts b/fe/src/types.ts index 526e7eb..c8f2f00 100644 --- a/fe/src/types.ts +++ b/fe/src/types.ts | |||
| @@ -1,14 +1,19 @@ | |||
| 1 | export interface Size { | 1 | import type { Invalidator, Subscriber, Unsubscriber, Updater } from "svelte/store"; |
| 2 | size: number; | ||
| 3 | unit: string; | ||
| 4 | } | ||
| 5 | 2 | ||
| 6 | export interface Preference { | 3 | export interface Preference { |
| 4 | id: number; | ||
| 7 | color: string; | 5 | color: string; |
| 8 | size: Size; | 6 | size_id: number; |
| 7 | user_id: number; | ||
| 8 | } | ||
| 9 | |||
| 10 | export interface Size { | ||
| 11 | size: number; | ||
| 12 | unit: string; | ||
| 9 | } | 13 | } |
| 10 | 14 | ||
| 11 | export interface User { | 15 | export interface User { |
| 16 | id: number; | ||
| 12 | name: string; | 17 | name: string; |
| 13 | uuid: string; | 18 | uuid: string; |
| 14 | } | 19 | } |
| @@ -17,4 +22,32 @@ export interface Statistic { | |||
| 17 | user_id: string; | 22 | user_id: string; |
| 18 | date: string; | 23 | date: string; |
| 19 | quantity: number; | 24 | quantity: number; |
| 20 | } \ No newline at end of file | 25 | } |
| 26 | |||
| 27 | export type Nullable<T> = T | null; | ||
| 28 | |||
| 29 | export interface User { | ||
| 30 | uuid: string; | ||
| 31 | username: string; | ||
| 32 | } | ||
| 33 | |||
| 34 | export interface TokenStore { | ||
| 35 | subscribe: (run: Subscriber<Nullable<string>>, invalidate?: Invalidator<Nullable<string>>) => Unsubscriber, | ||
| 36 | authenticate: (newToken: string) => void, | ||
| 37 | unauthenticate: () => void | ||
| 38 | } | ||
| 39 | |||
| 40 | |||
| 41 | export interface UserStore { | ||
| 42 | subscribe: (run: Subscriber<Nullable<User>>, invalidate?: Invalidator<Nullable<User>>) => Unsubscriber, | ||
| 43 | setUser: (user: User) => void, | ||
| 44 | reset: () => void | ||
| 45 | } | ||
| 46 | |||
| 47 | export interface PreferenceStore { | ||
| 48 | set: (this: void, value: Preference) => void; | ||
| 49 | subscribe: (this: void, run: Subscriber<Nullable<Preference>>, invalidate?: Invalidator<Nullable<Preference>>) => Unsubscriber; | ||
| 50 | reset: () => void; | ||
| 51 | update: (this: void, updater: Updater<Nullable<Preference>>) => void; | ||
| 52 | setPreference: (user: Preference) => void; | ||
| 53 | } | ||
