added counters; calendar styling; timestamp replaced created; various tweaks;
This commit is contained in:
parent
781ef76416
commit
d61646f5c1
7 changed files with 202 additions and 93 deletions
|
|
@ -1,4 +1,4 @@
|
|||
<script>
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
|
|
@ -13,7 +13,7 @@
|
|||
|
||||
const getLastActionTime = (type) => {
|
||||
const lastAction = actions.filter(action => action.type === type).pop();
|
||||
return lastAction ? new Date(lastAction.created) : null;
|
||||
return lastAction ? new Date(lastAction.timestamp) : null;
|
||||
};
|
||||
|
||||
const calculateDiffMinutes = (date) => {
|
||||
|
|
@ -36,6 +36,16 @@
|
|||
|
||||
// Update immediately when the component has access to actions
|
||||
updateTimes();
|
||||
|
||||
function convertMinutes(minutes: number): string {
|
||||
if (minutes < 60) {
|
||||
return `${minutes}m`;
|
||||
} else {
|
||||
const hours = Math.floor(minutes / 60);
|
||||
const remainingMinutes = minutes % 60;
|
||||
return `${hours}h${remainingMinutes}m`;
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
const interval = setInterval(updateTimes, 1000); // Update every minute
|
||||
|
|
@ -46,11 +56,11 @@
|
|||
});
|
||||
</script>
|
||||
|
||||
<div class="column">
|
||||
{#if $awakeMinutes !== null && $awakeMinutes < $asleepMinutes}<p>Awake for {$awakeMinutes} minutes</p>{/if}
|
||||
{#if $asleepMinutes !== null && $asleepMinutes < $awakeMinutes}<p>Sleeping for {$asleepMinutes} minutes</p>{/if}
|
||||
{#if $foodMinutes !== null}<p>Ate {$foodMinutes} minutes ago</p>{/if}
|
||||
{#if $diaperMinutes !== null}<p>Diaper changed {$diaperMinutes} minutes ago</p>{/if}
|
||||
{#if $poopDays === 0}<p>Pooped today</p>{:else if $poopDays !== null}<p>Pooped {$poopDays} days ago</p>{/if}
|
||||
<div class="counters">
|
||||
{#if $awakeMinutes !== null && $awakeMinutes < $asleepMinutes}<div>Awake for <strong>{convertMinutes($awakeMinutes)}</strong></div>{/if}
|
||||
{#if $asleepMinutes !== null && $asleepMinutes < $awakeMinutes}<div>Sleeping for <strong>{convertMinutes($asleepMinutes)}</strong></div>{/if}
|
||||
{#if $foodMinutes !== null}<div>Ate <strong>{convertMinutes($foodMinutes)}</strong> ago</div>{/if}
|
||||
{#if $diaperMinutes !== null}<div>Changed <strong>{convertMinutes($diaperMinutes)}</strong> ago</div>{/if}
|
||||
{#if $poopDays === 0}<div>Pooped today!</div>{:else if $poopDays !== null}<div>Pooped <strong>{$poopDays} days</strong> ago</div>{/if}
|
||||
</div>
|
||||
|
||||
|
|
@ -1,7 +1,6 @@
|
|||
<svelte:head>
|
||||
<!-- Google Fonts -->
|
||||
<link rel="stylesheet" href="https://api.fonts.coollabs.io/css?family=Roboto:300,300italic,700,700italic">
|
||||
|
||||
<link href="https://api.fonts.coollabs.io/css2?family=Roboto:ital,wght@0,100;0,300;0,400;0,500;0,700;0,900;1,100;1,300;1,400;1,500;1,700;1,900&display=swap" rel="stylesheet">
|
||||
<title>Coover Tracker</title>
|
||||
|
||||
</svelte:head>
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
|
||||
type Action = {
|
||||
type: string;
|
||||
created: string;
|
||||
timestamp: string;
|
||||
};
|
||||
|
||||
let actions: Action[] = [];
|
||||
|
|
@ -17,6 +17,7 @@
|
|||
async function recordAction(actionType: string) {
|
||||
const action = { // No longer specifying a timestamp here
|
||||
type: actionType,
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
|
||||
// Send action to PocketBase
|
||||
|
|
@ -30,7 +31,7 @@
|
|||
// Fetch actions for the current day from PocketBase
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
const result = await pb.collection('actions').getList<Action>(1, 50, {
|
||||
filter: `created >= '${today}'`
|
||||
filter: `timestamp >= '${today}'`
|
||||
});
|
||||
|
||||
// Assuming the items are in the 'items' property of the result
|
||||
|
|
@ -40,11 +41,11 @@
|
|||
|
||||
{#if $currentUser}
|
||||
<aside>
|
||||
<button on:click={() => recordAction('awake')}>Awake</button>
|
||||
<button on:click={() => recordAction('asleep')}>Asleep</button>
|
||||
<button on:click={() => recordAction('food')}>Food</button>
|
||||
<button on:click={() => recordAction('diaper')}>Diaper</button>
|
||||
<button on:click={() => recordAction('poop')}>Poop</button>
|
||||
<button class="button--awake" on:click={() => recordAction('awake')}>Awake</button>
|
||||
<button class="button--asleep" on:click={() => recordAction('asleep')}>Asleep</button>
|
||||
<button class="button--food" on:click={() => recordAction('food')}>Food</button>
|
||||
<button class="button--diaper" on:click={() => recordAction('diaper')}>Diaper</button>
|
||||
<button class="button--poop" on:click={() => recordAction('poop')}>Poop</button>
|
||||
</aside>
|
||||
|
||||
<section>
|
||||
|
|
@ -53,7 +54,7 @@
|
|||
<div class="row">
|
||||
<ul class="column">
|
||||
{#each actions as action}
|
||||
<li>{action.type} at {new Date(action.created).toLocaleTimeString('en-NL', { hour: '2-digit', minute: '2-digit' })}</li>
|
||||
<li><span class="badge badge--{action.type}">{action.type}</span> at {new Date(action.timestamp).toLocaleTimeString('en-NL', { hour: '2-digit', minute: '2-digit' })}</li>
|
||||
{/each}
|
||||
</ul>
|
||||
<Counters {actions} />
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { onMount } from 'svelte';
|
|||
|
||||
type Action = {
|
||||
type: string;
|
||||
created: string;
|
||||
timestamp: string;
|
||||
};
|
||||
|
||||
onMount(async () => {
|
||||
|
|
@ -30,7 +30,8 @@ import { onMount } from 'svelte';
|
|||
};
|
||||
|
||||
// Days of the week for headers
|
||||
const daysOfWeek = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
|
||||
//const daysOfWeek = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
|
||||
const daysOfWeek = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
|
||||
|
||||
// Generate hours for the grid
|
||||
const hours = Array.from({ length: 24 }, (_, i) => i);
|
||||
|
|
@ -39,67 +40,97 @@ import { onMount } from 'svelte';
|
|||
const formatHour = (hour: number) => {
|
||||
return `${hour.toString().padStart(2, '0')}:00`;
|
||||
};
|
||||
|
||||
// function formatTime(date: Date): string {
|
||||
// const hours = date.getHours().toString().padStart(2, '0');
|
||||
// const minutes = date.getMinutes().toString().padStart(2, '0');
|
||||
// return `${hours}:${minutes}`;
|
||||
// }
|
||||
|
||||
function formatTime(dateString: string): string {
|
||||
const date = new Date(dateString);
|
||||
const hours = date.getHours().toString().padStart(2, '0');
|
||||
const minutes = date.getMinutes().toString().padStart(2, '0');
|
||||
return `${hours}:${minutes}`;
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.calendar-wrapper {
|
||||
max-width: 100%;
|
||||
/* outline: 1px solid #ccc; */
|
||||
overflow-x:auto;
|
||||
position: relative;
|
||||
}
|
||||
.calendar {
|
||||
display: grid;
|
||||
grid-template-columns: 60px repeat(7, 1fr); /* Adjusted for hour column */
|
||||
text-align: center;
|
||||
min-width: 700px;
|
||||
}
|
||||
.day-header, .hour-label {
|
||||
font-weight: bold;
|
||||
border: 1px solid #ccc;
|
||||
font-size: 1.2rem;
|
||||
background: #e9e9e9;
|
||||
}
|
||||
.day-header {
|
||||
border-radius: 1em 1em 0 0;
|
||||
margin-top: 1rem;
|
||||
padding: .5rem;
|
||||
}
|
||||
.hour-label{
|
||||
border-radius: 1em 0 0 1em;
|
||||
padding: .5rem;
|
||||
margin-left: .5rem;
|
||||
}
|
||||
.hour {
|
||||
border: 1px solid #ccc;
|
||||
height: 60px;
|
||||
border: 1px solid #e9e9e9;
|
||||
min-height: 60px;
|
||||
}
|
||||
.event {
|
||||
background-color: lightblue;
|
||||
background-color: var(--color);
|
||||
border-radius: 4px;
|
||||
padding: 2px;
|
||||
text-align: left;
|
||||
padding: .5em 1em;
|
||||
line-height: 1;
|
||||
margin: 2px;
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
color: rgba(255,255,255,.5);
|
||||
}
|
||||
|
||||
.event--food {
|
||||
background-color: lightgreen;
|
||||
}
|
||||
.event--poop {
|
||||
background-color: burlywood;
|
||||
}
|
||||
.event--asleep {
|
||||
background-color: lightblue;
|
||||
}
|
||||
.event--awake {
|
||||
background-color: gold;
|
||||
}
|
||||
.event--diaper {
|
||||
background-color: lightpink;
|
||||
.event__title {
|
||||
font-weight: 900;
|
||||
font-size: 1.2rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: .2em;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
<div class="calendar">
|
||||
<!-- Empty cell for top-left corner -->
|
||||
<div></div>
|
||||
<div class="calendar-wrapper">
|
||||
<div class="calendar">
|
||||
<!-- Empty cell for top-left corner -->
|
||||
<div></div>
|
||||
|
||||
<!-- Day Headers -->
|
||||
{#each daysOfWeek as day}
|
||||
<div class="day-header">{day}</div>
|
||||
{/each}
|
||||
|
||||
<!-- Hour Labels and Calendar Grid -->
|
||||
{#each hours as hour}
|
||||
<div class="hour-label">{formatHour(hour)}</div> <!-- Hour label -->
|
||||
{#each daysOfWeek as _, dayIndex}
|
||||
<div class="hour">
|
||||
{#each actions as action}
|
||||
{#if getDayOfWeek(action.created) === dayIndex && getHourOfDay(action.created) === hour}
|
||||
<div class="event event--{action.type}">{action.type}</div>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
<!-- Day Headers -->
|
||||
{#each daysOfWeek as day}
|
||||
<div class="day-header">{day}</div>
|
||||
{/each}
|
||||
{/each}
|
||||
|
||||
<!-- Hour Labels and Calendar Grid -->
|
||||
{#each hours as hour}
|
||||
<div class="hour-label">{formatHour(hour)}</div> <!-- Hour label -->
|
||||
{#each daysOfWeek as _, dayIndex}
|
||||
<div class="hour">
|
||||
{#each actions as action}
|
||||
{#if getDayOfWeek(action.timestamp) === dayIndex && getHourOfDay(action.timestamp) === hour}
|
||||
<div class="event event--{action.type}"><span class="event__title">{action.type}</span><span>{formatTime(action.timestamp)}</span></div>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
{/each}
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -8,7 +8,7 @@
|
|||
avatar: ImageData
|
||||
collectionId: string
|
||||
collectionName: string
|
||||
created: string
|
||||
timestamp: string
|
||||
email: string
|
||||
emailVisibility: boolean
|
||||
id: string
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
type Action = {
|
||||
type: string;
|
||||
created: string;
|
||||
timestamp: string;
|
||||
};
|
||||
|
||||
let stats = {
|
||||
|
|
@ -40,7 +40,7 @@
|
|||
|
||||
function calculateAvgSleepPerNap(actions: Action[]): number {
|
||||
const sleepActions = actions.filter((a) => a.type === 'asleep' || a.type === 'awake');
|
||||
sleepActions.sort((a, b) => new Date(a.created).getTime() - new Date(b.created).getTime());
|
||||
sleepActions.sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());
|
||||
|
||||
let totalSleepTime = 0;
|
||||
let napCount = 0;
|
||||
|
|
@ -48,28 +48,25 @@
|
|||
for (let i = 0; i < sleepActions.length - 1; i++) {
|
||||
if (sleepActions[i].type === 'asleep' && sleepActions[i + 1].type === 'awake') {
|
||||
totalSleepTime +=
|
||||
new Date(sleepActions[i + 1].created).getTime() -
|
||||
new Date(sleepActions[i].created).getTime();
|
||||
new Date(sleepActions[i + 1].timestamp).getTime() -
|
||||
new Date(sleepActions[i].timestamp).getTime();
|
||||
napCount++;
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Total Sleep Time:', totalSleepTime);
|
||||
console.log('Nap Count:', napCount);
|
||||
|
||||
return Math.round(napCount > 0 ? totalSleepTime / napCount / 1000 / 60 : 0); // returns average time in minutes
|
||||
}
|
||||
|
||||
function calculateAvgNapsPerDay(actions: Action[]): number {
|
||||
const sortedActions = actions.sort(
|
||||
(a, b) => new Date(a.created).getTime() - new Date(b.created).getTime()
|
||||
(a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime()
|
||||
);
|
||||
const daysMap = new Map<string, number>();
|
||||
|
||||
for (let i = 0; i < sortedActions.length - 1; i++) {
|
||||
if (sortedActions[i].type === 'asleep' && sortedActions[i + 1].type === 'awake') {
|
||||
const asleepDate = new Date(sortedActions[i].created);
|
||||
const awakeDate = new Date(sortedActions[i + 1].created);
|
||||
const asleepDate = new Date(sortedActions[i].timestamp);
|
||||
const awakeDate = new Date(sortedActions[i + 1].timestamp);
|
||||
|
||||
if (asleepDate.toDateString() === awakeDate.toDateString()) {
|
||||
const dayKey = asleepDate.toDateString();
|
||||
|
|
@ -84,14 +81,14 @@
|
|||
|
||||
function calculateAvgSleepPerDay(actions: Action[]): number {
|
||||
const sortedActions = actions.sort(
|
||||
(a, b) => new Date(a.created).getTime() - new Date(b.created).getTime()
|
||||
(a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime()
|
||||
);
|
||||
const sleepDurations: number[] = [];
|
||||
|
||||
for (let i = 0; i < sortedActions.length - 1; i++) {
|
||||
if (sortedActions[i].type === 'asleep' && sortedActions[i + 1].type === 'awake') {
|
||||
const asleepDate = new Date(sortedActions[i].created);
|
||||
const awakeDate = new Date(sortedActions[i + 1].created);
|
||||
const asleepDate = new Date(sortedActions[i].timestamp);
|
||||
const awakeDate = new Date(sortedActions[i + 1].timestamp);
|
||||
|
||||
if (asleepDate.toDateString() === awakeDate.toDateString()) {
|
||||
const sleepDuration = (awakeDate.getTime() - asleepDate.getTime()) / 1000 / 60; // Sleep duration in minutes
|
||||
|
|
@ -111,7 +108,7 @@
|
|||
|
||||
for (const action of actions) {
|
||||
if (action.type === 'diaper') {
|
||||
const actionDate = action.created.split('T')[0]; // Extract the date part
|
||||
const actionDate = action.timestamp.split('T')[0]; // Extract the date part
|
||||
diaperChangesPerDay.set(actionDate, (diaperChangesPerDay.get(actionDate) || 0) + 1);
|
||||
}
|
||||
}
|
||||
|
|
@ -133,7 +130,7 @@
|
|||
// Extract dates of 'poop' actions
|
||||
for (const action of actions) {
|
||||
if (action.type === 'poop') {
|
||||
const actionDate = new Date(action.created.split('T')[0]); // Extract the date part
|
||||
const actionDate = new Date(action.timestamp.split('T')[0]); // Extract the date part
|
||||
poopDates.push(actionDate);
|
||||
}
|
||||
}
|
||||
|
|
@ -160,7 +157,7 @@
|
|||
|
||||
for (const action of actions) {
|
||||
if (action.type === 'food') {
|
||||
const actionDate = action.created.split('T')[0]; // Extract the date part
|
||||
const actionDate = action.timestamp.split('T')[0]; // Extract the date part
|
||||
mealsPerDay.set(actionDate, (mealsPerDay.get(actionDate) || 0) + 1);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,34 +1,105 @@
|
|||
:root {
|
||||
--c-primary: darkorange; /* Replace #yourColor with your desired color */
|
||||
--c-primary: darkorange; /* Replace #yourColor with your desired color */
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: #f2f0ef;
|
||||
}
|
||||
|
||||
a,
|
||||
input[type="text"]:focus,
|
||||
input[type="email"]:focus,
|
||||
input[type="password"]:focus,
|
||||
input[type='text']:focus,
|
||||
input[type='email']:focus,
|
||||
input[type='password']:focus,
|
||||
select:focus,
|
||||
textarea:focus {
|
||||
color: var(--c-primary);
|
||||
color: var(--c-primary);
|
||||
}
|
||||
|
||||
button,
|
||||
input[type="submit"] {
|
||||
background-color: var(--c-primary);
|
||||
border-color: var(--c-primary);
|
||||
input[type='submit'] {
|
||||
background-color: var(--color);
|
||||
border-color: var(--color);
|
||||
}
|
||||
|
||||
nav ul {
|
||||
list-style: circle inside;
|
||||
display: flex;
|
||||
list-style: none;
|
||||
gap: 1em;
|
||||
justify-content: end;
|
||||
list-style: circle inside;
|
||||
display: flex;
|
||||
list-style: none;
|
||||
gap: 1em;
|
||||
justify-content: end;
|
||||
}
|
||||
|
||||
nav ul li {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
header {
|
||||
padding-top: 2rem;
|
||||
padding-top: 2rem;
|
||||
}
|
||||
|
||||
/* aside {
|
||||
position: relative;
|
||||
padding-top: 1rem;
|
||||
display: block;
|
||||
} */
|
||||
|
||||
/* aside::after {
|
||||
content: "";
|
||||
width: 100vw;
|
||||
height: 50px;
|
||||
position: fixed;
|
||||
left: 0;
|
||||
outline: 1px solid #ccc;
|
||||
background-color: #eee;
|
||||
z-index: -1;
|
||||
} */
|
||||
|
||||
aside button {
|
||||
margin-top: 1rem;
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
section {
|
||||
margin-top: 4rem;
|
||||
}
|
||||
margin-top: 4rem;
|
||||
}
|
||||
|
||||
[class*='--food'] {
|
||||
--color: #136f63;
|
||||
}
|
||||
[class*='--poop'] {
|
||||
--color: #78290f;
|
||||
}
|
||||
[class*='--asleep'] {
|
||||
--color: #032b43;
|
||||
}
|
||||
[class*='--awake'] {
|
||||
--color: #ffba08;
|
||||
}
|
||||
[class*='--diaper'] {
|
||||
--color: #3f88c5;
|
||||
}
|
||||
/* d00000 ff7d00 */
|
||||
|
||||
.badge {
|
||||
background-color: var(--color);
|
||||
display: inline-block;
|
||||
padding: 0.35em .75em .25em .75em;
|
||||
color: rgba(255, 255, 255, 0.75);
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
border-radius: 0.25rem;
|
||||
font-size: 1rem;
|
||||
line-height: 1;
|
||||
min-width: 10ch;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.counters {
|
||||
position: fixed;
|
||||
right: 0;
|
||||
width: max-content;
|
||||
outline: 1px solid #ccc;
|
||||
background-color: #eee;
|
||||
padding: 2rem;
|
||||
border-radius: 2rem 0 0 2rem;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue