Working example

This commit is contained in:
Håkon Størdal 2025-09-02 22:51:17 +02:00
parent 5c04a0f1ee
commit b4444e7bd2
14 changed files with 935 additions and 22 deletions

21
src/hooks.client.ts Normal file
View 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
View 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'
};
};

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

View 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
View 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();
}
};

View file

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

View 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
View file

@ -0,0 +1,6 @@
import { query } from '$app/server';
import { db } from '$lib/db';
export const testConnection = query(async () => {
return db.testConnection();
});

View 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');
}
});