added counters; calendar styling; timestamp replaced created; various tweaks;

This commit is contained in:
Spencer Flagg 2024-01-24 23:13:00 +01:00
parent 781ef76416
commit d61646f5c1
7 changed files with 202 additions and 93 deletions

View file

@ -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>

View file

@ -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>

View file

@ -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} />

View file

@ -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>

View file

@ -8,7 +8,7 @@
avatar: ImageData
collectionId: string
collectionName: string
created: string
timestamp: string
email: string
emailVisibility: boolean
id: string

View file

@ -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);
}
}

View file

@ -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;
}