basical functionality complete

This commit is contained in:
Spencer Flagg 2024-01-24 12:46:09 +01:00
parent 543696dd7c
commit b5ebf99ff2
10 changed files with 545 additions and 3 deletions

42
package-lock.json generated
View file

@ -7,6 +7,11 @@
"": {
"name": "baby-tracker",
"version": "0.0.1",
"dependencies": {
"milligram": "^1.4.1",
"pocketbase": "^0.20.3",
"svelte-calendar": "^3.1.6"
},
"devDependencies": {
"@playwright/test": "^1.28.1",
"@sveltejs/adapter-auto": "^3.0.0",
@ -1615,6 +1620,11 @@
"node": ">=4"
}
},
"node_modules/dayjs": {
"version": "1.11.10",
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.10.tgz",
"integrity": "sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ=="
},
"node_modules/debug": {
"version": "4.3.4",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
@ -2494,6 +2504,11 @@
"integrity": "sha512-AilxAyFOAcK5wA1+LeaySVBrHsGQvUFCDWXKpZjzaL0PqW+xfBOttn8GNtWKFWqneyMZj41MWF9Kl6iPWLwgOA==",
"dev": true
},
"node_modules/just-throttle": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/just-throttle/-/just-throttle-2.3.1.tgz",
"integrity": "sha512-0H4miIAWZYpnpg7oD/Y/PBb77ISSHAETif5xK9EnwIgYCO6oC8ErkJxDumMUTR44shSOwptRIArRuvNuvN/hOw=="
},
"node_modules/keyv": {
"version": "4.5.4",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
@ -2650,6 +2665,14 @@
"node": ">=8.6"
}
},
"node_modules/milligram": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/milligram/-/milligram-1.4.1.tgz",
"integrity": "sha512-RCgh/boHhcXWOUfKJWm3RJRoUeaEguoipDg0mJ31G0tFfvcpWMUlO1Zlqqr12K4kAXfDlllaidu0x7PaL2PTFg==",
"dependencies": {
"normalize.css": "~8.0.1"
}
},
"node_modules/mimic-fn": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz",
@ -2776,6 +2799,11 @@
"node": ">=0.10.0"
}
},
"node_modules/normalize.css": {
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/normalize.css/-/normalize.css-8.0.1.tgz",
"integrity": "sha512-qizSNPO93t1YUuUhP22btGOo3chcvDFqFaj2TRybP0DMxkHOCTYwp3n34fel4a31ORXy4m1Xq0Gyqpb5m33qIg=="
},
"node_modules/npm-run-path": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.2.0.tgz",
@ -3007,6 +3035,11 @@
"node": ">=16"
}
},
"node_modules/pocketbase": {
"version": "0.20.3",
"resolved": "https://registry.npmjs.org/pocketbase/-/pocketbase-0.20.3.tgz",
"integrity": "sha512-qembHhE7HumDBZpxWgFIbhJPeaCoUIdwhW59xF/VlMR79pDTYz/LaQ4q89y7GczKo4X9actFgFN8hs4dTl0spQ=="
},
"node_modules/postcss": {
"version": "8.4.33",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.33.tgz",
@ -3567,6 +3600,15 @@
"node": ">=16"
}
},
"node_modules/svelte-calendar": {
"version": "3.1.6",
"resolved": "https://registry.npmjs.org/svelte-calendar/-/svelte-calendar-3.1.6.tgz",
"integrity": "sha512-jOHiPlxBAa1LGVFQZoczdAGnCSI2RKcuQQHj32TjLbXO/P2Povx4JphPEcP7XjKj/s/jimBC6xQyFhyjD0vxbg==",
"dependencies": {
"dayjs": "^1.10.6",
"just-throttle": "^2.3.1"
}
},
"node_modules/svelte-check": {
"version": "3.6.3",
"resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-3.6.3.tgz",

View file

@ -34,5 +34,10 @@
"vite": "^5.0.3",
"vitest": "^1.2.0"
},
"type": "module"
"type": "module",
"dependencies": {
"milligram": "^1.4.1",
"pocketbase": "^0.20.3",
"svelte-calendar": "^3.1.6"
}
}

18
src/lib/pocketbase.ts Normal file
View file

@ -0,0 +1,18 @@
// import PocketBase from 'pocketbase';
// const client = new PocketBase('https://pb.altweb.me'); //:8090
// export default client;
import PocketBase from 'pocketbase';
import { writable } from 'svelte/store';
export const pb = new PocketBase('https://pb.altweb.me'); // remote
// const pb = new PocketBase('http://127.0.0.1:8090'); // local
export const currentUser = writable(pb.authStore.model);
pb.authStore.onChange((auth) => {
console.log('authStore changed', auth);
currentUser.set(pb.authStore.model);
});

13
src/lib/userStore.ts Normal file
View file

@ -0,0 +1,13 @@
import { writable } from 'svelte/store';
interface UserRecord {
username: string;
email: string;
password: string;
}
interface User {
record: UserRecord;
}
export const user = writable<User>();

35
src/routes/+layout.svelte Normal file
View file

@ -0,0 +1,35 @@
<svelte:head>
<!-- Google Fonts -->
<link rel="stylesheet" href="https://api.fonts.coollabs.io/css?family=Roboto:300,300italic,700,700italic">
<!-- CSS Reset -->
<link rel="stylesheet" href="node_modules/normalize.css/normalize.css">
<!-- Milligram CSS -->
<link rel="stylesheet" href="node_modules/milligram/dist/milligram.min.css">
<link rel="stylesheet" href="src/styles/app.css">
<title>Coover Tracker</title>
</svelte:head>
<script lang="ts">
import { currentUser } from '$lib/pocketbase';
</script>
<main class="container">
<header>
<h1>Coover Tracker</h1>
<nav>
<a href="/">Home</a>
{#if $currentUser}
<a href="/stats">Stats</a>
<a href="/calendar">Calendar</a>
{/if}
<a href="/signin">{ $currentUser ? $currentUser.username : 'Sign In'}</a>
</nav>
</header>
<slot/>
</main>

View file

@ -1,2 +1,56 @@
<h1>Welcome to SvelteKit</h1>
<p>Visit <a href="https://kit.svelte.dev">kit.svelte.dev</a> to read the documentation</p>
<script lang="ts">
import { onMount } from 'svelte';
import { pb, currentUser } from '$lib/pocketbase';
import { user } from '$lib/userStore';
type Action = {
type: string;
created: string;
};
let actions: Action[] = [];
onMount(async () => {
await fetchActions();
});
async function recordAction(actionType: string) {
const action = { // No longer specifying a timestamp here
type: actionType,
};
// Send action to PocketBase
await pb.collection('actions').create(action);
// Refresh actions list
await fetchActions();
}
async function fetchActions() {
// 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}'`
});
// Assuming the items are in the 'items' property of the result
actions = result.items;
}
</script>
{#if $currentUser}
<div>
<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>
<h2>Today</h2>
<ul>
{#each actions as action}
<li>{action.type} at {new Date(action.created).toLocaleTimeString('en-NL', { hour: '2-digit', minute: '2-digit' })}</li>
{/each}
</ul>
</div>
{/if}

View file

@ -0,0 +1,79 @@
<script lang="ts">
import { onMount } from 'svelte';
import { pb } from '$lib/pocketbase';
type Action = {
type: string;
created: string;
};
onMount(async () => {
actions = await fetchAllActions();
});
async function fetchAllActions() {
// Fetch all actions. Adjust as needed for date range or pagination
const result = await pb.collection('actions').getList(1, 1000);
return result.items;
}
let actions: Action[] = [];
// Function to get the day of the week from a date string
const getDayOfWeek = (dateString: string) => {
return new Date(dateString).getDay();
};
// Function to get the hour of the day from a date string
const getHourOfDay = (dateString: string) => {
return new Date(dateString).getHours();
};
// Days of the week for headers
const daysOfWeek = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
// Generate hours for the grid
const hours = Array.from({ length: 24 }, (_, i) => i);
</script>
<style>
.calendar {
display: grid;
grid-template-columns: repeat(7, 1fr);
text-align: center;
}
.day-header {
font-weight: bold;
}
.hour {
border: 1px solid #ccc;
min-height: 60px;
}
.event {
background-color: lightblue;
border-radius: 4px;
padding: 2px;
text-align: left;
}
</style>
<div class="calendar">
<!-- Day Headers -->
{#each daysOfWeek as day}
<div class="day-header">{day}</div>
{/each}
<!-- Calendar Grid -->
{#each hours as hour}
{#each daysOfWeek as _, dayIndex}
<div class="hour">
{#each actions as action}
{#if getDayOfWeek(action.created) === dayIndex && getHourOfDay(action.created) === hour}
<div class="event">{action.type}</div>
{/if}
{/each}
</div>
{/each}
{/each}
</div>

View file

@ -0,0 +1,91 @@
<script lang="ts">
import { pb, currentUser } from '$lib/pocketbase';
import { goto } from '$app/navigation';
import { user } from '$lib/userStore';
interface UserData {
avatar: ImageData
collectionId: string
collectionName: string
created: string
email: string
emailVisibility: boolean
id: string
name: string
updated: string
username: string
verified: boolean
}
let username = '';
let email = 'irinavandijk@hotmail.com';
let password = '0lt*4ZruRaBAO$qo';
let passwordConfirm = '';
async function login(): Promise<void> {
try {
const userData = await pb.collection('users').authWithPassword(email, password);
if (userData) {
console.log('Login successful');
user.set(userData); // Update the user store
goto('/'); // Redirect to home page
} else {
console.error('Login failed');
}
} catch (error) {
console.error('Error during login:', error);
}
}
async function signUp() {
try {
console.log(email);
const user = await pb.collection('users').create({
username: username,
email,
password,
passwordConfirm: password
});
await login();
// Redirect to sign-in or dashboard
} catch (error) {
console.error(error);
}
}
async function signIn() {
try {
await login();
} catch (error) {
console.error(error);
}
}
function signOut() {
pb.authStore.clear();
}
</script>
{#if $currentUser}
<p>
Signed in as {$currentUser.username}
<button on:click={signOut}>Sign Out</button>
</p>
{:else}
<form on:submit|preventDefault={signUp}>
<h1>Sign Up</h1>
<input type="text" bind:value={username} placeholder="Username" />
<input type="email" bind:value={email} placeholder="Email" />
<input type="password" bind:value={password} placeholder="Password" />
<input type="password" bind:value={passwordConfirm} placeholder="Confirm Password" />
<button type="submit">Sign Up</button>
</form>
<form on:submit|preventDefault={signIn}>
<h1>Sign In</h1>
<input type="email" bind:value={email} placeholder="Email" />
<input type="password" bind:value={password} placeholder="Password" />
<button type="submit">Sign In</button>
</form>
{/if}

View file

@ -0,0 +1,187 @@
<script lang="ts">
import { onMount } from 'svelte';
import { pb } from '$lib/pocketbase';
type Action = {
type: string;
created: string;
};
let stats = {
avgSleepPerNap: 0,
avgNapsPerDay: 0,
avgSleepPerDay: 0,
avgDiaperChanges: 0,
avgPoops: 0,
avgEatingTimes: 0
};
onMount(async () => {
const actions = await fetchAllActions();
calculateStats(actions);
});
async function fetchAllActions() {
// Fetch all actions. Adjust as needed for date range or pagination
const result = await pb.collection('actions').getList(1, 1000);
return result.items;
}
function calculateStats(actions: Action[]) {
// Implement the logic to calculate each statistic
// This is a placeholder and should be replaced with actual calculation logic
stats.avgSleepPerNap = calculateAvgSleepPerNap(actions);
stats.avgNapsPerDay = calculateAvgNapsPerDay(actions);
stats.avgSleepPerDay = calculateAvgSleepPerDay(actions);
stats.avgDiaperChanges = calculateAvgDiaperChanges(actions);
stats.avgPoops = calculateAvgDaysBetweenPoops(actions);
stats.avgEatingTimes = calculateAvgMealsPerDay(actions);
}
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());
let totalSleepTime = 0;
let napCount = 0;
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();
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()
);
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);
if (asleepDate.toDateString() === awakeDate.toDateString()) {
const dayKey = asleepDate.toDateString();
daysMap.set(dayKey, (daysMap.get(dayKey) || 0) + 1);
}
}
}
const totalNaps = Array.from(daysMap.values()).reduce((acc, naps) => acc + naps, 0);
return daysMap.size > 0 ? totalNaps / daysMap.size : 0;
}
function calculateAvgSleepPerDay(actions: Action[]): number {
const sortedActions = actions.sort(
(a, b) => new Date(a.created).getTime() - new Date(b.created).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);
if (asleepDate.toDateString() === awakeDate.toDateString()) {
const sleepDuration = (awakeDate.getTime() - asleepDate.getTime()) / 1000 / 60; // Sleep duration in minutes
sleepDurations.push(sleepDuration);
}
}
}
if (sleepDurations.length === 0) return 0;
const totalSleepDuration = sleepDurations.reduce((a, b) => a + b, 0);
return Math.round(totalSleepDuration / sleepDurations.length);
}
function calculateAvgDiaperChanges(actions: Action[]): number {
let diaperChangesPerDay = new Map<string, number>();
for (const action of actions) {
if (action.type === 'diaper') {
const actionDate = action.created.split('T')[0]; // Extract the date part
diaperChangesPerDay.set(actionDate, (diaperChangesPerDay.get(actionDate) || 0) + 1);
}
}
if (diaperChangesPerDay.size === 0) {
return 0;
}
const totalDiaperChanges = Array.from(diaperChangesPerDay.values()).reduce(
(total, count) => total + count,
0
);
return totalDiaperChanges / diaperChangesPerDay.size;
}
function calculateAvgDaysBetweenPoops(actions: Action[]): number {
let poopDates = [];
// 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
poopDates.push(actionDate);
}
}
// Sort dates
poopDates.sort((a, b) => a.getTime() - b.getTime());
if (poopDates.length < 2) {
return 0; // Not enough data to calculate intervals
}
let totalDays = 0;
for (let i = 1; i < poopDates.length; i++) {
const interval =
(poopDates[i].getTime() - poopDates[i - 1].getTime()) / (1000 * 60 * 60 * 24); // Convert to days
totalDays += interval;
}
return totalDays / (poopDates.length - 1);
}
function calculateAvgMealsPerDay(actions: Action[]): number {
let mealsPerDay = new Map<string, number>();
for (const action of actions) {
if (action.type === 'food') {
const actionDate = action.created.split('T')[0]; // Extract the date part
mealsPerDay.set(actionDate, (mealsPerDay.get(actionDate) || 0) + 1);
}
}
if (mealsPerDay.size === 0) {
return 0;
}
const totalMeals = Array.from(mealsPerDay.values()).reduce((total, count) => total + count, 0);
return totalMeals / mealsPerDay.size;
}
</script>
<main>
<h2>Stats</h2>
<ul>
<li>Average Sleep per Nap: {stats.avgSleepPerNap} minutes</li>
<li>Average Naps per Day: {stats.avgNapsPerDay} naps</li>
<li>Average Sleep per Day: {stats.avgSleepPerDay} minutes</li>
<li>Average Diaper Changes per Day: {stats.avgDiaperChanges}</li>
<li>Average Days Between Poops: {stats.avgPoops}</li>
<li>Average Number of Meals per Day: {stats.avgEatingTimes}</li>
</ul>
</main>

18
src/styles/app.css Normal file
View file

@ -0,0 +1,18 @@
:root {
--c-primary: darkorange; /* Replace #yourColor with your desired color */
}
a,
input[type="text"]:focus,
input[type="email"]:focus,
input[type="password"]:focus,
select:focus,
textarea:focus {
color: var(--c-primary);
}
button,
input[type="submit"] {
background-color: var(--c-primary);
border-color: var(--c-primary);
}