Working example
This commit is contained in:
parent
5c04a0f1ee
commit
b4444e7bd2
14 changed files with 935 additions and 22 deletions
16
.prettierrc
16
.prettierrc
|
|
@ -1,16 +0,0 @@
|
|||
{
|
||||
"useTabs": true,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "none",
|
||||
"printWidth": 100,
|
||||
"plugins": ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"],
|
||||
"overrides": [
|
||||
{
|
||||
"files": "*.svelte",
|
||||
"options": {
|
||||
"parser": "svelte"
|
||||
}
|
||||
}
|
||||
],
|
||||
"tailwindStylesheet": "./src/app.css"
|
||||
}
|
||||
186
package-lock.json
generated
186
package-lock.json
generated
|
|
@ -7,6 +7,11 @@
|
|||
"": {
|
||||
"name": "egentrening",
|
||||
"version": "0.0.1",
|
||||
"dependencies": {
|
||||
"@types/pg": "^8.15.5",
|
||||
"pg": "^8.16.3",
|
||||
"zod": "^4.1.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sveltejs/adapter-auto": "^6.0.0",
|
||||
"@sveltejs/kit": "^2.22.0",
|
||||
|
|
@ -1225,6 +1230,26 @@
|
|||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "24.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.3.0.tgz",
|
||||
"integrity": "sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~7.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/pg": {
|
||||
"version": "8.15.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.15.5.tgz",
|
||||
"integrity": "sha512-LF7lF6zWEKxuT3/OR8wAZGzkg4ENGXFNyiV/JeOt9z5B+0ZVwbql9McqX5c/WStFq1GaGso7H1AzP/qSzmlCKQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "*",
|
||||
"pg-protocol": "*",
|
||||
"pg-types": "^2.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/acorn": {
|
||||
"version": "8.15.0",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
|
||||
|
|
@ -1833,6 +1858,95 @@
|
|||
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/pg": {
|
||||
"version": "8.16.3",
|
||||
"resolved": "https://registry.npmjs.org/pg/-/pg-8.16.3.tgz",
|
||||
"integrity": "sha512-enxc1h0jA/aq5oSDMvqyW3q89ra6XIIDZgCX9vkMrnz5DFTw/Ny3Li2lFQ+pt3L6MCgm/5o2o8HW9hiJji+xvw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"pg-connection-string": "^2.9.1",
|
||||
"pg-pool": "^3.10.1",
|
||||
"pg-protocol": "^1.10.3",
|
||||
"pg-types": "2.2.0",
|
||||
"pgpass": "1.0.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 16.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"pg-cloudflare": "^1.2.7"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"pg-native": ">=3.0.1"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"pg-native": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/pg-cloudflare": {
|
||||
"version": "1.2.7",
|
||||
"resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.2.7.tgz",
|
||||
"integrity": "sha512-YgCtzMH0ptvZJslLM1ffsY4EuGaU0cx4XSdXLRFae8bPP4dS5xL1tNB3k2o/N64cHJpwU7dxKli/nZ2lUa5fLg==",
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/pg-connection-string": {
|
||||
"version": "2.9.1",
|
||||
"resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.9.1.tgz",
|
||||
"integrity": "sha512-nkc6NpDcvPVpZXxrreI/FOtX3XemeLl8E0qFr6F2Lrm/I8WOnaWNhIPK2Z7OHpw7gh5XJThi6j6ppgNoaT1w4w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/pg-int8": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz",
|
||||
"integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/pg-pool": {
|
||||
"version": "3.10.1",
|
||||
"resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.10.1.tgz",
|
||||
"integrity": "sha512-Tu8jMlcX+9d8+QVzKIvM/uJtp07PKr82IUOYEphaWcoBhIYkoHpLXN3qO59nAI11ripznDsEzEv8nUxBVWajGg==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"pg": ">=8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/pg-protocol": {
|
||||
"version": "1.10.3",
|
||||
"resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.10.3.tgz",
|
||||
"integrity": "sha512-6DIBgBQaTKDJyxnXaLiLR8wBpQQcGWuAESkRBX/t6OwA8YsqP+iVSiond2EDy6Y/dsGk8rh/jtax3js5NeV7JQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/pg-types": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz",
|
||||
"integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"pg-int8": "1.0.1",
|
||||
"postgres-array": "~2.0.0",
|
||||
"postgres-bytea": "~1.0.0",
|
||||
"postgres-date": "~1.0.4",
|
||||
"postgres-interval": "^1.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/pgpass": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz",
|
||||
"integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"split2": "^4.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/picocolors": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||
|
|
@ -1882,6 +1996,45 @@
|
|||
"node": "^10 || ^12 || >=14"
|
||||
}
|
||||
},
|
||||
"node_modules/postgres-array": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz",
|
||||
"integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/postgres-bytea": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz",
|
||||
"integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/postgres-date": {
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz",
|
||||
"integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/postgres-interval": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz",
|
||||
"integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"xtend": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/prettier": {
|
||||
"version": "3.6.2",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz",
|
||||
|
|
@ -2096,6 +2249,15 @@
|
|||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/split2": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz",
|
||||
"integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">= 10.x"
|
||||
}
|
||||
},
|
||||
"node_modules/svelte": {
|
||||
"version": "5.38.6",
|
||||
"resolved": "https://registry.npmjs.org/svelte/-/svelte-5.38.6.tgz",
|
||||
|
|
@ -2226,6 +2388,12 @@
|
|||
"node": ">=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "7.10.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.10.0.tgz",
|
||||
"integrity": "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "7.1.4",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-7.1.4.tgz",
|
||||
|
|
@ -2321,6 +2489,15 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"node_modules/xtend": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
|
||||
"integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/yallist": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz",
|
||||
|
|
@ -2337,6 +2514,15 @@
|
|||
"integrity": "sha512-rAbqEGa8ovJy4pyBxZM70hg4pE6gDgaQ0Sl9M3enG3I0d6H4XSAM3GeNGLKnsBpuijUow064sf7ww1nutC5/3w==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/zod": {
|
||||
"version": "4.1.5",
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-4.1.5.tgz",
|
||||
"integrity": "sha512-rcUUZqlLJgBC33IT3PNMgsCq6TzLQEG/Ei/KTCU0PedSWRMAXoOUN+4t/0H+Q8bdnLPdqUYnvboJT0bn/229qg==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,5 +26,10 @@
|
|||
"tailwindcss": "^4.0.0",
|
||||
"typescript": "^5.0.0",
|
||||
"vite": "^7.0.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/pg": "^8.15.5",
|
||||
"pg": "^8.16.3",
|
||||
"zod": "^4.1.5"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
20
prettier.config.ts
Normal file
20
prettier.config.ts
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
import type { Config } from 'prettier';
|
||||
|
||||
const config: Config = {
|
||||
useTabs: true,
|
||||
singleQuote: true,
|
||||
trailingComma: 'none',
|
||||
printWidth: 100,
|
||||
plugins: ['prettier-plugin-svelte', 'prettier-plugin-tailwindcss'],
|
||||
overrides: [
|
||||
{
|
||||
files: '*.svelte',
|
||||
options: {
|
||||
parser: 'svelte'
|
||||
}
|
||||
}
|
||||
],
|
||||
tailwindStylesheet: './src/app.css'
|
||||
};
|
||||
|
||||
export default config;
|
||||
21
src/hooks.client.ts
Normal file
21
src/hooks.client.ts
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import type { HandleClientError } from '@sveltejs/kit';
|
||||
import { dev } from '$app/environment';
|
||||
|
||||
// Handle client-side errors
|
||||
export const handleError: HandleClientError = ({ error, event }) => {
|
||||
console.error('Client error:', error);
|
||||
|
||||
// Log additional context in development
|
||||
if (dev) {
|
||||
console.error('Event details:', {
|
||||
url: event.url,
|
||||
route: event.route?.id
|
||||
});
|
||||
}
|
||||
|
||||
// Return user-friendly error message
|
||||
return {
|
||||
message: dev ? String(error) : 'Something went wrong',
|
||||
code: 'CLIENT_ERROR'
|
||||
};
|
||||
};
|
||||
35
src/hooks.server.ts
Normal file
35
src/hooks.server.ts
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
import type { Handle, ServerInit } from '@sveltejs/kit';
|
||||
import { dev } from '$app/environment';
|
||||
import { db } from '$lib/db';
|
||||
|
||||
export const init: ServerInit = async () => {
|
||||
db.createTables();
|
||||
console.log('Tables created');
|
||||
};
|
||||
|
||||
// The handle function runs on every request
|
||||
export const handle: Handle = async ({ event, resolve }) => {
|
||||
// Initialize on first request (lazy initialization)
|
||||
|
||||
// Add custom headers or modify request/response if needed
|
||||
const response = await resolve(event);
|
||||
|
||||
// Optional: Add security headers
|
||||
if (!dev) {
|
||||
response.headers.set('X-Content-Type-Options', 'nosniff');
|
||||
response.headers.set('X-Frame-Options', 'DENY');
|
||||
response.headers.set('X-XSS-Protection', '1; mode=block');
|
||||
}
|
||||
|
||||
return response;
|
||||
};
|
||||
|
||||
// Optional: Handle server errors
|
||||
export const handleError = ({ error, event }) => {
|
||||
console.error('Server error:', error);
|
||||
|
||||
// Don't expose sensitive error details in production
|
||||
return {
|
||||
message: dev ? String(error) : 'Internal server error'
|
||||
};
|
||||
};
|
||||
148
src/lib/WorkoutDisplay.svelte
Normal file
148
src/lib/WorkoutDisplay.svelte
Normal file
|
|
@ -0,0 +1,148 @@
|
|||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { getTodaysWorkout } from '../routes/workout.remote';
|
||||
|
||||
// Workout data
|
||||
let workoutData: any = null;
|
||||
let loading = true;
|
||||
let error = '';
|
||||
|
||||
// Today's date for display
|
||||
let todayDate = '';
|
||||
|
||||
onMount(() => {
|
||||
const today = new Date();
|
||||
todayDate = today.toLocaleDateString('en-CA'); // YYYY-MM-DD format
|
||||
loadTodaysWorkout();
|
||||
});
|
||||
|
||||
export async function loadTodaysWorkout() {
|
||||
loading = true;
|
||||
error = '';
|
||||
|
||||
try {
|
||||
const result = await getTodaysWorkout();
|
||||
workoutData = result.data;
|
||||
} catch (err) {
|
||||
error = err instanceof Error ? err.message : 'Failed to load workout data';
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
function formatTime(seconds: number): string {
|
||||
if (seconds === 0) return '0 seconds';
|
||||
if (seconds < 60) return `${seconds} seconds`;
|
||||
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
const remainingSeconds = seconds % 60;
|
||||
|
||||
if (remainingSeconds === 0) return `${minutes} minutes`;
|
||||
return `${minutes}m ${remainingSeconds}s`;
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="mx-auto max-w-md rounded-lg bg-white p-6 shadow-lg">
|
||||
<h2 class="mb-6 text-center text-2xl font-bold text-gray-800">Today's Workout</h2>
|
||||
|
||||
<div class="mb-4 text-center text-gray-600">
|
||||
📅 {todayDate}
|
||||
</div>
|
||||
|
||||
{#if loading}
|
||||
<div class="flex items-center justify-center py-8">
|
||||
<div class="text-gray-500">⏳ Loading workout data...</div>
|
||||
</div>
|
||||
{:else if error}
|
||||
<div class="rounded-md border border-red-200 bg-red-100 p-4 text-red-800">
|
||||
<div class="font-medium">Error loading data</div>
|
||||
<div class="text-sm">{error}</div>
|
||||
<button
|
||||
on:click={loadTodaysWorkout}
|
||||
class="mt-2 rounded bg-red-200 px-3 py-1 text-xs text-red-700 hover:bg-red-300"
|
||||
>
|
||||
🔄 Retry
|
||||
</button>
|
||||
</div>
|
||||
{:else if !workoutData}
|
||||
<div class="rounded-md bg-gray-50 p-6 text-center">
|
||||
<div class="text-gray-600">📝 No workout recorded for today</div>
|
||||
<div class="mt-2 text-sm text-gray-500">Start logging your exercises below!</div>
|
||||
</div>
|
||||
{:else}
|
||||
<!-- Workout Stats Grid -->
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<!-- Push-ups -->
|
||||
<div class="rounded-lg bg-blue-50 p-4 text-center">
|
||||
<div class="text-2xl">💪</div>
|
||||
<div class="text-lg font-bold text-blue-700">{workoutData.pushups}</div>
|
||||
<div class="text-sm text-blue-600">Push-ups</div>
|
||||
</div>
|
||||
|
||||
<!-- Sit-ups -->
|
||||
<div class="rounded-lg bg-green-50 p-4 text-center">
|
||||
<div class="text-2xl">🏋️</div>
|
||||
<div class="text-lg font-bold text-green-700">{workoutData.situps}</div>
|
||||
<div class="text-sm text-green-600">Sit-ups</div>
|
||||
</div>
|
||||
|
||||
<!-- Plank -->
|
||||
<div class="rounded-lg bg-orange-50 p-4 text-center">
|
||||
<div class="text-2xl">🧘</div>
|
||||
<div class="text-lg font-bold text-orange-700">
|
||||
{formatTime(workoutData.plank_time_seconds)}
|
||||
</div>
|
||||
<div class="text-sm text-orange-600">Plank</div>
|
||||
</div>
|
||||
|
||||
<!-- Running -->
|
||||
<div class="rounded-lg bg-red-50 p-4 text-center">
|
||||
<div class="text-2xl">🏃</div>
|
||||
<div class="text-lg font-bold text-red-700">{workoutData.run_distance_km} km</div>
|
||||
<div class="text-sm text-red-600">Running</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Summary Stats -->
|
||||
<div class="mt-6 rounded-md bg-gray-50 p-4">
|
||||
<h3 class="mb-2 font-medium text-gray-700">📊 Summary</h3>
|
||||
<div class="space-y-1 text-sm text-gray-600">
|
||||
<div class="flex justify-between">
|
||||
<span>Total exercises:</span>
|
||||
<span class="font-medium">{workoutData.pushups + workoutData.situps}</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span>Plank time:</span>
|
||||
<span class="font-medium">{formatTime(workoutData.plank_time_seconds)}</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span>Distance:</span>
|
||||
<span class="font-medium">{workoutData.run_distance_km} km</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Timestamps -->
|
||||
{#if workoutData.created_at || workoutData.updated_at}
|
||||
<div class="mt-4 text-xs text-gray-500">
|
||||
{#if workoutData.created_at}
|
||||
<div>Created: {new Date(workoutData.created_at).toLocaleString()}</div>
|
||||
{/if}
|
||||
{#if workoutData.updated_at && workoutData.updated_at !== workoutData.created_at}
|
||||
<div>Updated: {new Date(workoutData.updated_at).toLocaleString()}</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
<!-- Refresh button -->
|
||||
<div class="mt-6 text-center">
|
||||
<button
|
||||
on:click={loadTodaysWorkout}
|
||||
disabled={loading}
|
||||
class="rounded bg-gray-200 px-4 py-2 text-sm text-gray-700 hover:bg-gray-300 disabled:opacity-50"
|
||||
>
|
||||
{loading ? '⏳ Loading...' : '🔄 Refresh'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
271
src/lib/WorkoutLogger.svelte
Normal file
271
src/lib/WorkoutLogger.svelte
Normal file
|
|
@ -0,0 +1,271 @@
|
|||
<script lang="ts">
|
||||
import { onMount, createEventDispatcher } from 'svelte';
|
||||
import { saveWorkout as saveWorkoutRemote, getTodaysWorkout } from '../routes/workout.remote';
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
// Form data
|
||||
let pushups = 0;
|
||||
let situps = 0;
|
||||
let plankSeconds = 0;
|
||||
let runKm = 0.0;
|
||||
|
||||
// UI state
|
||||
let loading = false;
|
||||
let message = '';
|
||||
let messageType: 'success' | 'error' | '' = '';
|
||||
|
||||
// Today's date for display
|
||||
let todayDate = '';
|
||||
|
||||
onMount(() => {
|
||||
const today = new Date();
|
||||
todayDate = today.toLocaleDateString('en-CA'); // YYYY-MM-DD format
|
||||
loadTodaysWorkout();
|
||||
});
|
||||
|
||||
async function loadTodaysWorkout() {
|
||||
try {
|
||||
const result = await getTodaysWorkout();
|
||||
if (result.data) {
|
||||
pushups = result.data.pushups || 0;
|
||||
situps = result.data.situps || 0;
|
||||
plankSeconds = result.data.plank_time_seconds || 0;
|
||||
runKm = result.data.run_distance_km || 0.0;
|
||||
}
|
||||
} catch (error) {
|
||||
// If no data for today, that's fine - keep defaults
|
||||
console.log('No workout data for today yet');
|
||||
}
|
||||
}
|
||||
|
||||
async function saveWorkout() {
|
||||
loading = true;
|
||||
message = '';
|
||||
|
||||
try {
|
||||
const result = await saveWorkoutRemote({
|
||||
pushups,
|
||||
situps,
|
||||
plankSeconds,
|
||||
runKm
|
||||
});
|
||||
|
||||
message = result.message || 'Workout saved successfully! 🎉';
|
||||
messageType = 'success';
|
||||
|
||||
// Dispatch event to refresh display
|
||||
dispatch('workoutSaved');
|
||||
} catch (error) {
|
||||
message = error instanceof Error ? error.message : 'Error saving workout. Please try again.';
|
||||
messageType = 'error';
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
function resetForm() {
|
||||
pushups = 0;
|
||||
situps = 0;
|
||||
plankSeconds = 0;
|
||||
runKm = 0.0;
|
||||
message = '';
|
||||
messageType = '';
|
||||
}
|
||||
|
||||
// Quick add buttons
|
||||
function quickAdd(exercise: string, amount: number) {
|
||||
switch (exercise) {
|
||||
case 'pushups':
|
||||
pushups += amount;
|
||||
break;
|
||||
case 'situps':
|
||||
situps += amount;
|
||||
break;
|
||||
case 'plank':
|
||||
plankSeconds += amount;
|
||||
break;
|
||||
case 'run':
|
||||
runKm += amount;
|
||||
break;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="mx-auto max-w-md rounded-lg bg-white p-6 shadow-lg">
|
||||
<h2 class="mb-6 text-center text-2xl font-bold text-gray-800">Log Today's Workout</h2>
|
||||
|
||||
<div class="mb-4 text-center text-gray-600">
|
||||
📅 {todayDate}
|
||||
</div>
|
||||
|
||||
<!-- Push-ups -->
|
||||
<div class="mb-6">
|
||||
<label for="pushups" class="mb-2 block text-sm font-medium text-gray-700"> 💪 Push-ups </label>
|
||||
<div class="flex items-center space-x-2">
|
||||
<input
|
||||
id="pushups"
|
||||
type="number"
|
||||
bind:value={pushups}
|
||||
min="0"
|
||||
class="flex-1 rounded-md border border-gray-300 px-3 py-2 focus:ring-2 focus:ring-blue-500 focus:outline-none"
|
||||
placeholder="0"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
on:click={() => quickAdd('pushups', 10)}
|
||||
class="rounded bg-blue-100 px-3 py-1 text-xs text-blue-700 hover:bg-blue-200"
|
||||
>
|
||||
+10
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
on:click={() => quickAdd('pushups', 25)}
|
||||
class="rounded bg-blue-100 px-3 py-1 text-xs text-blue-700 hover:bg-blue-200"
|
||||
>
|
||||
+25
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sit-ups -->
|
||||
<div class="mb-6">
|
||||
<label for="situps" class="mb-2 block text-sm font-medium text-gray-700"> 🏋️ Sit-ups </label>
|
||||
<div class="flex items-center space-x-2">
|
||||
<input
|
||||
id="situps"
|
||||
type="number"
|
||||
bind:value={situps}
|
||||
min="0"
|
||||
class="flex-1 rounded-md border border-gray-300 px-3 py-2 focus:ring-2 focus:ring-blue-500 focus:outline-none"
|
||||
placeholder="0"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
on:click={() => quickAdd('situps', 10)}
|
||||
class="rounded bg-green-100 px-3 py-1 text-xs text-green-700 hover:bg-green-200"
|
||||
>
|
||||
+10
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
on:click={() => quickAdd('situps', 20)}
|
||||
class="rounded bg-green-100 px-3 py-1 text-xs text-green-700 hover:bg-green-200"
|
||||
>
|
||||
+20
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Plank -->
|
||||
<div class="mb-6">
|
||||
<label for="plank" class="mb-2 block text-sm font-medium text-gray-700">
|
||||
🧘 Plank (seconds)
|
||||
</label>
|
||||
<div class="flex items-center space-x-2">
|
||||
<input
|
||||
id="plank"
|
||||
type="number"
|
||||
bind:value={plankSeconds}
|
||||
min="0"
|
||||
class="flex-1 rounded-md border border-gray-300 px-3 py-2 focus:ring-2 focus:ring-blue-500 focus:outline-none"
|
||||
placeholder="0"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
on:click={() => quickAdd('plank', 30)}
|
||||
class="rounded bg-orange-100 px-3 py-1 text-xs text-orange-700 hover:bg-orange-200"
|
||||
>
|
||||
+30s
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
on:click={() => quickAdd('plank', 60)}
|
||||
class="rounded bg-orange-100 px-3 py-1 text-xs text-orange-700 hover:bg-orange-200"
|
||||
>
|
||||
+60s
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Running -->
|
||||
<div class="mb-6">
|
||||
<label for="run" class="mb-2 block text-sm font-medium text-gray-700"> 🏃 Running (km) </label>
|
||||
<div class="flex items-center space-x-2">
|
||||
<input
|
||||
id="run"
|
||||
type="number"
|
||||
bind:value={runKm}
|
||||
min="0"
|
||||
step="0.1"
|
||||
class="flex-1 rounded-md border border-gray-300 px-3 py-2 focus:ring-2 focus:ring-blue-500 focus:outline-none"
|
||||
placeholder="0.0"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
on:click={() => quickAdd('run', 1)}
|
||||
class="rounded bg-red-100 px-3 py-1 text-xs text-red-700 hover:bg-red-200"
|
||||
>
|
||||
+1km
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
on:click={() => quickAdd('run', 2.5)}
|
||||
class="rounded bg-red-100 px-3 py-1 text-xs text-red-700 hover:bg-red-200"
|
||||
>
|
||||
+2.5km
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Action buttons -->
|
||||
<div class="mb-4 flex space-x-3">
|
||||
<button
|
||||
on:click={saveWorkout}
|
||||
disabled={loading}
|
||||
class="flex-1 rounded-md bg-blue-600 px-4 py-2 font-medium text-white hover:bg-blue-700 disabled:cursor-not-allowed disabled:opacity-50"
|
||||
>
|
||||
{loading ? '⏳ Saving...' : '💾 Save Workout'}
|
||||
</button>
|
||||
<button
|
||||
on:click={resetForm}
|
||||
disabled={loading}
|
||||
class="rounded-md bg-gray-300 px-4 py-2 text-gray-700 hover:bg-gray-400 disabled:opacity-50"
|
||||
>
|
||||
🔄 Reset
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Message display -->
|
||||
{#if message}
|
||||
<div
|
||||
class="rounded-md p-3 text-sm font-medium {messageType === 'success'
|
||||
? 'border border-green-200 bg-green-100 text-green-800'
|
||||
: 'border border-red-200 bg-red-100 text-red-800'}"
|
||||
>
|
||||
{message}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Quick example -->
|
||||
<div class="mt-6 rounded-md bg-gray-50 p-4">
|
||||
<h3 class="mb-2 text-sm font-medium text-gray-700">Your Example:</h3>
|
||||
<div class="space-y-1 text-sm text-gray-600">
|
||||
<div>💪 Push-ups: 100</div>
|
||||
<div>🏋️ Sit-ups: 50</div>
|
||||
<div>🧘 Plank: 0 seconds</div>
|
||||
<div>🏃 Running: 4.0 km</div>
|
||||
</div>
|
||||
<button
|
||||
on:click={() => {
|
||||
pushups = 100;
|
||||
situps = 50;
|
||||
plankSeconds = 0;
|
||||
runKm = 4.0;
|
||||
}}
|
||||
class="mt-2 rounded bg-gray-200 px-2 py-1 text-xs text-gray-700 hover:bg-gray-300"
|
||||
>
|
||||
Load Example
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
81
src/lib/db.ts
Normal file
81
src/lib/db.ts
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
import { DATABASE_URL } from '$env/static/private';
|
||||
import { Pool } from 'pg';
|
||||
|
||||
// Create a connection pool
|
||||
const pool = new Pool({
|
||||
connectionString: DATABASE_URL,
|
||||
ssl: false
|
||||
});
|
||||
|
||||
// Simple database client
|
||||
export const db = {
|
||||
// Execute a query
|
||||
async query(text: string, params?: any[]) {
|
||||
const client = await pool.connect();
|
||||
try {
|
||||
const result = await client.query(text, params);
|
||||
return result;
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
},
|
||||
|
||||
// Test connection
|
||||
async testConnection() {
|
||||
try {
|
||||
const result = await this.query('SELECT NOW() as current_time');
|
||||
console.log('Database connected successfully:', result.rows[0]);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Database connection failed:', error);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
// Create tables for tracking daily exercises
|
||||
async createTables() {
|
||||
// Create exercises table with all activities in one table
|
||||
await this.query(`
|
||||
CREATE TABLE IF NOT EXISTS daily_exercises (
|
||||
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||
date DATE NOT NULL UNIQUE,
|
||||
pushups INTEGER DEFAULT 0,
|
||||
situps INTEGER DEFAULT 0,
|
||||
plank_time_seconds INTEGER DEFAULT 0,
|
||||
run_distance_km DECIMAL(5,2) DEFAULT 0.0,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
`);
|
||||
|
||||
// Create index for faster date lookups
|
||||
await this.query(`
|
||||
CREATE INDEX IF NOT EXISTS idx_daily_exercises_date
|
||||
ON daily_exercises(date)
|
||||
`);
|
||||
|
||||
// Create trigger to update updated_at timestamp
|
||||
await this.query(`
|
||||
CREATE OR REPLACE FUNCTION update_updated_at_column()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
NEW.updated_at = CURRENT_TIMESTAMP;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ language 'plpgsql'
|
||||
`);
|
||||
|
||||
await this.query(`
|
||||
DROP TRIGGER IF EXISTS update_daily_exercises_updated_at ON daily_exercises;
|
||||
CREATE TRIGGER update_daily_exercises_updated_at
|
||||
BEFORE UPDATE ON daily_exercises
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION update_updated_at_column()
|
||||
`);
|
||||
},
|
||||
|
||||
// Close all connections
|
||||
async close() {
|
||||
await pool.end();
|
||||
}
|
||||
};
|
||||
|
|
@ -1,3 +1,32 @@
|
|||
<h1>Welcome to SvelteKit</h1>
|
||||
<p>Here by text</p>
|
||||
<p>Visit <a href="https://svelte.dev/docs/kit">svelte.dev/docs/kit</a> to read the documentation</p>
|
||||
<script lang="ts">
|
||||
import WorkoutLogger from '$lib/WorkoutLogger.svelte';
|
||||
import WorkoutDisplay from '$lib/WorkoutDisplay.svelte';
|
||||
|
||||
let workoutDisplayComponent: WorkoutDisplay;
|
||||
|
||||
function handleWorkoutSaved() {
|
||||
// Refresh the display when workout is saved
|
||||
if (workoutDisplayComponent) {
|
||||
workoutDisplayComponent.loadTodaysWorkout();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="min-h-screen bg-gray-100 py-8">
|
||||
<div class="container mx-auto px-4">
|
||||
<h1 class="mb-8 text-center text-4xl font-bold text-gray-800">🏋️ Egentrening</h1>
|
||||
<p class="mb-8 text-center text-gray-600">Track your daily fitness progress</p>
|
||||
|
||||
<div class="grid grid-cols-1 gap-8 lg:grid-cols-2">
|
||||
<!-- Display today's workout -->
|
||||
<div>
|
||||
<WorkoutDisplay bind:this={workoutDisplayComponent} />
|
||||
</div>
|
||||
|
||||
<!-- Log new workout -->
|
||||
<div>
|
||||
<WorkoutLogger on:workoutSaved={handleWorkoutSaved} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
12
src/routes/DbConnection.svelte
Normal file
12
src/routes/DbConnection.svelte
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
<script lang="ts">
|
||||
import { testConnection } from './db.remote';
|
||||
</script>
|
||||
|
||||
<svelte:boundary>
|
||||
<p>Here be connection: {await testConnection().current}</p>
|
||||
|
||||
|
||||
{#snippet pending()}
|
||||
<p>loading...</p>
|
||||
{/snippet}
|
||||
</svelte:boundary>
|
||||
6
src/routes/db.remote.js
Normal file
6
src/routes/db.remote.js
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
import { query } from '$app/server';
|
||||
import { db } from '$lib/db';
|
||||
|
||||
export const testConnection = query(async () => {
|
||||
return db.testConnection();
|
||||
});
|
||||
109
src/routes/workout.remote.ts
Normal file
109
src/routes/workout.remote.ts
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
import { query } from '$app/server';
|
||||
import { db } from '$lib/db';
|
||||
import { z } from 'zod';
|
||||
|
||||
interface WorkOutData {
|
||||
pushups: number;
|
||||
situps: number;
|
||||
plankSeconds: number;
|
||||
runKm: number;
|
||||
}
|
||||
|
||||
export const saveWorkout = query(
|
||||
z.object({
|
||||
pushups: z.number().min(0),
|
||||
situps: z.number().min(0),
|
||||
plankSeconds: z.number().min(0),
|
||||
runKm: z.number().min(0)
|
||||
}),
|
||||
async (workoutData: WorkOutData) => {
|
||||
const { pushups, situps, plankSeconds, runKm } = workoutData;
|
||||
|
||||
// Validate input data
|
||||
if (typeof pushups !== 'number' || pushups < 0) {
|
||||
throw new Error('Invalid pushups value');
|
||||
}
|
||||
if (typeof situps !== 'number' || situps < 0) {
|
||||
throw new Error('Invalid situps value');
|
||||
}
|
||||
if (typeof plankSeconds !== 'number' || plankSeconds < 0) {
|
||||
throw new Error('Invalid plank time value');
|
||||
}
|
||||
if (typeof runKm !== 'number' || runKm < 0) {
|
||||
throw new Error('Invalid run distance value');
|
||||
}
|
||||
|
||||
try {
|
||||
// Insert or update today's workout
|
||||
const result = await db.query(
|
||||
`
|
||||
INSERT INTO daily_exercises (date, pushups, situps, plank_time_seconds, run_distance_km)
|
||||
VALUES (CURRENT_DATE, $1, $2, $3, $4)
|
||||
ON CONFLICT (date)
|
||||
DO UPDATE SET
|
||||
pushups = $1,
|
||||
situps = $2,
|
||||
plank_time_seconds = $3,
|
||||
run_distance_km = $4,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
RETURNING *
|
||||
`,
|
||||
[pushups, situps, plankSeconds, runKm]
|
||||
);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: result.rows[0],
|
||||
message: 'Workout saved successfully!'
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error saving workout:', error);
|
||||
throw new Error('Failed to save workout to database');
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export const getTodaysWorkout = query(async () => {
|
||||
try {
|
||||
// Get today's workout data
|
||||
const result = await db.query('SELECT * FROM daily_exercises WHERE date = CURRENT_DATE');
|
||||
|
||||
if (result.rows.length === 0) {
|
||||
return {
|
||||
data: null,
|
||||
message: 'No workout recorded for today'
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: result.rows[0]
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error fetching workout:', error);
|
||||
throw new Error('Failed to fetch workout from database');
|
||||
}
|
||||
});
|
||||
|
||||
export const getWorkoutHistory = query(async (days: number = 7) => {
|
||||
try {
|
||||
// Get workout history for the last N days
|
||||
const result = await db.query(
|
||||
`
|
||||
SELECT * FROM daily_exercises
|
||||
WHERE date >= CURRENT_DATE - INTERVAL '$1 days'
|
||||
ORDER BY date DESC
|
||||
`,
|
||||
[days]
|
||||
);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: result.rows,
|
||||
message: `Retrieved ${result.rows.length} workout records`
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error fetching workout history:', error);
|
||||
throw new Error('Failed to fetch workout history from database');
|
||||
}
|
||||
});
|
||||
|
|
@ -1,12 +1,18 @@
|
|||
import adapter from '@sveltejs/adapter-auto';
|
||||
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
|
||||
import type { Config } from '@sveltejs/kit';
|
||||
|
||||
/** @type {import('@sveltejs/kit').Config} */
|
||||
const config = {
|
||||
const config: Config = {
|
||||
// Consult https://svelte.dev/docs/kit/integrations
|
||||
// for more information about preprocessors
|
||||
preprocess: vitePreprocess(),
|
||||
kit: { adapter: adapter() }
|
||||
compilerOptions: {
|
||||
experimental: { async: true }
|
||||
},
|
||||
kit: {
|
||||
adapter: adapter(),
|
||||
experimental: { remoteFunctions: true }
|
||||
}
|
||||
};
|
||||
|
||||
export default config;
|
||||
Loading…
Add table
Add a link
Reference in a new issue