diff options
author | Zach Berwaldt <zberwaldt@tutamail.com> | 2024-03-02 16:52:55 -0500 |
---|---|---|
committer | Zach Berwaldt <zberwaldt@tutamail.com> | 2024-03-02 16:52:55 -0500 |
commit | cf2113e77edabf8e3a632c7b76c769752039ba88 (patch) | |
tree | 874872f22aa63df532769de62119816748b167f8 /fe | |
parent | 326f186d67017f87e631a1fbcdf3f184cbc42d7d (diff) |
feat: Add API logging
Add logging to the API to keep track of specific requests and headers. Also added CORS middleware to handle OPTIONS requests.
---
The commit adds logging functionality to the API and includes a middleware function to handle CORS OPTIONS requests. This will allow us to track specific requests and headers received by the API.
[API/main.go](/api/main.go):
- Added import for the 'log' package
- Added logging statements to print the request headers and "_I am here_" message
- Removed unnecessary newlines and comments
[fe/src/app.css](/fe/src/app.css):
- Added a new style for button hover effects
[fe/src/lib/Card.svelte](/fe/src/lib/Card.svelte):
- Added a new `height` prop to the Card component
[fe/src/lib/Column.svelte](/fe/src/lib/Column.svelte):
- Added a new CSS class for column layout
- Set the width and gap using CSS variables
[fe/src/lib/DataView.svelte](/fe/src/lib/DataView.svelte):
- Updated the 'fetchData' function to also fetch 'totals' and 'userStats' data
- Added canvas references and chart variables for bar and line charts
- Added a new 'getLastSevenDays' function to calculate the labels for the charts
- Updated the 'onMount' function to initialize the bar and line charts using the canvas references and data
- Updated the 'onDestroy' function to destroy the bar and line charts
- Added a new 'addFormOpen' store and imported it
- Added a new 'onClick' handler for the Add button to open the AddForm modal
- Updated the layout and added Card components to display the bar and line charts and the JSON data
- Added a new 'fetchTotals' function to fetch data for the 'totals' section
- Added a new 'fetchStatsForUser' function to fetch data for the 'userStats' section
[fe/src/lib/Layout.svelte](/fe/src/lib/Layout.svelte):
- Added a new 'preferenceFormOpen' variable and initialized it to 'false'
- Added a new 'showPreferencesDialog' function to set 'preferenceFormOpen' to 'true'
- Added a new 'closePreferenceDialog' function to set 'preferenceFormOpen' to 'false'
- Added a new 'showAddDialog' function to open the AddForm modal
Diffstat (limited to 'fe')
-rw-r--r-- | fe/src/app.css | 101 | ||||
-rw-r--r-- | fe/src/lib/Card.svelte | 30 | ||||
-rw-r--r-- | fe/src/lib/Column.svelte | 13 | ||||
-rw-r--r-- | fe/src/lib/DataView.svelte | 129 | ||||
-rw-r--r-- | fe/src/lib/Layout.svelte | 116 | ||||
-rw-r--r-- | fe/src/lib/Table.svelte | 172 | ||||
-rw-r--r-- | fe/src/stores/forms.ts | 6 |
7 files changed, 345 insertions, 222 deletions
diff --git a/fe/src/app.css b/fe/src/app.css index 0d5fa90..de19b52 100644 --- a/fe/src/app.css +++ b/fe/src/app.css | |||
@@ -1,82 +1,87 @@ | |||
1 | :root { | 1 | :root { |
2 | font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; | 2 | font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; |
3 | line-height: 1.5; | 3 | line-height: 1.5; |
4 | font-weight: 400; | 4 | font-weight: 400; |
5 | 5 | ||
6 | color-scheme: light dark; | 6 | color-scheme: light dark; |
7 | color: rgba(255, 255, 255, 0.87); | 7 | color: rgba(255, 255, 255, 0.87); |
8 | background-color: #242424; | 8 | background-color: #242424; |
9 | 9 | ||
10 | font-synthesis: none; | 10 | font-synthesis: none; |
11 | text-rendering: optimizeLegibility; | 11 | text-rendering: optimizeLegibility; |
12 | -webkit-font-smoothing: antialiased; | 12 | -webkit-font-smoothing: antialiased; |
13 | -moz-osx-font-smoothing: grayscale; | 13 | -moz-osx-font-smoothing: grayscale; |
14 | 14 | ||
15 | --submit: #28a745; | 15 | --submit: #28a745; |
16 | } | 16 | } |
17 | 17 | ||
18 | a { | 18 | a { |
19 | font-weight: 500; | 19 | font-weight: 500; |
20 | color: #646cff; | 20 | color: #646cff; |
21 | text-decoration: inherit; | 21 | text-decoration: inherit; |
22 | } | 22 | } |
23 | |||
23 | a:hover { | 24 | a:hover { |
24 | color: #535bf2; | 25 | color: #535bf2; |
25 | } | 26 | } |
26 | 27 | ||
27 | body { | 28 | body { |
28 | margin: 0; | 29 | margin: 0; |
29 | display: flex; | 30 | display: flex; |
30 | place-items: center; | 31 | place-items: center; |
31 | min-width: 320px; | 32 | min-width: 320px; |
32 | min-height: 100vh; | 33 | min-height: 100vh; |
33 | } | 34 | } |
34 | 35 | ||
35 | h1 { | 36 | h1 { |
36 | font-size: 3.2em; | 37 | font-size: 3.2em; |
37 | line-height: 1.1; | 38 | line-height: 1.1; |
38 | } | 39 | } |
39 | 40 | ||
40 | .card { | 41 | .card { |
41 | padding: 2em; | 42 | padding: 2em; |
42 | } | 43 | } |
43 | 44 | ||
44 | #app { | 45 | #app { |
45 | flex-grow: 2; | 46 | flex-grow: 2; |
46 | max-width: 1280px; | 47 | max-width: 1280px; |
47 | margin: 0 auto; | 48 | margin: 0 auto; |
48 | } | 49 | } |
49 | 50 | ||
50 | button { | 51 | button { |
51 | border-radius: 8px; | 52 | border-radius: 8px; |
52 | border: 1px solid transparent; | 53 | border: 1px solid transparent; |
53 | padding: 0.6em 1.2em; | 54 | padding: 0.6em 1.2em; |
54 | font-size: 1em; | 55 | font-size: 1em; |
55 | font-weight: 500; | 56 | font-weight: 500; |
56 | font-family: inherit; | 57 | font-family: inherit; |
57 | background-color: #1a1a1a; | 58 | background-color: #1a1a1a; |
58 | cursor: pointer; | 59 | cursor: pointer; |
59 | transition: border-color 0.25s; | 60 | transition: border-color 0.25s; |
60 | } | 61 | } |
62 | |||
61 | button:hover { | 63 | button:hover { |
62 | border-color: #646cff; | 64 | border-color: #646cff; |
63 | } | 65 | } |
66 | |||
64 | button:focus, | 67 | button:focus, |
65 | button:focus-visible { | 68 | button:focus-visible { |
66 | outline: 4px auto -webkit-focus-ring-color; | 69 | outline: 4px auto -webkit-focus-ring-color; |
67 | } | 70 | } |
68 | 71 | ||
69 | @media (prefers-color-scheme: light) { | 72 | @media (prefers-color-scheme: light) { |
70 | :root { | 73 | :root { |
71 | color: #213547; | 74 | color: #213547; |
72 | background-color: #ffffff; | 75 | background-color: #ffffff; |
73 | } | 76 | } |
74 | a:hover { | 77 | |
75 | color: #747bff; | 78 | a:hover { |
76 | } | 79 | color: #747bff; |
77 | button { | 80 | } |
78 | background-color: #f9f9f9; | 81 | |
79 | } | 82 | button { |
83 | background-color: #f9f9f9; | ||
84 | } | ||
80 | } | 85 | } |
81 | 86 | ||
82 | @media (prefers-color-scheme: dark) { | 87 | @media (prefers-color-scheme: dark) { |
@@ -97,7 +102,7 @@ button:focus-visible { | |||
97 | } | 102 | } |
98 | 103 | ||
99 | .form.input.group label { | 104 | .form.input.group label { |
100 | margin-bottom: .5em; | 105 | margin-bottom: .5em; |
101 | } | 106 | } |
102 | 107 | ||
103 | .form.input.group input { | 108 | .form.input.group input { |
diff --git a/fe/src/lib/Card.svelte b/fe/src/lib/Card.svelte index 0835940..d7cd900 100644 --- a/fe/src/lib/Card.svelte +++ b/fe/src/lib/Card.svelte | |||
@@ -1,22 +1,24 @@ | |||
1 | <script lang="ts"> | 1 | <script lang="ts"> |
2 | export let title = ""; | 2 | export let title = ""; |
3 | export let height: number | undefined = undefined; | ||
3 | </script> | 4 | </script> |
4 | 5 | ||
5 | <div class="card"> | 6 | <div class="card"> |
6 | {#if title} | 7 | {#if title} |
7 | <h2>{title}</h2> | 8 | <h2>{title}</h2> |
8 | {/if} | 9 | {/if} |
9 | <slot /> | 10 | <slot /> |
10 | </div> | 11 | </div> |
11 | 12 | ||
12 | <style> | 13 | <style> |
13 | .card { | 14 | .card { |
14 | background: #fff; | 15 | background: #fff; |
15 | width: 16rem; | 16 | display: flex; |
16 | display: flex; | 17 | flex-direction: column; |
17 | flex-direction: column; | 18 | align-items: flex-start; |
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 | height: var(--height, fit-content); |
21 | } | 22 | overflow-y: var(--overflow, initial); |
23 | } | ||
22 | </style> | 24 | </style> |
diff --git a/fe/src/lib/Column.svelte b/fe/src/lib/Column.svelte new file mode 100644 index 0000000..f036073 --- /dev/null +++ b/fe/src/lib/Column.svelte | |||
@@ -0,0 +1,13 @@ | |||
1 | <div class="column"> | ||
2 | <slot /> | ||
3 | </div> | ||
4 | |||
5 | <style> | ||
6 | .column { | ||
7 | display: flex; | ||
8 | flex-direction: column; | ||
9 | height: 100%; | ||
10 | gap: var(--gap, 32px); | ||
11 | width: var(--width, initial); | ||
12 | } | ||
13 | </style> \ No newline at end of file | ||
diff --git a/fe/src/lib/DataView.svelte b/fe/src/lib/DataView.svelte index 5182a85..2b1b8b9 100644 --- a/fe/src/lib/DataView.svelte +++ b/fe/src/lib/DataView.svelte | |||
@@ -1,16 +1,21 @@ | |||
1 | <script lang="ts"> | 1 | <script lang="ts"> |
2 | import { onDestroy, onMount } from "svelte"; | 2 | import { onDestroy, onMount } from "svelte"; |
3 | import { token } from "../stores/auth"; | 3 | import { token } from "../stores/auth"; |
4 | import { addFormOpen } from "../stores/forms"; | ||
4 | import Table from "./Table.svelte"; | 5 | import Table from "./Table.svelte"; |
5 | import Chart from "chart.js/auto"; | 6 | import Chart from "chart.js/auto"; |
7 | import Card from "./Card.svelte"; | ||
8 | import Column from "./Column.svelte"; | ||
6 | import AddForm from "./forms/AddForm.svelte"; | 9 | import AddForm from "./forms/AddForm.svelte"; |
7 | 10 | ||
8 | let open: boolean = false; | ||
9 | |||
10 | let json: Promise<any>; | 11 | let json: Promise<any>; |
12 | let totals: Promise<any>; | ||
13 | let userStats: Promise<any>; | ||
11 | 14 | ||
12 | let canvasRef: HTMLCanvasElement; | 15 | let barCanvasRef: HTMLCanvasElement; |
13 | let chart: any; | 16 | let lineCanvasRef: HTMLCanvasElement; |
17 | let barChart: any; | ||
18 | let lineChart: any; | ||
14 | 19 | ||
15 | let lastSevenDays: string[]; | 20 | let lastSevenDays: string[]; |
16 | 21 | ||
@@ -28,6 +33,37 @@ | |||
28 | } | 33 | } |
29 | } | 34 | } |
30 | 35 | ||
36 | async function fetchTotals() { | ||
37 | const res = await fetch("http://localhost:8080/api/v1/stats/totals/", { | ||
38 | method: 'GET', | ||
39 | mode: 'no-cors', | ||
40 | headers: { | ||
41 | Authorization: `Bearer ${$token}` | ||
42 | } | ||
43 | }); | ||
44 | |||
45 | if (res.ok) { | ||
46 | totals = res.json(); | ||
47 | } else { | ||
48 | throw new Error("There was a problem with your request"); | ||
49 | } | ||
50 | } | ||
51 | |||
52 | async function fetchStatsForUser() { | ||
53 | const res = await fetch("http://localhost:8080/api/v1/stats/user/1aa668f3-7527-4a67-9c24-fdf307542eeb", { | ||
54 | method: "GET", | ||
55 | headers: { | ||
56 | Authorization: `Bearer ${$token}` | ||
57 | } | ||
58 | }); | ||
59 | |||
60 | if (res.ok) { | ||
61 | userStats = res.json(); | ||
62 | } else { | ||
63 | throw new Error("There was a problem with your request"); | ||
64 | } | ||
65 | } | ||
66 | |||
31 | function getLastSevenDays() { | 67 | function getLastSevenDays() { |
32 | const result = []; | 68 | const result = []; |
33 | for (let i = 0; i < 7; i++) { | 69 | for (let i = 0; i < 7; i++) { |
@@ -38,23 +74,21 @@ | |||
38 | return result; | 74 | return result; |
39 | } | 75 | } |
40 | 76 | ||
41 | function handleClick() { | ||
42 | open = true; | ||
43 | } | ||
44 | |||
45 | function closeDialog() { | 77 | function closeDialog() { |
46 | open = false; | 78 | addFormOpen.set(false); |
47 | } | 79 | } |
48 | 80 | ||
49 | function onStatisticAdd() { | 81 | function onStatisticAdd() { |
50 | open = false; | 82 | closeDialog(); |
51 | fetchData(); | 83 | fetchData(); |
52 | } | 84 | } |
53 | 85 | ||
54 | onMount(() => { | 86 | onMount(() => { |
55 | fetchData(); | 87 | fetchData(); |
88 | // fetchTotals(); | ||
89 | fetchStatsForUser(); | ||
56 | lastSevenDays = getLastSevenDays(); | 90 | lastSevenDays = getLastSevenDays(); |
57 | chart = new Chart(canvasRef, { | 91 | barChart = new Chart(barCanvasRef, { |
58 | type: "bar", | 92 | type: "bar", |
59 | data: { | 93 | data: { |
60 | labels: lastSevenDays, | 94 | labels: lastSevenDays, |
@@ -73,27 +107,74 @@ | |||
73 | borderWidth: 1 | 107 | borderWidth: 1 |
74 | } | 108 | } |
75 | ] | 109 | ] |
110 | }, | ||
111 | options: { | ||
112 | responsive: true | ||
113 | } | ||
114 | }); | ||
115 | lineChart = new Chart(lineCanvasRef, { | ||
116 | type: "line", | ||
117 | data: { | ||
118 | labels: lastSevenDays, | ||
119 | datasets: [ | ||
120 | { | ||
121 | label: "Zach", | ||
122 | data: [1, 2, 8, 2, 5, 5, 1], | ||
123 | backgroundColor: "rgba(255, 192, 192, 0.2)", | ||
124 | borderColor: "rgba(75, 192, 192, 1)", | ||
125 | borderWidth: 1 | ||
126 | }, { | ||
127 | label: "Parker", | ||
128 | data: [6, 1, 1, 4, 3, 5, 1], | ||
129 | backgroundColor: "rgba(75, 192, 192, 0.2)", | ||
130 | borderColor: "rgba(75, 192, 192, 1)", | ||
131 | borderWidth: 1 | ||
132 | } | ||
133 | ] | ||
134 | }, | ||
135 | options: { | ||
136 | responsive: true | ||
76 | } | 137 | } |
77 | }); | 138 | }); |
78 | }); | 139 | }); |
79 | 140 | ||
80 | onDestroy(() => { | 141 | onDestroy(() => { |
81 | if (chart) chart.destroy(); | 142 | if (barChart) barChart.destroy(); |
82 | chart = null; | 143 | if (lineChart) lineChart.destroy(); |
144 | barChart = null; | ||
145 | lineChart = null; | ||
83 | }); | 146 | }); |
84 | </script> | 147 | </script> |
85 | 148 | ||
86 | <div> | 149 | <Column --width="500px"> |
87 | <button on:click={handleClick}>Add</button> | 150 | <Card> |
88 | <AddForm {open} on:submit={onStatisticAdd} on:close={closeDialog} /> | 151 | <canvas bind:this={barCanvasRef} width="" /> |
89 | <canvas bind:this={canvasRef} /> | 152 | </Card> |
90 | {#await json then data} | 153 | <Card> |
91 | <Table {data} nofooter /> | 154 | <canvas bind:this={lineCanvasRef} /> |
92 | {:catch error} | 155 | </Card> |
93 | <p>{error}</p> | 156 | </Column> |
94 | {/await} | 157 | |
95 | <!-- <Chart /> --> | 158 | <AddForm open={$addFormOpen} on:submit={onStatisticAdd} on:close={closeDialog} /> |
96 | </div> | 159 | <Column> |
160 | <Card> | ||
161 | {#await json then data} | ||
162 | <Table {data} nofooter /> | ||
163 | {:catch error} | ||
164 | <p>{error}</p> | ||
165 | {/await} | ||
166 | </Card> | ||
167 | <Card> | ||
168 | <button on:click={() => fetchTotals()}>Get totals</button> | ||
169 | {#await totals then data} | ||
170 | {JSON.stringify(data)} | ||
171 | {:catch error} | ||
172 | <p>{error}</p> | ||
173 | {/await} | ||
174 | </Card> | ||
175 | </Column> | ||
176 | <!-- <Chart /> --> | ||
177 | |||
97 | 178 | ||
98 | <style> | 179 | <style> |
99 | dialog { | 180 | dialog { |
diff --git a/fe/src/lib/Layout.svelte b/fe/src/lib/Layout.svelte index 94ce84d..f208f34 100644 --- a/fe/src/lib/Layout.svelte +++ b/fe/src/lib/Layout.svelte | |||
@@ -1,62 +1,70 @@ | |||
1 | <script> | 1 | <script> |
2 | import { authenticated, token } from "../stores/auth"; | 2 | import { authenticated, token } from "../stores/auth"; |
3 | import PreferencesForm from "./PreferencesForm.svelte"; | 3 | import PreferencesForm from "./PreferencesForm.svelte"; |
4 | const logout = () => token.unauthenticate(); | 4 | import { addFormOpen } from "../stores/forms"; |
5 | let open = false; | 5 | |
6 | 6 | const logout = () => token.unauthenticate(); | |
7 | function showSettingsDialog() { | 7 | let preferenceFormOpen = false; |
8 | open = true; | 8 | |
9 | } | 9 | function showPreferencesDialog() { |
10 | 10 | preferenceFormOpen = true; | |
11 | function closeDialog() { | 11 | } |
12 | open = false; | 12 | |
13 | } | 13 | function closePreferenceDialog() { |
14 | preferenceFormOpen = false; | ||
15 | } | ||
16 | |||
17 | function showAddDialog() { | ||
18 | addFormOpen.set(true); | ||
19 | } | ||
14 | </script> | 20 | </script> |
15 | 21 | ||
16 | <div class="layout"> | 22 | <div class="layout"> |
17 | {#if $authenticated} | 23 | {#if $authenticated} |
18 | <nav> | 24 | <nav> |
19 | <div> | 25 | <div> |
20 | <h1>Water</h1> | 26 | <h1>Water</h1> |
21 | </div> | 27 | </div> |
22 | <div> | 28 | <div> |
23 | <button on:click={showSettingsDialog}>Settings</button> | 29 | <button on:click={showAddDialog}>Log Water</button> |
24 | <button on:click={logout}>Logout</button> | 30 | <button on:click={showPreferencesDialog}>Preference</button> |
25 | </div> | 31 | <button on:click={logout}>Logout</button> |
26 | </nav> | 32 | </div> |
27 | <PreferencesForm {open} on:close={closeDialog} /> | 33 | </nav> |
28 | {/if} | 34 | <PreferencesForm open={preferenceFormOpen} on:close={closePreferenceDialog} /> |
29 | <div id="content"> | 35 | {/if} |
30 | <slot /> | 36 | <div id="content"> |
31 | </div> | 37 | <slot /> |
38 | </div> | ||
32 | </div> | 39 | </div> |
33 | 40 | ||
34 | <style> | 41 | <style> |
35 | .layout { | 42 | .layout { |
36 | height: 100vh; | 43 | height: 100vh; |
37 | } | 44 | } |
38 | nav { | 45 | |
39 | display: flex; | 46 | nav { |
40 | flex-direction: row; | 47 | display: flex; |
41 | align-items: center; | 48 | flex-direction: row; |
42 | justify-content: space-between; | 49 | align-items: center; |
43 | height: 64px; | 50 | justify-content: space-between; |
44 | padding: 0 2em; | 51 | height: 64px; |
45 | } | 52 | padding: 0 2em; |
46 | 53 | } | |
47 | nav div { | 54 | |
48 | width: fit-content; | 55 | nav div { |
49 | } | 56 | width: fit-content; |
50 | 57 | } | |
51 | nav div h1 { | 58 | |
52 | font-size: 1.75em; | 59 | nav div h1 { |
53 | } | 60 | font-size: 1.75em; |
54 | 61 | } | |
55 | #content { | 62 | |
56 | display: flex; | 63 | #content { |
57 | flex-direction: column; | 64 | display: flex; |
58 | justify-content: center; | 65 | flex-direction: row; |
59 | align-items: center; | 66 | justify-content: center; |
60 | padding: 3em 0; | 67 | gap: 2em; |
61 | } | 68 | margin-top: 4em; |
69 | } | ||
62 | </style> | 70 | </style> |
diff --git a/fe/src/lib/Table.svelte b/fe/src/lib/Table.svelte index 3a66e0d..d1cd7da 100644 --- a/fe/src/lib/Table.svelte +++ b/fe/src/lib/Table.svelte | |||
@@ -1,105 +1,113 @@ | |||
1 | <script lang="ts"> | 1 | <script lang="ts"> |
2 | export let data: Array<any> | undefined = undefined; | 2 | export let data: Array<any> | undefined = undefined; |
3 | export let nofooter: boolean = false; | 3 | export let nofooter: boolean = false; |
4 | export let noheader: boolean = false; | 4 | export let noheader: boolean = false; |
5 | export let omit: string[] = ["id"]; | 5 | export let omit: string[] = ["id"]; |
6 | export let title: string | undefined = undefined; | 6 | export let title: string | undefined = undefined; |
7 | 7 | ||
8 | function getDataKeys(data: any[]): string[] { | 8 | function getDataKeys(data: any[]): string[] { |
9 | if (!data || data.length === 0) return []; | 9 | if (!data || data.length === 0) return []; |
10 | return Object.keys(data[0]) | 10 | return Object.keys(data[0]) |
11 | .map((k) => k.split("_").join(" ")) | 11 | .map((k) => k.split("_").join(" ")) |
12 | .filter((k) => !omit.includes(k)); | 12 | .filter((k) => !omit.includes(k)); |
13 | } | 13 | } |
14 | 14 | ||
15 | function getRow(row: Record<string, any>): Array<any> { | 15 | function getRow(row: Record<string, any>): Array<any> { |
16 | return Object.entries(row).filter((r) => !omit.includes(r[0])); | 16 | return Object.entries(row).filter((r) => !omit.includes(r[0])); |
17 | } | 17 | } |
18 | 18 | ||
19 | const formatter = new Intl.DateTimeFormat("en", { | ||
20 | year: "numeric", | ||
21 | month: "numeric", | ||
22 | day: "numeric", | ||
23 | hour: "numeric", | ||
24 | minute: "2-digit", | ||
25 | second: "2-digit", | ||
26 | timeZone: "America/New_York", | ||
27 | }); | ||
28 | 19 | ||
29 | function formatDatum([key, value]: any[]) { | 20 | let limitedData: Array<any> = []; |
30 | if (key === "date") { | ||
31 | const parsedDate = new Date(value); | ||
32 | return formatter.format(parsedDate); | ||
33 | } | ||
34 | 21 | ||
35 | if (key === "user") { | 22 | if (data && (data as any[]).length > 0) { |
36 | return value["name"]; | 23 | limitedData = (data as any[]).slice(0, 4); |
37 | } | 24 | } |
38 | 25 | ||
39 | return value; | 26 | const formatter = new Intl.DateTimeFormat("en", { |
40 | } | 27 | year: "numeric", |
28 | month: "numeric", | ||
29 | day: "numeric", | ||
30 | hour: "numeric", | ||
31 | minute: "2-digit", | ||
32 | second: "2-digit", | ||
33 | timeZone: "America/New_York" | ||
34 | }); | ||
35 | |||
36 | function formatDatum([key, value]: any[]) { | ||
37 | if (key === "date") { | ||
38 | const parsedDate = new Date(value); | ||
39 | return formatter.format(parsedDate); | ||
40 | } | ||
41 | |||
42 | if (key === "user") { | ||
43 | return value["name"]; | ||
44 | } | ||
45 | |||
46 | return value; | ||
47 | } | ||
41 | </script> | 48 | </script> |
42 | 49 | ||
43 | <table> | 50 | <table> |
44 | {#if title} | 51 | {#if title} |
45 | <h2>{title}</h2> | 52 | <h2>{title}</h2> |
46 | {/if} | 53 | {/if} |
47 | {#if !noheader && data} | 54 | {#if !noheader && data} |
48 | <thead> | 55 | <thead> |
49 | <tr> | ||
50 | {#each getDataKeys(data) as header} | ||
51 | <th>{header}</th> | ||
52 | {/each} | ||
53 | </tr> | ||
54 | </thead> | ||
55 | {/if} | ||
56 | <tbody> | ||
57 | {#if data} | ||
58 | {#each data as row} | ||
59 | <tr> | 56 | <tr> |
60 | {#each getRow(row) as datum} | 57 | {#each getDataKeys(data) as header} |
61 | <td>{formatDatum(datum)}</td> | 58 | <th>{header}</th> |
62 | {/each} | 59 | {/each} |
63 | </tr> | 60 | </tr> |
64 | {/each} | 61 | </thead> |
62 | {/if} | ||
63 | <tbody> | ||
64 | {#if data} | ||
65 | {#each limitedData as row} | ||
66 | <tr> | ||
67 | {#each getRow(row) as datum} | ||
68 | <td>{formatDatum(datum)}</td> | ||
69 | {/each} | ||
70 | </tr> | ||
71 | {/each} | ||
65 | {:else} | 72 | {:else} |
66 | <tr> There is not data. </tr> | 73 | <tr> There is not data.</tr> |
74 | {/if} | ||
75 | </tbody> | ||
76 | {#if !nofooter} | ||
77 | <slot name="footer"> | ||
78 | <tfoot> | ||
79 | <tr> | ||
80 | <td>Table Footer</td> | ||
81 | </tr> | ||
82 | </tfoot> | ||
83 | </slot> | ||
67 | {/if} | 84 | {/if} |
68 | </tbody> | ||
69 | {#if !nofooter} | ||
70 | <slot name="footer"> | ||
71 | <tfoot> | ||
72 | <tr> | ||
73 | <td>Table Footer</td> | ||
74 | </tr> | ||
75 | </tfoot> | ||
76 | </slot> | ||
77 | {/if} | ||
78 | </table> | 85 | </table> |
79 | 86 | ||
80 | <style> | 87 | <style> |
81 | table { | 88 | table { |
82 | padding: 16px; | 89 | padding: 16px; |
83 | margin: 8px; | 90 | margin: 8px; |
84 | border: solid 1px black; | 91 | border: solid 1px black; |
85 | border-collapse: collapse; | 92 | border-collapse: collapse; |
86 | } | 93 | overflow-y: hidden; |
94 | } | ||
87 | 95 | ||
88 | th { | 96 | th { |
89 | text-transform: capitalize; | 97 | text-transform: capitalize; |
90 | } | 98 | } |
91 | 99 | ||
92 | thead tr { | 100 | thead tr { |
93 | background: rgba(0, 0, 23, 0.34); | 101 | background: rgba(0, 0, 23, 0.34); |
94 | } | 102 | } |
95 | 103 | ||
96 | tbody tr:nth-child(odd) { | 104 | tbody tr:nth-child(odd) { |
97 | background: rgba(0, 0, 23, 0.14); | 105 | background: rgba(0, 0, 23, 0.14); |
98 | } | 106 | } |
99 | 107 | ||
100 | th, | 108 | th, |
101 | td { | 109 | td { |
102 | padding: 1em; | 110 | padding: 1em; |
103 | border: 1px solid rgba(0, 0, 0, 1); | 111 | border: 1px solid rgba(0, 0, 0, 1); |
104 | } | 112 | } |
105 | </style> | 113 | </style> |
diff --git a/fe/src/stores/forms.ts b/fe/src/stores/forms.ts new file mode 100644 index 0000000..daf9181 --- /dev/null +++ b/fe/src/stores/forms.ts | |||
@@ -0,0 +1,6 @@ | |||
1 | import type { Writable } from "svelte/store"; | ||
2 | import { writable } from "svelte/store"; | ||
3 | |||
4 | |||
5 | export const preferencesFormOpen: Writable<boolean> = writable<boolean>(false); | ||
6 | export const addFormOpen: Writable<boolean> = writable<boolean>(false); \ No newline at end of file | ||